From 19b87b7a3d317b8a13b25fc103ce9c097ccff4da Mon Sep 17 00:00:00 2001 From: Johan von Forstner Date: Wed, 13 Jul 2022 15:18:59 +0200 Subject: [PATCH] implement chargeprice feedback dialog #195 --- app/build.gradle | 4 +- app/src/debug/res/values/donottranslate.xml | 5 + .../vonforst/evmap/auto/ChargepriceScreen.kt | 100 +++-- .../vonforst/evmap/auto/SettingsScreens.kt | 10 +- .../evmap/api/chargeprice/ChargepriceApi.kt | 59 ++- .../evmap/api/chargeprice/ChargepriceModel.kt | 422 +++++++++--------- .../fragment/ChargepriceFeedbackFragment.kt | 74 +++ .../evmap/fragment/ChargepriceFragment.kt | 46 +- .../vonforst/evmap/fragment/MapFragment.kt | 8 +- .../preference/ChargepriceSettingsFragment.kt | 3 +- .../preference/DataSettingsFragment.kt | 3 +- .../net/vonforst/evmap/ui/BindingAdapters.kt | 17 +- .../viewmodel/ChargepriceFeedbackViewModel.kt | 129 ++++++ .../evmap/viewmodel/ChargepriceViewModel.kt | 89 ++-- .../evmap/viewmodel/SettingsViewModel.kt | 8 +- .../main/res/layout/fragment_chargeprice.xml | 22 +- .../layout/fragment_chargeprice_feedback.xml | 208 +++++++++ app/src/main/res/layout/item_chargeprice.xml | 2 +- .../main/res/layout/item_simple_multiline.xml | 19 + app/src/main/res/menu/chargeprice.xml | 12 +- app/src/main/res/navigation/nav_graph.xml | 33 +- app/src/main/res/values-de/strings.xml | 9 + app/src/main/res/values/donottranslate.xml | 1 + app/src/main/res/values/strings.xml | 9 + .../api/chargeprice/ChargepriceApiTest.kt | 10 +- build.gradle | 1 - 26 files changed, 952 insertions(+), 351 deletions(-) create mode 100644 app/src/debug/res/values/donottranslate.xml create mode 100644 app/src/main/java/net/vonforst/evmap/fragment/ChargepriceFeedbackFragment.kt create mode 100644 app/src/main/java/net/vonforst/evmap/viewmodel/ChargepriceFeedbackViewModel.kt create mode 100644 app/src/main/res/layout/fragment_chargeprice_feedback.xml create mode 100644 app/src/main/res/layout/item_simple_multiline.xml diff --git a/app/build.gradle b/app/build.gradle index 5c49f4443..970682ddd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -165,8 +165,8 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.9.0' implementation 'com.squareup.moshi:moshi-kotlin:1.13.0' implementation 'com.squareup.moshi:moshi-adapters:1.13.0' - implementation 'moe.banana:moshi-jsonapi:3.5.0' - implementation 'moe.banana:moshi-jsonapi-retrofit-converter:3.5.0' + implementation 'com.markomilos.jsonapi:jsonapi-adapters:1.0.1' + implementation 'com.markomilos.jsonapi:jsonapi-retrofit:1.0.1' implementation 'io.coil-kt:coil:1.1.0' implementation 'com.github.johan12345:StfalconImageViewer:5082ebd392' implementation "com.mikepenz:aboutlibraries-core:$about_libs_version" diff --git a/app/src/debug/res/values/donottranslate.xml b/app/src/debug/res/values/donottranslate.xml new file mode 100644 index 000000000..944b19fe1 --- /dev/null +++ b/app/src/debug/res/values/donottranslate.xml @@ -0,0 +1,5 @@ + + + https://staging-api.chargeprice.app/v1/ + 20c0d68918c9dc96c564784b711a6570 + \ No newline at end of file diff --git a/app/src/google/java/net/vonforst/evmap/auto/ChargepriceScreen.kt b/app/src/google/java/net/vonforst/evmap/auto/ChargepriceScreen.kt index 223b84760..577d167ce 100644 --- a/app/src/google/java/net/vonforst/evmap/auto/ChargepriceScreen.kt +++ b/app/src/google/java/net/vonforst/evmap/auto/ChargepriceScreen.kt @@ -15,13 +15,13 @@ import androidx.car.app.model.* import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.IconCompat import androidx.lifecycle.lifecycleScope +import jsonapi.Meta +import jsonapi.Relationship +import jsonapi.Relationships +import jsonapi.ResourceIdentifier import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import moe.banana.jsonapi2.HasMany -import moe.banana.jsonapi2.HasOne -import moe.banana.jsonapi2.JsonBuffer -import moe.banana.jsonapi2.ResourceIdentifier import net.vonforst.evmap.R import net.vonforst.evmap.api.chargeprice.* import net.vonforst.evmap.model.ChargeLocation @@ -34,7 +34,10 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c private val prefs = PreferenceDataSource(ctx) private val db = AppDatabase.getInstance(carContext) private val api by lazy { - ChargepriceApi.create(carContext.getString(R.string.chargeprice_key)) + ChargepriceApi.create( + carContext.getString(R.string.chargeprice_key), + carContext.getString(R.string.chargeprice_api_url) + ) } private var prices: List? = null private var meta: ChargepriceChargepointMeta? = null @@ -94,7 +97,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c ) .build().intent intent.data = - Uri.parse("https://www.chargeprice.app/?poi_id=${charger.id}&poi_source=${getDataAdapter()}") + Uri.parse(ChargepriceApi.getPoiUrl(charger)) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) try { carContext.startActivity(intent) @@ -169,39 +172,44 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c } private fun loadPrices(model: Model?) { - val dataAdapter = getDataAdapter() ?: return + val dataAdapter = ChargepriceApi.getDataAdapter(charger) ?: return val manufacturer = model?.manufacturer?.value val modelName = getVehicleModel(model?.manufacturer?.value, model?.name?.value) lifecycleScope.launch { try { val car = determineVehicle(manufacturer, modelName) val cpStation = ChargepriceStation.fromEvmap(charger, car.compatibleEvmapConnectors) - val result = api.getChargePrices(ChargepriceRequest().apply { - this.dataAdapter = dataAdapter - station = cpStation - vehicle = HasOne(car) - tariffs = if (!prefs.chargepriceMyTariffsAll) { - val myTariffs = prefs.chargepriceMyTariffs ?: emptySet() - HasMany(*myTariffs.map { - ResourceIdentifier( - "tariff", - it - ) - }.toTypedArray()).apply { - meta = JsonBuffer.create( - ChargepriceApi.moshi.adapter(ChargepriceRequestTariffMeta::class.java), - ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS) + val result = api.getChargePrices( + ChargepriceRequest( + dataAdapter = dataAdapter, + station = cpStation, + vehicle = car, + options = ChargepriceOptions( + batteryRange = batteryRange.map { it.toDouble() }, + providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs, + maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null, + currency = prefs.chargepriceCurrency, + allowUnbalancedLoad = prefs.chargepriceAllowUnbalancedLoad + ), + relationships = if (!prefs.chargepriceMyTariffsAll) { + val myTariffs = prefs.chargepriceMyTariffs ?: emptySet() + Relationships( + "tariffs" to Relationship.ToMany( + myTariffs.map { + ResourceIdentifier( + "tariff", + id = it + ) + }, + meta = Meta.from( + ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS), + ChargepriceApi.moshi + ) + ) ) - } - } else null - options = ChargepriceOptions( - batteryRange = batteryRange.map { it.toDouble() }, - providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs, - maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null, - currency = prefs.chargepriceCurrency, - allowUnbalancedLoad = prefs.chargepriceAllowUnbalancedLoad - ) - }, ChargepriceApi.getChargepriceLanguage()) + } else null + ), ChargepriceApi.getChargepriceLanguage() + ) val myTariffs = prefs.chargepriceMyTariffs @@ -215,14 +223,16 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c invalidate() return@launch } - meta = - (result.meta.get(ChargepriceApi.moshi.adapter(ChargepriceMeta::class.java)) as ChargepriceMeta).chargePoints.filterIndexed { i, cp -> - charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors - }.maxByOrNull { - it.power - } - prices = result.map { cp -> + val metaMapped = + result.meta!!.map(ChargepriceMeta::class.java, ChargepriceApi.moshi)!! + meta = metaMapped.chargePoints.filterIndexed { i, cp -> + charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors + }.maxByOrNull { + it.power + } + + prices = result.data!!.map { cp -> val filteredPrices = cp.chargepointPrices.filter { it.plug == chargepoint.plug && it.power == chargepoint.power @@ -230,15 +240,15 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c if (filteredPrices.isEmpty()) { null } else { - cp.clone().apply { + cp.copy( chargepointPrices = filteredPrices - } + ) } }.filterNotNull() .sortedBy { it.chargepointPrices.first().price } .sortedByDescending { prefs.chargepriceMyTariffsAll || - myTariffs != null && it.tariff?.get()?.id in myTariffs + myTariffs != null && it.tariff?.id in myTariffs } invalidate() } catch (e: IOException) { @@ -316,10 +326,4 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c } return vehicles[0] } - - private fun getDataAdapter(): String? = when (charger.dataSource) { - "goingelectric" -> ChargepriceApi.DATA_SOURCE_GOINGELECTRIC - "openchargemap" -> ChargepriceApi.DATA_SOURCE_OPENCHARGEMAP - else -> null - } } \ No newline at end of file diff --git a/app/src/google/java/net/vonforst/evmap/auto/SettingsScreens.kt b/app/src/google/java/net/vonforst/evmap/auto/SettingsScreens.kt index dfc631d31..6155c80cd 100644 --- a/app/src/google/java/net/vonforst/evmap/auto/SettingsScreens.kt +++ b/app/src/google/java/net/vonforst/evmap/auto/SettingsScreens.kt @@ -283,7 +283,10 @@ class ChargepriceSettingsScreen(ctx: CarContext) : Screen(ctx) { class SelectVehiclesScreen(ctx: CarContext) : MultiSelectSearchScreen(ctx) { private val prefs = PreferenceDataSource(carContext) - private var api = ChargepriceApi.create(carContext.getString(R.string.chargeprice_key)) + private var api = ChargepriceApi.create( + carContext.getString(R.string.chargeprice_key), + carContext.getString(R.string.chargeprice_api_url) + ) override val isMultiSelect = true override val shouldShowSelectAll = false @@ -308,7 +311,10 @@ class SelectVehiclesScreen(ctx: CarContext) : MultiSelectSearchScreen(ctx) { private val prefs = PreferenceDataSource(carContext) - private var api = ChargepriceApi.create(carContext.getString(R.string.chargeprice_key)) + private var api = ChargepriceApi.create( + carContext.getString(R.string.chargeprice_key), + carContext.getString(R.string.chargeprice_api_url) + ) override val isMultiSelect = true override val shouldShowSelectAll = true diff --git a/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceApi.kt b/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceApi.kt index 4745e8d6b..2ac4ffbcd 100644 --- a/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceApi.kt +++ b/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceApi.kt @@ -3,14 +3,16 @@ package net.vonforst.evmap.api.chargeprice import android.content.Context import com.facebook.stetho.okhttp3.StethoInterceptor import com.squareup.moshi.Moshi -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory -import moe.banana.jsonapi2.ArrayDocument -import moe.banana.jsonapi2.JsonApiConverterFactory -import moe.banana.jsonapi2.ResourceAdapterFactory +import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory +import jsonapi.Document +import jsonapi.JsonApiFactory +import jsonapi.retrofit.DocumentConverterFactory import net.vonforst.evmap.BuildConfig +import net.vonforst.evmap.model.ChargeLocation import okhttp3.Cache import okhttp3.OkHttpClient import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header @@ -20,34 +22,45 @@ import java.util.* interface ChargepriceApi { @POST("charge_prices") suspend fun getChargePrices( - @Body request: ChargepriceRequest, + @Body @jsonapi.retrofit.Document request: ChargepriceRequest, @Header("Accept-Language") language: String - ): ArrayDocument + ): Document> @GET("vehicles") - suspend fun getVehicles(): ArrayDocument + @jsonapi.retrofit.Document + suspend fun getVehicles(): List @GET("tariffs") - suspend fun getTariffs(): ArrayDocument + @jsonapi.retrofit.Document + suspend fun getTariffs(): List + + @POST("user_feedback") + suspend fun userFeedback(@Body @jsonapi.retrofit.Document feedback: ChargepriceUserFeedback) companion object { private val cacheSize = 1L * 1024 * 1024 // 1MB val supportedLanguages = setOf("de", "en", "fr", "nl") - val DATA_SOURCE_GOINGELECTRIC = "going_electric" - val DATA_SOURCE_OPENCHARGEMAP = "open_charge_map" + private val DATA_SOURCE_GOINGELECTRIC = "going_electric" + private val DATA_SOURCE_OPENCHARGEMAP = "open_charge_map" - private val jsonApiAdapterFactory = ResourceAdapterFactory.builder() - .add(ChargepriceRequest::class.java) - .add(ChargepriceTariff::class.java) - .add(ChargepriceBrand::class.java) - .add(ChargePrice::class.java) - .add(ChargepriceCar::class.java) + private val jsonApiAdapterFactory = JsonApiFactory.Builder() + .addType(ChargepriceRequest::class.java) + .addType(ChargepriceTariff::class.java) + .addType(ChargepriceBrand::class.java) + .addType(ChargePrice::class.java) + .addType(ChargepriceCar::class.java) .build() val moshi = Moshi.Builder() .add(jsonApiAdapterFactory) - .add(KotlinJsonAdapterFactory()) + .add( + PolymorphicJsonAdapterFactory.of(ChargepriceUserFeedback::class.java, "type") + .withSubtype(ChargepriceMissingPriceFeedback::class.java, "missing_price") + .withSubtype(ChargepriceWrongPriceFeedback::class.java, "wrong_price") + .withSubtype(ChargepriceMissingVehicleFeedback::class.java, "missing_vehicle") + ) .build() + fun create( apikey: String, baseurl: String = "https://api.chargeprice.app/v1/", @@ -73,7 +86,8 @@ interface ChargepriceApi { val retrofit = Retrofit.Builder() .baseUrl(baseurl) - .addConverterFactory(JsonApiConverterFactory.create(moshi)) + .addConverterFactory(DocumentConverterFactory.create()) + .addConverterFactory(MoshiConverterFactory.create(moshi)) .client(client) .build() return retrofit.create(ChargepriceApi::class.java) @@ -89,6 +103,15 @@ interface ChargepriceApi { } } + fun getPoiUrl(charger: ChargeLocation) = + "https://www.chargeprice.app/?poi_id=${charger.id}&poi_source=${getDataAdapter(charger)}" + + fun getDataAdapter(charger: ChargeLocation) = when (charger.dataSource) { + "goingelectric" -> DATA_SOURCE_GOINGELECTRIC + "openchargemap" -> DATA_SOURCE_OPENCHARGEMAP + else -> throw IllegalArgumentException() + } + @JvmStatic fun isCountrySupported(country: String, dataSource: String): Boolean = when (dataSource) { // list of countries updated 2021/08/24 diff --git a/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceModel.kt b/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceModel.kt index e237d17c7..e98afc236 100644 --- a/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceModel.kt +++ b/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceModel.kt @@ -1,12 +1,12 @@ package net.vonforst.evmap.api.chargeprice import android.content.Context +import android.os.Parcelable +import android.util.Patterns import com.squareup.moshi.Json -import moe.banana.jsonapi2.HasMany -import moe.banana.jsonapi2.HasOne - -import moe.banana.jsonapi2.JsonApi -import moe.banana.jsonapi2.Resource +import com.squareup.moshi.JsonClass +import jsonapi.* +import kotlinx.parcelize.Parcelize import net.vonforst.evmap.R import net.vonforst.evmap.adapter.Equatable import net.vonforst.evmap.api.equivalentPlugTypes @@ -17,16 +17,21 @@ import kotlin.math.ceil import kotlin.math.floor -@JsonApi(type = "charge_price_request") -class ChargepriceRequest : Resource() { - @field:Json(name = "data_adapter") - lateinit var dataAdapter: String - lateinit var station: ChargepriceStation - lateinit var options: ChargepriceOptions - var tariffs: HasMany? = null - var vehicle: HasOne? = null -} +@Resource("charge_price_request") +@JsonClass(generateAdapter = true) +data class ChargepriceRequest( + @Json(name = "data_adapter") + val dataAdapter: String, + val station: ChargepriceStation, + val options: ChargepriceOptions, + @ToMany("tariffs") + val tariffs: List? = null, + @ToOne("vehicle") + val vehicle: ChargepriceCar? = null, + @RelationshipsObject var relationships: Relationships? = null +) +@JsonClass(generateAdapter = true) data class ChargepriceStation( val longitude: Double, val latitude: Double, @@ -56,11 +61,13 @@ data class ChargepriceStation( } } +@JsonClass(generateAdapter = true) data class ChargepriceChargepoint( val power: Double, val plug: String ) +@JsonClass(generateAdapter = true) data class ChargepriceOptions( @Json(name = "max_monthly_fees") val maxMonthlyFees: Double? = null, val energy: Double? = null, @@ -73,142 +80,109 @@ data class ChargepriceOptions( @Json(name = "provider_customer_tariffs") val providerCustomerTariffs: Boolean? = null ) -@JsonApi(type = "tariff") -class ChargepriceTariff() : Resource() { - lateinit var provider: String - lateinit var name: String - @field:Json(name = "direct_payment") - var directPayment: Boolean = false - @field:Json(name = "provider_customer_tariff") - var providerCustomerTariff: Boolean = false - @field:Json(name = "supported_cuntries") - lateinit var supportedCountries: Set - @field:Json(name = "charge_card_id") - lateinit var chargeCardId: String // GE charge card ID - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - if (!super.equals(other)) return false - - other as ChargepriceTariff - - if (provider != other.provider) return false - if (name != other.name) return false - if (directPayment != other.directPayment) return false - if (providerCustomerTariff != other.providerCustomerTariff) return false - if (supportedCountries != other.supportedCountries) return false - if (chargeCardId != other.chargeCardId) return false - - return true - } - - override fun hashCode(): Int { - var result = super.hashCode() - result = 31 * result + provider.hashCode() - result = 31 * result + name.hashCode() - result = 31 * result + directPayment.hashCode() - result = 31 * result + providerCustomerTariff.hashCode() - result = 31 * result + supportedCountries.hashCode() - result = 31 * result + chargeCardId.hashCode() - return result - } +@Resource("tariff") +@Parcelize +@JsonClass(generateAdapter = true) +data class ChargepriceTariff( + @Id val id_: String?, + val provider: String, + val name: String, + @Json(name = "direct_payment") + val directPayment: Boolean = false, + @Json(name = "provider_customer_tariff") + val providerCustomerTariff: Boolean = false, + @Json(name = "supported_countries") + val supportedCountries: Set, + @Json(name = "charge_card_id") + val chargeCardId: String?, // GE charge card ID +) : Parcelable { + val id: String + get() = id_!! } -@JsonApi(type = "car") -class ChargepriceCar : Resource(), Equatable { - lateinit var name: String - lateinit var brand: String - - @field:Json(name = "dc_charge_ports") - lateinit var dcChargePorts: List - lateinit var manufacturer: HasOne +@JsonClass(generateAdapter = true) +@Resource("car") +@Parcelize +data class ChargepriceCar( + @Id val id_: String?, + val name: String, + val brand: String, - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - if (!super.equals(other)) return false + @Json(name = "dc_charge_ports") + val dcChargePorts: List, + @ToOne("manufacturer") + val manufacturer: ChargepriceBrand? +) : Equatable, Parcelable { - other as ChargepriceCar - - if (name != other.name) return false - if (brand != other.brand) return false - if (dcChargePorts != other.dcChargePorts) return false - if (manufacturer != other.manufacturer) return false - - return true + companion object { + private val acConnectors = listOf( + Chargepoint.CEE_BLAU, + Chargepoint.CEE_ROT, + Chargepoint.SCHUKO, + Chargepoint.TYPE_1, + Chargepoint.TYPE_2_UNKNOWN, + Chargepoint.TYPE_2_SOCKET, + Chargepoint.TYPE_2_PLUG + ) + private val plugMapping = mapOf( + "ccs" to Chargepoint.CCS_UNKNOWN, + "tesla_suc" to Chargepoint.SUPERCHARGER, + "tesla_ccs" to Chargepoint.CCS_UNKNOWN, + "chademo" to Chargepoint.CHADEMO + ) } - override fun hashCode(): Int { - var result = super.hashCode() - result = 31 * result + name.hashCode() - result = 31 * result + brand.hashCode() - result = 31 * result + dcChargePorts.hashCode() - result = 31 * result + manufacturer.hashCode() - return result - } + val id: String + get() = id_!! - private val acConnectors = listOf( - Chargepoint.CEE_BLAU, - Chargepoint.CEE_ROT, - Chargepoint.SCHUKO, - Chargepoint.TYPE_1, - Chargepoint.TYPE_2_UNKNOWN, - Chargepoint.TYPE_2_SOCKET, - Chargepoint.TYPE_2_PLUG - ) - private val plugMapping = mapOf( - "ccs" to Chargepoint.CCS_UNKNOWN, - "tesla_suc" to Chargepoint.SUPERCHARGER, - "tesla_ccs" to Chargepoint.CCS_UNKNOWN, - "chademo" to Chargepoint.CHADEMO - ) val compatibleEvmapConnectors: List get() = dcChargePorts.map { plugMapping[it] }.filterNotNull().plus(acConnectors) } -@JsonApi(type = "brand") -class ChargepriceBrand : Resource() - -@JsonApi(type = "charge_price") -class ChargePrice : Resource(), Equatable, Cloneable { - lateinit var provider: String - - @field:Json(name = "tariff_name") - lateinit var tariffName: String - lateinit var url: String - - @field:Json(name = "monthly_min_sales") - var monthlyMinSales: Double = 0.0 - - @field:Json(name = "total_monthly_fee") - var totalMonthlyFee: Double = 0.0 - - @field:Json(name = "flat_rate") - var flatRate: Boolean = false - - @field:Json(name = "direct_payment") - var directPayment: Boolean = false - - @field:Json(name = "provider_customer_tariff") - var providerCustomerTariff: Boolean = false - lateinit var currency: String - - @field:Json(name = "start_time") - var startTime: Int = 0 - lateinit var tags: List - - @field:Json(name = "charge_point_prices") - lateinit var chargepointPrices: List - - @field:Json(name = "branding") - var branding: ChargepriceBranding? = null - - var tariff: HasOne? = null - - +@JsonClass(generateAdapter = true) +@Resource("brand") +@Parcelize +data class ChargepriceBrand( + @Id val id: String? +) : Parcelable + +@JsonClass(generateAdapter = true) +@Resource("charge_price") +@Parcelize +data class ChargePrice( + val provider: String, + @Json(name = "tariff_name") + val tariffName: String, + val url: String, + @Json(name = "monthly_min_sales") + val monthlyMinSales: Double = 0.0, + @Json(name = "total_monthly_fee") + val totalMonthlyFee: Double = 0.0, + @Json(name = "flat_rate") + val flatRate: Boolean = false, + + @Json(name = "direct_payment") + val directPayment: Boolean = false, + + @Json(name = "provider_customer_tariff") + val providerCustomerTariff: Boolean = false, + val currency: String, + + @Json(name = "start_time") + val startTime: Int = 0, + val tags: List, + + @Json(name = "charge_point_prices") + val chargepointPrices: List, + + @Json(name = "branding") + val branding: ChargepriceBranding? = null, + + @ToOne("tariff") + val tariff: ChargepriceTariff? +) : Equatable, Cloneable, Parcelable { fun formatMonthlyFees(ctx: Context): String { return listOfNotNull( if (totalMonthlyFee > 0) { @@ -219,69 +193,10 @@ class ChargePrice : Resource(), Equatable, Cloneable { } else null ).joinToString(", ") } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - if (!super.equals(other)) return false - - other as ChargePrice - - if (provider != other.provider) return false - if (tariffName != other.tariffName) return false - if (url != other.url) return false - if (monthlyMinSales != other.monthlyMinSales) return false - if (totalMonthlyFee != other.totalMonthlyFee) return false - if (flatRate != other.flatRate) return false - if (directPayment != other.directPayment) return false - if (providerCustomerTariff != other.providerCustomerTariff) return false - if (currency != other.currency) return false - if (startTime != other.startTime) return false - if (tags != other.tags) return false - if (chargepointPrices != other.chargepointPrices) return false - if (branding != other.branding) return false - - return true - } - - override fun hashCode(): Int { - var result = super.hashCode() - result = 31 * result + provider.hashCode() - result = 31 * result + tariffName.hashCode() - result = 31 * result + url.hashCode() - result = 31 * result + monthlyMinSales.hashCode() - result = 31 * result + totalMonthlyFee.hashCode() - result = 31 * result + flatRate.hashCode() - result = 31 * result + directPayment.hashCode() - result = 31 * result + providerCustomerTariff.hashCode() - result = 31 * result + currency.hashCode() - result = 31 * result + startTime - result = 31 * result + tags.hashCode() - result = 31 * result + chargepointPrices.hashCode() - result = 31 * result + branding.hashCode() - return result - } - - public override fun clone(): ChargePrice { - return ChargePrice().apply { - chargepointPrices = this@ChargePrice.chargepointPrices - currency = this@ChargePrice.currency - directPayment = this@ChargePrice.directPayment - flatRate = this@ChargePrice.flatRate - monthlyMinSales = this@ChargePrice.monthlyMinSales - provider = this@ChargePrice.provider - providerCustomerTariff = this@ChargePrice.providerCustomerTariff - startTime = this@ChargePrice.startTime - tags = this@ChargePrice.tags - tariffName = this@ChargePrice.tariffName - totalMonthlyFee = this@ChargePrice.totalMonthlyFee - url = this@ChargePrice.url - tariff = this@ChargePrice.tariff - branding = this@ChargePrice.branding - } - } } +@JsonClass(generateAdapter = true) +@Parcelize data class ChargepointPrice( val power: Double, val plug: String, @@ -289,7 +204,7 @@ data class ChargepointPrice( @Json(name = "price_distribution") val priceDistribution: PriceDistribution, @Json(name = "blocking_fee_start") val blockingFeeStart: Int?, @Json(name = "no_price_reason") var noPriceReason: String? -) { +) : Parcelable { fun formatDistribution(ctx: Context): String { fun percent(value: Double): String { return ctx.getString(R.string.percent_format, value * 100) + "\u00a0" @@ -332,19 +247,28 @@ data class ChargepointPrice( } } +@JsonClass(generateAdapter = true) +@Parcelize data class ChargepriceBranding( @Json(name = "background_color") val backgroundColor: String, @Json(name = "text_color") val textColor: String, @Json(name = "logo_url") val logoUrl: String -) - -data class PriceDistribution(val kwh: Double?, val session: Double?, val minute: Double?) { - val isOnlyKwh = - kwh != null && kwh > 0 && (session == null || session == 0.0) && (minute == null || minute == 0.0) +) : Parcelable + +@JsonClass(generateAdapter = true) +@Parcelize +data class PriceDistribution(val kwh: Double?, val session: Double?, val minute: Double?) : + Parcelable { + val isOnlyKwh + get() = kwh != null && kwh > 0 && (session == null || session == 0.0) && (minute == null || minute == 0.0) } -data class ChargepriceTag(val kind: String, val text: String, val url: String?) : Equatable +@JsonClass(generateAdapter = true) +@Parcelize +data class ChargepriceTag(val kind: String, val text: String, val url: String?) : Equatable, + Parcelable +@JsonClass(generateAdapter = true) data class ChargepriceMeta( @Json(name = "charge_points") val chargePoints: List ) @@ -358,13 +282,97 @@ enum class ChargepriceInclude { EXCLUSIVE } +@JsonClass(generateAdapter = true) +@Parcelize data class ChargepriceRequestTariffMeta( val include: ChargepriceInclude -) +) : Parcelable +@JsonClass(generateAdapter = true) data class ChargepriceChargepointMeta( val power: Double, val plug: String, val energy: Double, val duration: Double -) \ No newline at end of file +) + +@Resource("user_feedback") +sealed class ChargepriceUserFeedback( + val notes: String, + val email: String, + val context: String, + val language: String +) { + init { + if (email.isBlank() || email.length > 100 || !Patterns.EMAIL_ADDRESS.matcher(email) + .matches() + ) { + throw IllegalArgumentException("invalid email") + } + if (!ChargepriceApi.supportedLanguages.contains(language)) { + throw IllegalArgumentException("invalid language") + } + if (context.length > 500) throw IllegalArgumentException("invalid context") + if (notes.length > 1000) throw IllegalArgumentException("invalid notes") + } +} + +@JsonClass(generateAdapter = true) +@Resource(type = "missing_price") +class ChargepriceMissingPriceFeedback( + val tariff: String, + val cpo: String, + val price: String, + @Json(name = "poi_link") val poiLink: String, + notes: String, + email: String, + context: String, + language: String +) : ChargepriceUserFeedback(notes, email, context, language) { + init { + if (tariff.isBlank() || tariff.length > 100) throw IllegalArgumentException("invalid tariff") + if (cpo.length > 200) throw IllegalArgumentException("invalid cpo") + if (price.isBlank() || price.length > 100) throw IllegalArgumentException("invalid price") + if (poiLink.isBlank() || poiLink.length > 200) throw IllegalArgumentException("invalid poiLink") + } +} + + +@JsonClass(generateAdapter = true) +@Resource(type = "wrong_price") +class ChargepriceWrongPriceFeedback( + val tariff: String, + val cpo: String, + @Json(name = "displayed_price") val displayedPrice: String, + @Json(name = "actual_price") val actualPrice: String, + @Json(name = "poi_link") val poiLink: String, + notes: String, + email: String, + context: String, + language: String, +) : ChargepriceUserFeedback(notes, email, context, language) { + init { + if (tariff.length > 100) throw IllegalArgumentException("invalid tariff") + if (cpo.length > 200) throw IllegalArgumentException("invalid cpo") + if (displayedPrice.length > 100) throw IllegalArgumentException("invalid displayedPrice") + if (actualPrice.length > 100) throw IllegalArgumentException("invalid actualPrice") + if (poiLink.length > 200) throw IllegalArgumentException("invalid poiLink") + } +} + + +@JsonClass(generateAdapter = true) +@Resource(type = "missing_vehicle") +class ChargepriceMissingVehicleFeedback( + val brand: String, + val model: String, + notes: String, + email: String, + context: String, + language: String, +) : ChargepriceUserFeedback(notes, email, context, language) { + init { + if (brand.length > 100) throw IllegalArgumentException("invalid brand") + if (model.length > 100) throw IllegalArgumentException("invalid model") + } +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/fragment/ChargepriceFeedbackFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/ChargepriceFeedbackFragment.kt new file mode 100644 index 000000000..f185093b3 --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/fragment/ChargepriceFeedbackFragment.kt @@ -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( + requireContext(), + R.layout.item_simple_multiline, + R.id.text, + mutableListOf() + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/fragment/ChargepriceFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/ChargepriceFragment.kt index f011ddcd0..329f5d5a4 100644 --- a/app/src/main/java/net/vonforst/evmap/fragment/ChargepriceFragment.kt +++ b/app/src/main/java/net/vonforst/evmap/fragment/ChargepriceFragment.kt @@ -24,11 +24,13 @@ import net.vonforst.evmap.R import net.vonforst.evmap.adapter.ChargepriceAdapter import net.vonforst.evmap.adapter.CheckableChargepriceCarAdapter import net.vonforst.evmap.adapter.CheckableConnectorAdapter +import net.vonforst.evmap.api.chargeprice.ChargepriceApi import net.vonforst.evmap.api.chargeprice.ChargepriceCar 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.viewModelFactory @@ -42,7 +44,8 @@ class ChargepriceFragment : Fragment() { viewModelFactory { ChargepriceViewModel( requireActivity().application, - getString(R.string.chargeprice_key) + getString(R.string.chargeprice_key), + getString(R.string.chargeprice_api_url) ) } }) @@ -103,9 +106,7 @@ class ChargepriceFragment : Fragment() { val fragmentArgs: ChargepriceFragmentArgs by navArgs() val charger = fragmentArgs.charger - val dataSource = fragmentArgs.dataSource vm.charger.value = charger - vm.dataSource.value = dataSource if (vm.chargepoint.value == null) { vm.chargepoint.value = charger.chargepointsMerged.get(0) } @@ -170,12 +171,15 @@ class ChargepriceFragment : Fragment() { } binding.imgChargepriceLogo.setOnClickListener { - (requireActivity() as MapsActivity).openUrl("https://www.chargeprice.app/?poi_id=${charger.id}&poi_source=${dataSource}") + (requireActivity() as MapsActivity).openUrl(ChargepriceApi.getPoiUrl(charger)) } binding.btnSettings.setOnClickListener { findNavController().navigate(R.id.action_chargeprice_to_chargepriceSettingsFragment) } + binding.btnFeedbackMissingPrice.setOnClickListener { + feedbackMissingPrice() + } binding.batteryRange.setLabelFormatter { value: Float -> val fmt = NumberFormat.getNumberInstance() @@ -196,6 +200,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 } } @@ -229,4 +241,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() + ) + } + } diff --git a/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt index 671c694cb..b8fc367b1 100644 --- a/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt +++ b/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt @@ -76,7 +76,6 @@ import net.vonforst.evmap.adapter.DetailsAdapter import net.vonforst.evmap.adapter.GalleryAdapter import net.vonforst.evmap.adapter.PlaceAutocompleteAdapter import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper -import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper import net.vonforst.evmap.autocomplete.ApiUnavailableException import net.vonforst.evmap.autocomplete.PlaceWithBounds import net.vonforst.evmap.databinding.FragmentMapBinding @@ -356,16 +355,11 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac } binding.detailView.btnChargeprice.setOnClickListener { val charger = vm.charger.value?.data ?: return@setOnClickListener - val dataSource = when (vm.apiType) { - GoingElectricApiWrapper::class.java -> "going_electric" - OpenChargeMapApiWrapper::class.java -> "open_charge_map" - else -> throw IllegalArgumentException("unsupported data source") - } val extras = FragmentNavigatorExtras(binding.detailView.btnChargeprice to getString(R.string.shared_element_chargeprice)) findNavController().navigate( R.id.action_map_to_chargepriceFragment, - ChargepriceFragmentArgs(charger, dataSource).toBundle(), + ChargepriceFragmentArgs(charger).toBundle(), null, extras ) } diff --git a/app/src/main/java/net/vonforst/evmap/fragment/preference/ChargepriceSettingsFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/preference/ChargepriceSettingsFragment.kt index c07b0f08c..7a179c515 100644 --- a/app/src/main/java/net/vonforst/evmap/fragment/preference/ChargepriceSettingsFragment.kt +++ b/app/src/main/java/net/vonforst/evmap/fragment/preference/ChargepriceSettingsFragment.kt @@ -16,7 +16,8 @@ class ChargepriceSettingsFragment : BaseSettingsFragment() { viewModelFactory { SettingsViewModel( requireActivity().application, - getString(R.string.chargeprice_key) + getString(R.string.chargeprice_key), + getString(R.string.chargeprice_api_url) ) } }) diff --git a/app/src/main/java/net/vonforst/evmap/fragment/preference/DataSettingsFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/preference/DataSettingsFragment.kt index c9e11ecd2..71615b52b 100644 --- a/app/src/main/java/net/vonforst/evmap/fragment/preference/DataSettingsFragment.kt +++ b/app/src/main/java/net/vonforst/evmap/fragment/preference/DataSettingsFragment.kt @@ -17,7 +17,8 @@ class DataSettingsFragment : BaseSettingsFragment() { viewModelFactory { SettingsViewModel( requireActivity().application, - getString(R.string.chargeprice_key) + getString(R.string.chargeprice_key), + getString(R.string.chargeprice_api_url) ) } }) diff --git a/app/src/main/java/net/vonforst/evmap/ui/BindingAdapters.kt b/app/src/main/java/net/vonforst/evmap/ui/BindingAdapters.kt index 0eaf4745e..c185f811e 100644 --- a/app/src/main/java/net/vonforst/evmap/ui/BindingAdapters.kt +++ b/app/src/main/java/net/vonforst/evmap/ui/BindingAdapters.kt @@ -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 setRecyclerViewData(recyclerView: RecyclerView, items: List?) { } @BindingAdapter("data") -fun setRecyclerViewData(recyclerView: ViewPager2, items: List?) { - if (recyclerView.adapter is ListAdapter<*, *>) { - (recyclerView.adapter as ListAdapter).submitList(items) +fun setViewPager2Data(viewPager: ViewPager2, items: List?) { + if (viewPager.adapter is ListAdapter<*, *>) { + (viewPager.adapter as ListAdapter).submitList(items) + } +} + +@BindingAdapter("data") +fun setAutoCompleteTextViewData(atv: AutoCompleteTextView, items: List?) { + if (atv.adapter is ArrayAdapter<*>) { + val arrayAdapter = atv.adapter as ArrayAdapter + arrayAdapter.clear() + items?.let { arrayAdapter.addAll(it) } } } diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/ChargepriceFeedbackViewModel.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/ChargepriceFeedbackViewModel.kt new file mode 100644 index 000000000..fcc3e37a3 --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/ChargepriceFeedbackViewModel.kt @@ -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() + val charger = MutableLiveData() + val chargepoint = MutableLiveData() + val vehicle = MutableLiveData() + val chargePrices = MutableLiveData>() + val batteryRange = MutableLiveData>() + + // data input by user + val tariff = MutableLiveData() + val price = MutableLiveData() + val notes = MutableLiveData() + val email = MutableLiveData() + + val loading = MutableLiveData().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().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() + } +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/ChargepriceViewModel.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/ChargepriceViewModel.kt index 593b3994f..dce8149f7 100644 --- a/app/src/main/java/net/vonforst/evmap/viewmodel/ChargepriceViewModel.kt +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/ChargepriceViewModel.kt @@ -2,12 +2,12 @@ package net.vonforst.evmap.viewmodel import android.app.Application import androidx.lifecycle.* +import jsonapi.Meta +import jsonapi.Relationship +import jsonapi.Relationships +import jsonapi.ResourceIdentifier import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import moe.banana.jsonapi2.HasMany -import moe.banana.jsonapi2.HasOne -import moe.banana.jsonapi2.JsonBuffer -import moe.banana.jsonapi2.ResourceIdentifier import net.vonforst.evmap.api.chargeprice.* import net.vonforst.evmap.api.equivalentPlugTypes import net.vonforst.evmap.model.ChargeLocation @@ -16,19 +16,19 @@ import net.vonforst.evmap.storage.PreferenceDataSource import retrofit2.HttpException import java.io.IOException -class ChargepriceViewModel(application: Application, chargepriceApiKey: String) : +class ChargepriceViewModel( + application: Application, + chargepriceApiKey: String, + chargepriceApiUrl: String +) : AndroidViewModel(application) { - private var api = ChargepriceApi.create(chargepriceApiKey) + private var api = ChargepriceApi.create(chargepriceApiKey, chargepriceApiUrl) private var prefs = PreferenceDataSource(application) val charger: MutableLiveData by lazy { MutableLiveData() } - val dataSource: MutableLiveData by lazy { - MutableLiveData() - } - val chargepoint: MutableLiveData by lazy { MutableLiveData() } @@ -99,7 +99,6 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String) value = Resource.loading(null) listOf( charger, - dataSource, batteryRange, batteryRangeSliderDragging, vehicleCompatibleConnectors, @@ -140,15 +139,15 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String) if (filteredPrices.isEmpty()) { null } else { - cp.clone().apply { + cp.copy( chargepointPrices = filteredPrices - } + ) } }.filterNotNull() .sortedBy { it.chargepointPrices.first().price } .sortedByDescending { prefs.chargepriceMyTariffsAll || - myTariffs != null && it.tariff?.get()?.id in myTariffs + myTariffs != null && it.tariff?.id in myTariffs } ) } @@ -210,10 +209,9 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String) val charger = charger.value val car = vehicle.value val compatibleConnectors = vehicleCompatibleConnectors.value - val dataSource = dataSource.value val myTariffs = myTariffs.value val myTariffsAll = myTariffsAll.value - if (charger == null || car == null || compatibleConnectors == null || dataSource == null || myTariffsAll == null || myTariffsAll == false && myTariffs == null) { + if (charger == null || car == null || compatibleConnectors == null || myTariffsAll == null || myTariffsAll == false && myTariffs == null) { chargePrices.value = Resource.error(null, null) return } @@ -223,34 +221,39 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String) loadPricesJob?.cancel() loadPricesJob = viewModelScope.launch { try { - val result = api.getChargePrices(ChargepriceRequest().apply { - dataAdapter = dataSource - station = cpStation - vehicle = HasOne(car) - tariffs = if (!myTariffsAll) { - HasMany(*myTariffs!!.map { - ResourceIdentifier( - "tariff", - it + val result = api.getChargePrices( + ChargepriceRequest( + dataAdapter = ChargepriceApi.getDataAdapter(charger), + station = cpStation, + vehicle = car, + options = ChargepriceOptions( + batteryRange = batteryRange.value!!.map { it.toDouble() }, + providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs, + maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null, + currency = prefs.chargepriceCurrency, + allowUnbalancedLoad = prefs.chargepriceAllowUnbalancedLoad + ), + relationships = if (!myTariffsAll) { + Relationships( + "tariffs" to Relationship.ToMany( + (myTariffs ?: emptySet()).map { + ResourceIdentifier( + "tariff", + id = it + ) + }, + meta = Meta.from( + ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS), + ChargepriceApi.moshi + ) + ) ) - }.toTypedArray()).apply { - meta = JsonBuffer.create( - ChargepriceApi.moshi.adapter(ChargepriceRequestTariffMeta::class.java), - ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS) - ) - } - } else null - options = ChargepriceOptions( - batteryRange = batteryRange.value!!.map { it.toDouble() }, - providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs, - maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null, - currency = prefs.chargepriceCurrency, - allowUnbalancedLoad = prefs.chargepriceAllowUnbalancedLoad - ) - }, ChargepriceApi.getChargepriceLanguage()) - val meta = - result.meta.get(ChargepriceApi.moshi.adapter(ChargepriceMeta::class.java)) as ChargepriceMeta - chargePrices.value = Resource.success(result) + } else null + ), ChargepriceApi.getChargepriceLanguage() + ) + + val meta = result.meta!!.map(ChargepriceMeta::class.java, ChargepriceApi.moshi)!! + chargePrices.value = Resource.success(result.data) chargePriceMeta.value = Resource.success(meta) } catch (e: IOException) { chargePrices.value = Resource.error(e.message, null) diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/SettingsViewModel.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/SettingsViewModel.kt index 2f9355497..6c952b4cc 100644 --- a/app/src/main/java/net/vonforst/evmap/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/SettingsViewModel.kt @@ -11,9 +11,13 @@ import net.vonforst.evmap.api.chargeprice.ChargepriceTariff import net.vonforst.evmap.storage.AppDatabase import java.io.IOException -class SettingsViewModel(application: Application, chargepriceApiKey: String) : +class SettingsViewModel( + application: Application, + chargepriceApiKey: String, + chargepriceApiUrl: String +) : AndroidViewModel(application) { - private var api = ChargepriceApi.create(chargepriceApiKey) + private var api = ChargepriceApi.create(chargepriceApiKey, chargepriceApiUrl) private var db = AppDatabase.getInstance(application) val vehicles: MutableLiveData>> by lazy { diff --git a/app/src/main/res/layout/fragment_chargeprice.xml b/app/src/main/res/layout/fragment_chargeprice.xml index 838f8c99a..2531d53dc 100644 --- a/app/src/main/res/layout/fragment_chargeprice.xml +++ b/app/src/main/res/layout/fragment_chargeprice.xml @@ -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" /> + app:layout_constraintTop_toTopOf="@+id/charge_prices_list" + app:layout_constraintVertical_chainStyle="packed" /> +