Compare commits

...

1 Commits

Author SHA1 Message Date
johan12345
a22b347edc implement chargeprice feedback dialog
#195
2022-09-04 11:11:46 +02:00
11 changed files with 563 additions and 7 deletions

View File

@@ -0,0 +1,74 @@
package net.vonforst.evmap.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.navigation.ui.setupWithNavController
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.databinding.FragmentChargepriceFeedbackBinding
import net.vonforst.evmap.viewmodel.ChargepriceFeedbackViewModel
import net.vonforst.evmap.viewmodel.viewModelFactory
class ChargepriceFeedbackFragment : Fragment() {
private lateinit var binding: FragmentChargepriceFeedbackBinding
private val vm: ChargepriceFeedbackViewModel by viewModels(factoryProducer = {
viewModelFactory {
ChargepriceFeedbackViewModel(
requireActivity().application,
getString(R.string.chargeprice_key),
getString(R.string.chargeprice_api_url)
)
}
})
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val fragmentArgs: ChargepriceFeedbackFragmentArgs by navArgs()
vm.feedbackType.value = fragmentArgs.feedbackType
vm.charger.value = fragmentArgs.charger
vm.vehicle.value = fragmentArgs.vehicle
vm.chargePrices.value = fragmentArgs.chargePrices?.toList()
vm.batteryRange.value = fragmentArgs.batteryRange?.toList()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_chargeprice_feedback, container, false
)
binding.lifecycleOwner = this
binding.vm = vm
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.toolbar.setupWithNavController(
findNavController(),
(requireActivity() as MapsActivity).appBarConfiguration
)
binding.tariffSpinner.setAdapter(
ArrayAdapter<String>(
requireContext(),
R.layout.item_simple_multiline,
R.id.text,
mutableListOf()
)
)
}
}

View File

@@ -30,6 +30,7 @@ import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.databinding.FragmentChargepriceBinding
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.viewmodel.ChargepriceFeedbackType
import net.vonforst.evmap.viewmodel.ChargepriceViewModel
import net.vonforst.evmap.viewmodel.Status
import net.vonforst.evmap.viewmodel.savedStateViewModelFactory
@@ -182,6 +183,9 @@ class ChargepriceFragment : Fragment() {
binding.btnSettings.setOnClickListener {
findNavController().navigate(R.id.action_chargeprice_to_chargepriceSettingsFragment)
}
binding.btnFeedbackMissingPrice.setOnClickListener {
feedbackMissingPrice()
}
binding.batteryRange.setLabelFormatter { value: Float ->
val fmt = NumberFormat.getNumberInstance()
@@ -202,6 +206,14 @@ class ChargepriceFragment : Fragment() {
(activity as? MapsActivity)?.openUrl(getString(R.string.chargeprice_faq_link))
true
}
R.id.menu_feedback_missing_price -> {
feedbackMissingPrice()
true
}
R.id.menu_feedback_wrong_price -> {
feedbackWrongPrice()
true
}
else -> false
}
}
@@ -235,4 +247,30 @@ class ChargepriceFragment : Fragment() {
view.setBackgroundColor(MaterialColors.getColor(view, android.R.attr.windowBackground))
}
private fun feedbackMissingPrice() {
findNavController().navigate(
R.id.action_chargeprice_to_chargepriceFeedbackFragment,
ChargepriceFeedbackFragmentArgs(
ChargepriceFeedbackType.MISSING_PRICE,
vm.charger.value,
vm.vehicle.value,
vm.chargePricesForChargepoint.value?.data?.toTypedArray(),
vm.batteryRange.value?.toFloatArray()
).toBundle()
)
}
private fun feedbackWrongPrice() {
findNavController().navigate(
R.id.action_chargeprice_to_chargepriceFeedbackFragment,
ChargepriceFeedbackFragmentArgs(
ChargepriceFeedbackType.WRONG_PRICE,
vm.charger.value,
vm.vehicle.value,
vm.chargePricesForChargepoint.value?.data?.toTypedArray(),
vm.batteryRange.value?.toFloatArray()
).toBundle()
)
}
}

View File

@@ -9,6 +9,8 @@ import android.graphics.drawable.LayerDrawable
import android.text.SpannableString
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorInt
@@ -128,9 +130,18 @@ fun <T> setRecyclerViewData(recyclerView: RecyclerView, items: List<T>?) {
}
@BindingAdapter("data")
fun <T> setRecyclerViewData(recyclerView: ViewPager2, items: List<T>?) {
if (recyclerView.adapter is ListAdapter<*, *>) {
(recyclerView.adapter as ListAdapter<T, *>).submitList(items)
fun <T> setViewPager2Data(viewPager: ViewPager2, items: List<T>?) {
if (viewPager.adapter is ListAdapter<*, *>) {
(viewPager.adapter as ListAdapter<T, *>).submitList(items)
}
}
@BindingAdapter("data")
fun <T> setAutoCompleteTextViewData(atv: AutoCompleteTextView, items: List<T>?) {
if (atv.adapter is ArrayAdapter<*>) {
val arrayAdapter = atv.adapter as ArrayAdapter<T>
arrayAdapter.clear()
items?.let { arrayAdapter.addAll(it) }
}
}

View File

@@ -0,0 +1,129 @@
package net.vonforst.evmap.viewmodel
import android.app.Application
import androidx.lifecycle.*
import kotlinx.coroutines.launch
import net.vonforst.evmap.R
import net.vonforst.evmap.api.chargeprice.*
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.currency
import java.io.IOException
enum class ChargepriceFeedbackType {
MISSING_PRICE, WRONG_PRICE, MISSING_VEHICLE
}
class ChargepriceFeedbackViewModel(
application: Application,
chargepriceApiKey: String,
chargepriceApiUrl: String
) :
AndroidViewModel(application) {
private var api = ChargepriceApi.create(chargepriceApiKey, chargepriceApiUrl)
private var prefs = PreferenceDataSource(application)
// data supplied through fragment args
val feedbackType = MutableLiveData<ChargepriceFeedbackType>()
val charger = MutableLiveData<ChargeLocation>()
val chargepoint = MutableLiveData<Chargepoint>()
val vehicle = MutableLiveData<ChargepriceCar>()
val chargePrices = MutableLiveData<List<ChargePrice>>()
val batteryRange = MutableLiveData<List<Float>>()
// data input by user
val tariff = MutableLiveData<String>()
val price = MutableLiveData<String>()
val notes = MutableLiveData<String>()
val email = MutableLiveData<String>()
val loading = MutableLiveData<Boolean>().apply { value = false }
val chargePricesStrings = chargePrices.map {
it.map {
val name = if (!it.tariffName.lowercase().startsWith(it.provider.lowercase())) {
"${it.provider} ${it.tariffName}"
} else it.tariffName
val price = application.getString(
R.string.charge_price_format,
it.chargepointPrices[0].price,
currency(it.currency)
)
"$name: $price"
}.toList()
}
private val feedback = MediatorLiveData<ChargepriceUserFeedback>().apply {
listOf(
feedbackType,
charger,
chargepoint,
vehicle,
chargePrices,
tariff,
price,
notes,
email
).forEach {
addSource(it) {
try {
value = when (feedbackType.value!!) {
ChargepriceFeedbackType.MISSING_PRICE -> {
ChargepriceMissingPriceFeedback(
tariff.value ?: "",
charger.value?.network?.take(200) ?: "",
price.value ?: "",
charger.value?.let { ChargepriceApi.getPoiUrl(it) } ?: "",
notes.value ?: "",
email.value ?: "",
getChargepriceContext(),
ChargepriceApi.getChargepriceLanguage()
)
}
ChargepriceFeedbackType.WRONG_PRICE -> {
ChargepriceWrongPriceFeedback(
"", // TODO: dropdown value
charger.value?.network?.take(200) ?: "",
"", // TODO: dropdown value
price.value ?: "",
charger.value?.let { ChargepriceApi.getPoiUrl(it) } ?: "",
notes.value ?: "",
email.value ?: "",
getChargepriceContext(),
ChargepriceApi.getChargepriceLanguage()
)
}
ChargepriceFeedbackType.MISSING_VEHICLE -> {
TODO()
}
}
} catch (e: IllegalArgumentException) {
value = null
}
}
}
}
val formValid = feedback.map { it != null }
fun sendFeedback() {
val feedback = feedback.value ?: return
viewModelScope.launch {
loading.value = true
try {
api.userFeedback(feedback)
} catch (e: IOException) {
}
loading.value = false
}
}
private fun getChargepriceContext(): String {
val result = StringBuilder()
vehicle.value?.let { result.append("Vehicle: ${it.brand} ${it.name}\n") }
batteryRange.value?.let { result.append("Battery SOC: ${it[0]} to ${it[1]}\n") }
return result.toString()
}
}

View File

@@ -168,7 +168,8 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/battery_range"
tools:itemCount="3"
tools:listitem="@layout/item_chargeprice" />
tools:listitem="@layout/item_chargeprice"
tools:visibility="invisible" />
<TextView
android:id="@+id/textView8"
@@ -179,10 +180,11 @@
android:gravity="center_horizontal"
android:text="@string/chargeprice_no_tariffs_found"
app:goneUnless="@{vm.chargePricesForChargepoint.status == Status.SUCCESS &amp;&amp; vm.chargePricesForChargepoint.data.size() == 0}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/btnFeedbackMissingPrice"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
app:layout_constraintTop_toTopOf="@+id/charge_prices_list"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/textView9"
@@ -237,6 +239,20 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView3" />
<Button
android:id="@+id/btnFeedbackMissingPrice"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="@string/chargeprice_feedback_missing_price"
app:goneUnless="@{vm.chargePricesForChargepoint.status == Status.SUCCESS &amp;&amp; vm.chargePricesForChargepoint.data.size() == 0}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView8" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View File

@@ -0,0 +1,209 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="net.vonforst.evmap.viewmodel.ChargepriceFeedbackViewModel" />
<import type="net.vonforst.evmap.viewmodel.ChargepriceFeedbackType" />
<variable
name="vm"
type="ChargepriceFeedbackViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:goneUnless="@{vm.loading}" />
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/toolbar_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.appbar.AppBarLayout>
<ScrollView
android:id="@+id/scrollView2"
android:layout_width="0dp"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar_container"
app:goneUnless="@{!vm.loading}">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/mainLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/txtCPO"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="@{@string/chargeprice_feedback_cpo(vm.charger.network)}"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
app:goneUnless="@{vm.charger.network != null &amp;&amp; vm.feedbackType == ChargepriceFeedbackType.MISSING_PRICE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/chargeprice_feedback_cpo" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_tariff"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
app:counterEnabled="true"
app:counterMaxLength="100"
app:goneUnless="@{vm.feedbackType == ChargepriceFeedbackType.MISSING_PRICE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/txtCPO">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/chargeprice_feedback_tariff"
android:maxLength="100"
android:text="@={vm.tariff}" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_tariff_spinner"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
app:goneUnless="@{vm.feedbackType == ChargepriceFeedbackType.WRONG_PRICE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/input_tariff">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/tariff_spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/chargeprice_feedback_tariff"
android:inputType="none"
app:data="@{vm.chargePricesStrings}" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_price"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
app:counterEnabled="true"
app:counterMaxLength="100"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/input_tariff_spinner">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/chargeprice_feedback_price"
android:maxLength="100"
android:text="@={vm.price}" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_comment"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
app:counterEnabled="true"
app:counterMaxLength="1000"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/input_price">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="160dp"
android:gravity="top"
android:hint="@string/chargeprice_feedback_comment"
android:inputType="textMultiLine"
android:maxLength="1000"
android:text="@={vm.notes}" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/input_email"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
app:counterEnabled="true"
app:counterMaxLength="100"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/input_comment">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/chargeprice_feedback_email"
android:inputType="textEmailAddress"
android:maxLength="100"
android:text="@={vm.email}" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btnSend"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:onClick="@{(view) -> vm.sendFeedback()}"
android:enabled="@{vm.formValid}"
android:text="@string/chargeprice_feedback_send"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/input_email" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="13dp"
android:paddingTop="13dp"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="3"
android:textAppearance="?attr/textAppearanceBodyLarge"
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam" />
</FrameLayout>

View File

@@ -6,6 +6,16 @@
android:id="@+id/menu_help"
android:icon="@drawable/ic_help"
android:title="@string/help"
app:showAsAction="always" />
app:showAsAction="ifRoom" />
<item
android:id="@+id/menu_feedback_missing_price"
android:title="@string/chargeprice_feedback_missing_price"
app:showAsAction="never" />
<item
android:id="@+id/menu_feedback_wrong_price"
android:title="@string/chargeprice_feedback_wrong_price"
app:showAsAction="never" />
</menu>

View File

@@ -124,6 +124,13 @@
app:enterAnim="@animator/nav_default_enter_anim"
app:popEnterAnim="@animator/nav_default_enter_anim"
app:popExitAnim="@animator/nav_default_exit_anim" />
<action
android:id="@+id/action_chargeprice_to_chargepriceFeedbackFragment"
app:destination="@id/chargeprice_feedback"
app:exitAnim="@animator/nav_default_exit_anim"
app:enterAnim="@animator/nav_default_enter_anim"
app:popEnterAnim="@animator/nav_default_enter_anim"
app:popExitAnim="@animator/nav_default_exit_anim" />
<action
android:id="@+id/action_chargeprice_to_donateFragment"
app:destination="@id/donate" />
@@ -131,6 +138,31 @@
android:name="charger"
app:argType="net.vonforst.evmap.model.ChargeLocation" />
</fragment>
<fragment
android:id="@+id/chargeprice_feedback"
android:name="net.vonforst.evmap.fragment.ChargepriceFeedbackFragment"
android:label="@string/chargeprice_feedback"
tools:layout="@layout/fragment_chargeprice_feedback">
<argument
android:name="feedbackType"
app:argType="net.vonforst.evmap.viewmodel.ChargepriceFeedbackType" />
<argument
android:name="charger"
app:argType="net.vonforst.evmap.model.ChargeLocation"
app:nullable="true" />
<argument
android:name="vehicle"
app:argType="net.vonforst.evmap.api.chargeprice.ChargepriceCar"
app:nullable="true" />
<argument
android:name="chargePrices"
app:argType="net.vonforst.evmap.api.chargeprice.ChargePrice[]"
app:nullable="true" />
<argument
android:name="batteryRange"
app:argType="float[]"
app:nullable="true" />
</fragment>
<fragment
android:id="@+id/donate"
android:name="net.vonforst.evmap.fragment.DonateFragment"

View File

@@ -270,4 +270,13 @@
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
<string name="about_contributors">Mitwirkende</string>
<string name="about_contributors_text">Dank an alle Mitwirkenden für ihre Beiträge von Code und Übersetzungen für EVMap:</string>
<string name="chargeprice_feedback">Feedback</string>
<string name="chargeprice_feedback_missing_price">Fehlenden Preis melden</string>
<string name="chargeprice_feedback_wrong_price">Falschen Preis melden</string>
<string name="chargeprice_feedback_email">E-Mail-Adresse für Rückfragen</string>
<string name="chargeprice_feedback_comment">Weitere Infos</string>
<string name="chargeprice_feedback_price">Preis (pro kWh, Minute, etc.)</string>
<string name="chargeprice_feedback_cpo">Betreiber der Station (CPO): %s</string>
<string name="chargeprice_feedback_tariff">Anbieter bzw. Tarif</string>
<string name="chargeprice_feedback_send">Absenden</string>
</resources>

View File

@@ -269,4 +269,13 @@
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
<string name="about_contributors">Contributors</string>
<string name="about_contributors_text">Thanks to all contributors for their coding and translation contributions to EVMap:</string>
<string name="chargeprice_feedback">Feedback</string>
<string name="chargeprice_feedback_missing_price">Report missing price</string>
<string name="chargeprice_feedback_wrong_price">Report incorrect price</string>
<string name="chargeprice_feedback_email">Email address for further questions</string>
<string name="chargeprice_feedback_comment">Other details</string>
<string name="chargeprice_feedback_price">Price (per kWh, minute, etc.)</string>
<string name="chargeprice_feedback_cpo">Station operator (CPO): %s</string>
<string name="chargeprice_feedback_tariff">Provider / plan</string>
<string name="chargeprice_feedback_send">Submit</string>
</resources>