mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-23 23:27:46 -05:00
committed by
Johan von Forstner
parent
f6f2b15f41
commit
21e23efb50
@@ -21,7 +21,7 @@ android {
|
||||
minSdk = 21
|
||||
targetSdk = 36
|
||||
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
|
||||
versionCode = 264
|
||||
versionCode = 266
|
||||
versionName = "2.0.1"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="chargeprice_api_url">https://staging-api.chargeprice.app/v1/</string>
|
||||
<string name="chargeprice_key">20c0d68918c9dc96c564784b711a6570</string>
|
||||
</resources>
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">EVMap</string>
|
||||
<string name="app_name">EVMap (debug)</string>
|
||||
</resources>
|
||||
@@ -1,27 +1,16 @@
|
||||
package net.vonforst.evmap.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.chip.Chip
|
||||
import net.vonforst.evmap.BR
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.api.chargeprice.ChargePrice
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceChargepointMeta
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceTag
|
||||
import net.vonforst.evmap.databinding.ItemChargepriceBinding
|
||||
import net.vonforst.evmap.databinding.ItemChargepriceVehicleChipBinding
|
||||
import net.vonforst.evmap.databinding.ItemConnectorButtonBinding
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.ui.CheckableConstraintLayout
|
||||
import java.time.Instant
|
||||
|
||||
interface Equatable {
|
||||
@@ -106,141 +95,4 @@ class ConnectorDetailsAdapter : DataBindingAdapter<ConnectorDetailsAdapter.Conne
|
||||
Equatable
|
||||
|
||||
override fun getItemViewType(position: Int): Int = R.layout.dialog_connector_details_item
|
||||
}
|
||||
|
||||
class ChargepriceAdapter :
|
||||
DataBindingAdapter<ChargePrice>() {
|
||||
|
||||
val viewPool = RecyclerView.RecycledViewPool()
|
||||
var meta: ChargepriceChargepointMeta? = null
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
var myTariffs: Set<String>? = null
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
var myTariffsAll: Boolean? = null
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_chargeprice
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder<ChargePrice> {
|
||||
val holder = super.onCreateViewHolder(parent, viewType)
|
||||
val binding = holder.binding as ItemChargepriceBinding
|
||||
binding.rvTags.apply {
|
||||
adapter = ChargepriceTagsAdapter()
|
||||
layoutManager =
|
||||
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false).apply {
|
||||
recycleChildrenOnDetach = true
|
||||
}
|
||||
itemAnimator = null
|
||||
setRecycledViewPool(viewPool)
|
||||
}
|
||||
return holder
|
||||
}
|
||||
|
||||
override fun bind(holder: ViewHolder<ChargePrice>, item: ChargePrice) {
|
||||
super.bind(holder, item)
|
||||
(holder.binding as ItemChargepriceBinding).apply {
|
||||
this.meta = this@ChargepriceAdapter.meta
|
||||
this.myTariffs = this@ChargepriceAdapter.myTariffs
|
||||
this.myTariffsAll = this@ChargepriceAdapter.myTariffsAll
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CheckableConnectorAdapter : DataBindingAdapter<Chargepoint>() {
|
||||
private var checkedItem: Int? = 0
|
||||
|
||||
var enabledConnectors: List<String>? = null
|
||||
get() = field
|
||||
set(value) {
|
||||
field = value
|
||||
checkedItem?.let {
|
||||
if (value != null && getItem(it).type !in value) {
|
||||
checkedItem = currentList.indexOfFirst {
|
||||
it.type in value
|
||||
}.takeIf { it != -1 }
|
||||
onCheckedItemChangedListener?.invoke(getCheckedItem())
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_connector_button
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder<Chargepoint>, position: Int) {
|
||||
val item = getItem(position)
|
||||
super.bind(holder, item)
|
||||
val binding = holder.binding as ItemConnectorButtonBinding
|
||||
binding.enabled = enabledConnectors?.let { item.type in it } ?: true
|
||||
val root = binding.root as CheckableConstraintLayout
|
||||
root.setOnCheckedChangeListener { _, _ -> }
|
||||
root.isChecked = checkedItem == position
|
||||
root.setOnClickListener {
|
||||
root.isChecked = true
|
||||
}
|
||||
root.setOnCheckedChangeListener { _, checked: Boolean ->
|
||||
if (checked) {
|
||||
checkedItem = holder.bindingAdapterPosition.takeIf { it != -1 }
|
||||
root.post {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
getCheckedItem()?.let { onCheckedItemChangedListener?.invoke(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getCheckedItem(): Chargepoint? = checkedItem?.let { getItem(it) }
|
||||
|
||||
fun setCheckedItem(item: Chargepoint?) {
|
||||
checkedItem = item?.let { currentList.indexOf(item) }.takeIf { it != -1 }
|
||||
}
|
||||
|
||||
var onCheckedItemChangedListener: ((Chargepoint?) -> Unit)? = null
|
||||
}
|
||||
|
||||
class ChargepriceTagsAdapter() :
|
||||
DataBindingAdapter<ChargepriceTag>() {
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_chargeprice_tag
|
||||
}
|
||||
|
||||
class CheckableChargepriceCarAdapter : DataBindingAdapter<ChargepriceCar>() {
|
||||
private var checkedItem: ChargepriceCar? = null
|
||||
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_chargeprice_vehicle_chip
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder<ChargepriceCar>, position: Int) {
|
||||
val item = getItem(position)
|
||||
super.bind(holder, item)
|
||||
val binding = holder.binding as ItemChargepriceVehicleChipBinding
|
||||
val root = binding.root as Chip
|
||||
root.isChecked = checkedItem == item
|
||||
root.setOnClickListener {
|
||||
root.isChecked = true
|
||||
}
|
||||
root.setOnCheckedChangeListener { _, checked: Boolean ->
|
||||
if (checked && item != checkedItem) {
|
||||
checkedItem = item
|
||||
root.post {
|
||||
notifyDataSetChanged()
|
||||
onCheckedItemChangedListener?.invoke(getCheckedItem()!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getCheckedItem(): ChargepriceCar? = checkedItem
|
||||
|
||||
fun setCheckedItem(item: ChargepriceCar?) {
|
||||
checkedItem = item
|
||||
}
|
||||
|
||||
var onCheckedItemChangedListener: ((ChargepriceCar?) -> Unit)? = null
|
||||
}
|
||||
@@ -1,99 +1,16 @@
|
||||
package net.vonforst.evmap.api.chargeprice
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.Moshi
|
||||
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.addDebugInterceptors
|
||||
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
|
||||
import retrofit2.http.POST
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
interface ChargepriceApi {
|
||||
@POST("charge_prices")
|
||||
suspend fun getChargePrices(
|
||||
@Body @jsonapi.retrofit.Document request: ChargepriceRequest,
|
||||
@Header("Accept-Language") language: String
|
||||
): Document<List<ChargePrice>>
|
||||
|
||||
@GET("vehicles")
|
||||
@jsonapi.retrofit.Document
|
||||
suspend fun getVehicles(): List<ChargepriceCar>
|
||||
|
||||
@GET("tariffs")
|
||||
@jsonapi.retrofit.Document
|
||||
suspend fun getTariffs(): List<ChargepriceTariff>
|
||||
|
||||
@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")
|
||||
|
||||
private val DATA_SOURCE_GOINGELECTRIC = "going_electric"
|
||||
private val DATA_SOURCE_OPENCHARGEMAP = "open_charge_map"
|
||||
|
||||
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(
|
||||
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/",
|
||||
context: Context? = null
|
||||
): ChargepriceApi {
|
||||
val client = OkHttpClient.Builder().apply {
|
||||
addInterceptor { chain ->
|
||||
// add API key to every request
|
||||
val original = chain.request()
|
||||
val new = original.newBuilder()
|
||||
.header("API-Key", apikey)
|
||||
.header("Content-Type", "application/json")
|
||||
.build()
|
||||
chain.proceed(new)
|
||||
}
|
||||
if (BuildConfig.DEBUG) {
|
||||
addDebugInterceptors()
|
||||
}
|
||||
if (context != null) {
|
||||
cache(Cache(context.cacheDir, cacheSize))
|
||||
}
|
||||
}.build()
|
||||
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseurl)
|
||||
.addConverterFactory(DocumentConverterFactory.create())
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.client(client)
|
||||
.build()
|
||||
return retrofit.create(ChargepriceApi::class.java)
|
||||
}
|
||||
|
||||
|
||||
fun getChargepriceLanguage(): String {
|
||||
val locale = Locale.getDefault().language
|
||||
return if (supportedLanguages.contains(locale)) {
|
||||
|
||||
@@ -1,466 +0,0 @@
|
||||
package net.vonforst.evmap.api.chargeprice
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.util.Patterns
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import jsonapi.*
|
||||
import kotlinx.parcelize.Parceler
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.parcelize.WriteWith
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.ui.currency
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
|
||||
|
||||
@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<ChargepriceTariff>? = null,
|
||||
@ToOne("vehicle")
|
||||
val vehicle: ChargepriceCar? = null,
|
||||
@RelationshipsObject var relationships: Relationships? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargepriceStation(
|
||||
val longitude: Double,
|
||||
val latitude: Double,
|
||||
val country: String?,
|
||||
val network: String?,
|
||||
@Json(name = "charge_points") val chargePoints: List<ChargepriceChargepoint>
|
||||
) {
|
||||
companion object {
|
||||
fun fromEvmap(
|
||||
charger: ChargeLocation,
|
||||
compatibleConnectors: List<String>,
|
||||
): ChargepriceStation {
|
||||
if (charger.chargepriceData == null) throw IllegalArgumentException()
|
||||
|
||||
val plugTypes =
|
||||
charger.chargepriceData.plugTypes ?: charger.chargepoints.map { it.type }
|
||||
return ChargepriceStation(
|
||||
charger.coordinates.lng,
|
||||
charger.coordinates.lat,
|
||||
charger.chargepriceData.country,
|
||||
charger.chargepriceData.network,
|
||||
charger.chargepoints.zip(plugTypes)
|
||||
.filter { equivalentPlugTypes(it.first.type).any { it in compatibleConnectors } }
|
||||
.map { ChargepriceChargepoint(it.first.power ?: 0.0, it.second) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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,
|
||||
val duration: Int? = null,
|
||||
@Json(name = "battery_range") val batteryRange: List<Double>? = null,
|
||||
@Json(name = "car_ac_phases") val carAcPhases: Int? = null,
|
||||
val currency: String? = null,
|
||||
@Json(name = "start_time") val startTime: Int? = null,
|
||||
@Json(name = "allow_unbalanced_load") val allowUnbalancedLoad: Boolean? = null,
|
||||
@Json(name = "provider_customer_tariffs") val providerCustomerTariffs: Boolean? = null,
|
||||
@Json(name = "show_price_unavailable") val showPriceUnavailable: Boolean? = null,
|
||||
@Json(name = "show_all_brand_restricted_tariffs") val showAllBrandRestrictedTariffs: Boolean? = null
|
||||
)
|
||||
|
||||
@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<String>,
|
||||
@Json(name = "charge_card_id")
|
||||
val chargeCardId: String?, // GE charge card ID
|
||||
) : Parcelable {
|
||||
val id: String
|
||||
get() = id_!!
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Resource("car")
|
||||
@Parcelize
|
||||
data class ChargepriceCar(
|
||||
@Id val id_: String?,
|
||||
val name: String,
|
||||
val brand: String,
|
||||
|
||||
@Json(name = "dc_charge_ports")
|
||||
val dcChargePorts: List<String>,
|
||||
|
||||
@Json(name = "usable_battery_size")
|
||||
val usableBatterySize: Float,
|
||||
|
||||
@Json(name = "ac_max_power")
|
||||
val acMaxPower: Float,
|
||||
|
||||
@Json(name = "dc_max_power")
|
||||
val dcMaxPower: Float?
|
||||
) : Equatable, Parcelable {
|
||||
fun formatSpecs(): String = buildString {
|
||||
append("%.0f kWh".format(usableBatterySize))
|
||||
append(" | ")
|
||||
append("AC %.0f kW".format(acMaxPower))
|
||||
dcMaxPower?.let {
|
||||
append(" | ")
|
||||
append("DC %.0f kW".format(it))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
val id: String
|
||||
get() = id_!!
|
||||
|
||||
val compatibleEvmapConnectors: List<String>
|
||||
get() = dcChargePorts.mapNotNull {
|
||||
plugMapping[it]
|
||||
}.plus(acConnectors)
|
||||
}
|
||||
|
||||
@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<ChargepriceTag>,
|
||||
|
||||
@Json(name = "charge_point_prices")
|
||||
val chargepointPrices: List<ChargepointPrice>,
|
||||
|
||||
@Json(name = "branding")
|
||||
val branding: ChargepriceBranding? = null,
|
||||
|
||||
@RelationshipsObject
|
||||
val relationships: @WriteWith<RelationshipsParceler>() Relationships? = null,
|
||||
) : Equatable, Cloneable, Parcelable {
|
||||
val tariffId: String?
|
||||
get() = (relationships?.get("tariff") as? Relationship.ToOne)?.data?.id
|
||||
|
||||
fun formatMonthlyFees(ctx: Context): String {
|
||||
return listOfNotNull(
|
||||
if (totalMonthlyFee > 0) {
|
||||
ctx.getString(R.string.chargeprice_base_fee, totalMonthlyFee, currency(currency))
|
||||
} else null,
|
||||
if (monthlyMinSales > 0) {
|
||||
ctx.getString(R.string.chargeprice_min_spend, monthlyMinSales, currency(currency))
|
||||
} else null
|
||||
).joinToString(", ")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parceler implementation for the Relationships object.
|
||||
* Note that this ignores certain fields that we don't need (links, meta, etc.)
|
||||
*/
|
||||
internal object RelationshipsParceler : Parceler<Relationships?> {
|
||||
override fun create(parcel: Parcel): Relationships? {
|
||||
if (parcel.readInt() == 0) return null
|
||||
|
||||
val nMembers = parcel.readInt()
|
||||
val members = (0 until nMembers).associate { _ ->
|
||||
val key = parcel.readString()!!
|
||||
val value = if (parcel.readInt() == 0) {
|
||||
val type = parcel.readString()
|
||||
val id = parcel.readString()
|
||||
val ri = if (type != null && id != null) {
|
||||
ResourceIdentifier(type, id)
|
||||
} else null
|
||||
Relationship.ToOne(ri)
|
||||
} else {
|
||||
val size = parcel.readInt()
|
||||
val ris = (0 until size).map { _ ->
|
||||
val type = parcel.readString()!!
|
||||
val id = parcel.readString()!!
|
||||
ResourceIdentifier(type, id)
|
||||
}
|
||||
Relationship.ToMany(ris)
|
||||
}
|
||||
key to value
|
||||
}
|
||||
|
||||
return Relationships(members)
|
||||
}
|
||||
|
||||
override fun Relationships?.write(parcel: Parcel, flags: Int) {
|
||||
if (this == null) {
|
||||
parcel.writeInt(0)
|
||||
return
|
||||
} else {
|
||||
parcel.writeInt(1)
|
||||
}
|
||||
|
||||
parcel.writeInt(members.size)
|
||||
for (member in this.members) {
|
||||
parcel.writeString(member.key)
|
||||
when (val value = member.value) {
|
||||
is Relationship.ToOne -> {
|
||||
parcel.writeInt(0)
|
||||
parcel.writeString(value.data?.type)
|
||||
parcel.writeString(value.data?.id)
|
||||
}
|
||||
is Relationship.ToMany -> {
|
||||
parcel.writeInt(1)
|
||||
parcel.writeInt(value.data.size)
|
||||
for (ri in value.data) {
|
||||
parcel.writeString(ri.type)
|
||||
parcel.writeString(ri.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class ChargepointPrice(
|
||||
val power: Double,
|
||||
val plug: String,
|
||||
val price: Double?,
|
||||
@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"
|
||||
}
|
||||
|
||||
fun time(value: Int): String {
|
||||
val h = floor(value.toDouble() / 60).toInt()
|
||||
val min = ceil(value.toDouble() % 60).toInt()
|
||||
return if (h == 0 && min > 0) "${min}min";
|
||||
// be slightly sloppy (3:01 is shown as 3h) to save space
|
||||
else if (h > 0 && (min == 0 || min == 1)) "${h}h";
|
||||
else "%d:%02dh".format(h, min)
|
||||
}
|
||||
|
||||
// based on https://github.com/chargeprice/chargeprice-client/blob/d420bb2f216d9ad91a210a36dd0859a368a8229a/src/views/priceList.js
|
||||
with(priceDistribution) {
|
||||
return listOfNotNull(
|
||||
if (session != null && session > 0.0) {
|
||||
(if (session < 1) percent(session) else "") + ctx.getString(R.string.chargeprice_session_fee)
|
||||
} else null,
|
||||
if (kwh != null && kwh > 0.0 && !isOnlyKwh) {
|
||||
(if (kwh < 1) percent(kwh) else "") + ctx.getString(R.string.chargeprice_per_kwh)
|
||||
} else null,
|
||||
if (minute != null && minute > 0.0) {
|
||||
(if (minute < 1) percent(minute) else "") + ctx.getString(R.string.chargeprice_per_minute) +
|
||||
if (blockingFeeStart != null) {
|
||||
" (${
|
||||
ctx.getString(
|
||||
R.string.chargeprice_blocking_fee,
|
||||
time(blockingFeeStart)
|
||||
)
|
||||
})"
|
||||
} else ""
|
||||
} else null,
|
||||
if ((minute == null || minute == 0.0) && blockingFeeStart != null) {
|
||||
ctx.getString(R.string.chargeprice_blocking_fee, time(blockingFeeStart))
|
||||
} else null
|
||||
).joinToString(" +\u00a0")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
) : 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)
|
||||
}
|
||||
|
||||
@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<ChargepriceChargepointMeta>
|
||||
)
|
||||
|
||||
enum class ChargepriceInclude {
|
||||
@Json(name = "filter")
|
||||
FILTER,
|
||||
@Json(name = "always")
|
||||
ALWAYS,
|
||||
@Json(name = "exclusive")
|
||||
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
|
||||
)
|
||||
|
||||
@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")
|
||||
}
|
||||
}
|
||||
@@ -1,402 +0,0 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.annotations.ExperimentalCarApi
|
||||
import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.hardware.CarHardwareManager
|
||||
import androidx.car.app.hardware.info.Model
|
||||
import androidx.car.app.model.Action
|
||||
import androidx.car.app.model.ActionStrip
|
||||
import androidx.car.app.model.CarIcon
|
||||
import androidx.car.app.model.ItemList
|
||||
import androidx.car.app.model.ListTemplate
|
||||
import androidx.car.app.model.Row
|
||||
import androidx.car.app.model.SectionedItemList
|
||||
import androidx.car.app.model.Template
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.erfansn.localeconfigx.currentOrDefaultLocale
|
||||
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 net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.chargeprice.ChargePrice
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceChargepointMeta
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceInclude
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceMeta
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceOptions
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceRequest
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceRequestTariffMeta
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceStation
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
import net.vonforst.evmap.api.nameForPlugType
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.currency
|
||||
import net.vonforst.evmap.ui.time
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@ExperimentalCarApi
|
||||
class ChargepriceScreen(ctx: CarContext, val session: EVMapSession, val charger: ChargeLocation) :
|
||||
Screen(ctx) {
|
||||
private val prefs = PreferenceDataSource(ctx)
|
||||
private val db = AppDatabase.getInstance(carContext)
|
||||
private val api by lazy {
|
||||
ChargepriceApi.create(
|
||||
carContext.getString(R.string.chargeprice_key),
|
||||
carContext.getString(R.string.chargeprice_api_url)
|
||||
)
|
||||
}
|
||||
private var prices: List<ChargePrice>? = null
|
||||
private var meta: ChargepriceChargepointMeta? = null
|
||||
private var chargepoint: Chargepoint? = null
|
||||
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
private var errorMessage: String? = null
|
||||
private val batteryRange = prefs.chargepriceBatteryRangeAndroidAuto
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
if (prices == null) loadData()
|
||||
|
||||
return ListTemplate.Builder().apply {
|
||||
setTitle(
|
||||
carContext.getString(
|
||||
R.string.chargeprice_battery_range,
|
||||
batteryRange[0],
|
||||
batteryRange[1]
|
||||
) + " · " + carContext.getString(R.string.powered_by_chargeprice)
|
||||
)
|
||||
setHeaderAction(Action.BACK)
|
||||
if (prices == null && errorMessage == null) {
|
||||
setLoading(true)
|
||||
} else {
|
||||
val header = meta?.let { meta ->
|
||||
chargepoint?.let { chargepoint ->
|
||||
"${
|
||||
nameForPlugType(
|
||||
carContext.stringProvider(),
|
||||
chargepoint.type
|
||||
)
|
||||
} ${chargepoint.formatPower(carContext.currentOrDefaultLocale)} ${
|
||||
carContext.getString(
|
||||
R.string.chargeprice_stats,
|
||||
meta.energy,
|
||||
time(meta.duration.roundToInt()),
|
||||
meta.energy / meta.duration * 60
|
||||
)
|
||||
}"
|
||||
}
|
||||
}
|
||||
val myTariffs = prefs.chargepriceMyTariffs
|
||||
val myTariffsAll = prefs.chargepriceMyTariffsAll
|
||||
|
||||
val prices = prices?.take(maxRows)
|
||||
if (prices != null && prices.isNotEmpty() && !myTariffsAll && myTariffs != null) {
|
||||
val (myPrices, otherPrices) = prices.partition { price -> price.tariffId in myTariffs }
|
||||
val myPricesList = buildPricesList(myPrices)
|
||||
val otherPricesList = buildPricesList(otherPrices)
|
||||
if (myPricesList.items.isNotEmpty() && otherPricesList.items.isNotEmpty()) {
|
||||
addSectionedList(
|
||||
SectionedItemList.create(
|
||||
myPricesList,
|
||||
(header?.let { it + "\n" } ?: "") +
|
||||
carContext.getString(R.string.chargeprice_header_my_tariffs)
|
||||
)
|
||||
)
|
||||
addSectionedList(
|
||||
SectionedItemList.create(
|
||||
otherPricesList,
|
||||
carContext.getString(R.string.chargeprice_header_other_tariffs)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
val list =
|
||||
if (myPricesList.items.isNotEmpty()) myPricesList else otherPricesList
|
||||
if (header != null) {
|
||||
addSectionedList(SectionedItemList.create(list, header))
|
||||
} else {
|
||||
setSingleList(list)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val list = buildPricesList(prices)
|
||||
if (header != null && list.items.isNotEmpty()) {
|
||||
addSectionedList(SectionedItemList.create(list, header))
|
||||
} else {
|
||||
setSingleList(list)
|
||||
}
|
||||
}
|
||||
}
|
||||
setActionStrip(
|
||||
ActionStrip.Builder().addAction(
|
||||
Action.Builder().setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_chargeprice
|
||||
)
|
||||
).build()
|
||||
).setOnClickListener {
|
||||
openUrl(carContext, session.cas, ChargepriceApi.getPoiUrl(charger))
|
||||
}.build()
|
||||
).build()
|
||||
)
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun buildPricesList(prices: List<ChargePrice>?): ItemList {
|
||||
return ItemList.Builder().apply {
|
||||
setNoItemsMessage(
|
||||
errorMessage
|
||||
?: carContext.getString(R.string.chargeprice_no_tariffs_found)
|
||||
)
|
||||
prices?.forEach { price ->
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(formatProvider(price))
|
||||
addText(formatPrice(price))
|
||||
}.build())
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun formatProvider(price: ChargePrice): String {
|
||||
if (!price.tariffName.startsWith(price.provider)) {
|
||||
return price.provider + " " + price.tariffName
|
||||
} else {
|
||||
return price.tariffName
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatPrice(price: ChargePrice): String {
|
||||
val amount = price.chargepointPrices.first().price
|
||||
?: return "${carContext.getString(R.string.chargeprice_price_not_available)} (${price.chargepointPrices.first().noPriceReason})"
|
||||
val totalPrice = carContext.getString(
|
||||
R.string.charge_price_format,
|
||||
amount,
|
||||
currency(price.currency)
|
||||
)
|
||||
val kwhPrice = if (amount > 0f) {
|
||||
carContext.getString(
|
||||
if (price.chargepointPrices[0].priceDistribution.isOnlyKwh) {
|
||||
R.string.charge_price_kwh_format
|
||||
} else {
|
||||
R.string.charge_price_average_format
|
||||
},
|
||||
amount / meta!!.energy,
|
||||
currency(price.currency)
|
||||
)
|
||||
} else null
|
||||
val monthlyFees = if (price.totalMonthlyFee > 0 || price.monthlyMinSales > 0) {
|
||||
price.formatMonthlyFees(carContext)
|
||||
} else null
|
||||
var text = totalPrice
|
||||
if (kwhPrice != null && monthlyFees != null) {
|
||||
text += " ($kwhPrice, $monthlyFees)"
|
||||
} else if (kwhPrice != null) {
|
||||
text += " ($kwhPrice)"
|
||||
} else if (monthlyFees != null) {
|
||||
text += " ($monthlyFees)"
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
private fun loadData() {
|
||||
if (supportsCarApiLevel3(carContext)) {
|
||||
val exec = ContextCompat.getMainExecutor(carContext)
|
||||
val hardwareMan =
|
||||
carContext.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
|
||||
hardwareMan.carInfo.fetchModel(exec) { model ->
|
||||
loadPrices(model)
|
||||
}
|
||||
} else {
|
||||
loadPrices(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadPrices(model: Model?) {
|
||||
val dataAdapter = ChargepriceApi.getDataAdapter(charger)
|
||||
val manufacturer = getVehicleBrand(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)
|
||||
|
||||
if (cpStation.chargePoints.isEmpty()) {
|
||||
errorMessage =
|
||||
carContext.getString(R.string.chargeprice_no_compatible_connectors)
|
||||
invalidate()
|
||||
return@launch
|
||||
}
|
||||
|
||||
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,
|
||||
showPriceUnavailable = true
|
||||
),
|
||||
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
|
||||
), ChargepriceApi.getChargepriceLanguage()
|
||||
)
|
||||
|
||||
val myTariffs = prefs.chargepriceMyTariffs
|
||||
|
||||
// choose the highest power chargepoint
|
||||
// (we have already filtered so that only compatible ones are included)
|
||||
val chargepoint = cpStation.chargePoints.maxByOrNull { it.power }
|
||||
|
||||
val index = cpStation.chargePoints.indexOf(chargepoint)
|
||||
this@ChargepriceScreen.chargepoint =
|
||||
charger.chargepoints.filter { equivalentPlugTypes(it.type).any { it in car.compatibleEvmapConnectors } }[index]
|
||||
|
||||
if (chargepoint == null) {
|
||||
errorMessage =
|
||||
carContext.getString(R.string.chargeprice_no_compatible_connectors)
|
||||
invalidate()
|
||||
return@launch
|
||||
}
|
||||
|
||||
val metaMapped =
|
||||
result.meta!!.map(ChargepriceMeta::class.java, ChargepriceApi.moshi)!!
|
||||
meta = metaMapped.chargePoints.maxByOrNull { it.power }
|
||||
|
||||
prices = result.data!!.mapNotNull { cp ->
|
||||
val filteredPrices =
|
||||
cp.chargepointPrices.filter {
|
||||
it.plug == chargepoint.plug && it.power == chargepoint.power
|
||||
}
|
||||
if (filteredPrices.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
cp.copy(
|
||||
chargepointPrices = filteredPrices
|
||||
)
|
||||
}
|
||||
}
|
||||
.sortedBy { it.chargepointPrices.first().price ?: Double.MAX_VALUE }
|
||||
.sortedByDescending {
|
||||
prefs.chargepriceMyTariffsAll ||
|
||||
myTariffs != null && it.tariffId in myTariffs
|
||||
}
|
||||
invalidate()
|
||||
} catch (e: IOException) {
|
||||
withContext(Dispatchers.Main) {
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.chargeprice_connection_error,
|
||||
CarToast.LENGTH_LONG
|
||||
)
|
||||
.show()
|
||||
}
|
||||
} catch (e: HttpException) {
|
||||
withContext(Dispatchers.Main) {
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.chargeprice_connection_error,
|
||||
CarToast.LENGTH_LONG
|
||||
)
|
||||
.show()
|
||||
}
|
||||
} catch (e: NoVehicleSelectedException) {
|
||||
errorMessage = carContext.getString(R.string.chargeprice_select_car_first)
|
||||
invalidate()
|
||||
} catch (e: VehicleUnknownException) {
|
||||
errorMessage = carContext.getString(
|
||||
R.string.auto_chargeprice_vehicle_unknown,
|
||||
manufacturer,
|
||||
modelName
|
||||
)
|
||||
invalidate()
|
||||
} catch (e: VehicleAmbiguousException) {
|
||||
errorMessage = carContext.getString(
|
||||
R.string.auto_chargeprice_vehicle_ambiguous,
|
||||
manufacturer,
|
||||
modelName
|
||||
)
|
||||
invalidate()
|
||||
} catch (e: VehicleUnavailableException) {
|
||||
errorMessage =
|
||||
carContext.getString(R.string.auto_chargeprice_vehicle_unavailable)
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class NoVehicleSelectedException : Exception()
|
||||
private class VehicleUnknownException : Exception()
|
||||
private class VehicleAmbiguousException : Exception()
|
||||
private class VehicleUnavailableException : Exception()
|
||||
|
||||
private suspend fun determineVehicle(
|
||||
manufacturer: String?,
|
||||
modelName: String?
|
||||
): ChargepriceCar {
|
||||
var vehicles = api.getVehicles().filter {
|
||||
it.id in prefs.chargepriceMyVehicles
|
||||
}
|
||||
if (vehicles.isEmpty()) {
|
||||
throw NoVehicleSelectedException()
|
||||
} else if (vehicles.size > 1) {
|
||||
if (manufacturer != null) {
|
||||
vehicles = vehicles.filter {
|
||||
it.brand.lowercase() == getVehicleBrand(manufacturer)?.lowercase()
|
||||
}
|
||||
if (vehicles.isEmpty()) {
|
||||
throw VehicleUnknownException()
|
||||
} else if (vehicles.size > 1) {
|
||||
if (modelName != null) {
|
||||
vehicles = vehicles.filter {
|
||||
it.name.lowercase().startsWith(modelName.lowercase())
|
||||
}
|
||||
if (vehicles.isEmpty()) {
|
||||
throw VehicleUnknownException()
|
||||
} else if (vehicles.size > 1) {
|
||||
throw VehicleAmbiguousException()
|
||||
}
|
||||
} else {
|
||||
throw VehicleAmbiguousException()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw VehicleUnavailableException()
|
||||
}
|
||||
}
|
||||
return vehicles[0]
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,6 @@ import androidx.core.text.HtmlCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import com.github.erfansn.localeconfigx.currentOrDefaultLocale
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -152,22 +151,12 @@ class ChargerDetailScreen(
|
||||
)
|
||||
.setTitle(carContext.getString(R.string.auto_prices))
|
||||
.setOnClickListener {
|
||||
if (prefs.chargepriceNativeIntegration) {
|
||||
screenManager.push(
|
||||
ChargepriceScreen(
|
||||
carContext,
|
||||
session,
|
||||
charger
|
||||
)
|
||||
)
|
||||
} else {
|
||||
val intent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(ChargepriceApi.getPoiUrl(charger))
|
||||
)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
session.cas.startActivity(intent)
|
||||
}
|
||||
val intent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(ChargepriceApi.getPoiUrl(charger))
|
||||
)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
session.cas.startActivity(intent)
|
||||
}
|
||||
.build())
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorManager
|
||||
@@ -17,8 +15,6 @@ import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.model.Action
|
||||
import androidx.car.app.model.CarColor
|
||||
import androidx.car.app.model.CarIcon
|
||||
import androidx.car.app.model.GridItem
|
||||
import androidx.car.app.model.GridTemplate
|
||||
import androidx.car.app.model.ItemList
|
||||
import androidx.car.app.model.ListTemplate
|
||||
import androidx.car.app.model.MessageTemplate
|
||||
@@ -27,15 +23,12 @@ import androidx.car.app.model.Row
|
||||
import androidx.car.app.model.SectionedItemList
|
||||
import androidx.car.app.model.Template
|
||||
import androidx.car.app.model.Toggle
|
||||
import androidx.core.content.IntentCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.single
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.EXTRA_DONATE
|
||||
@@ -44,11 +37,6 @@ import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.addDebugInterceptors
|
||||
import net.vonforst.evmap.api.availability.tesla.TeslaAuthenticationApi
|
||||
import net.vonforst.evmap.api.availability.tesla.TeslaOwnerApi
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceTariff
|
||||
import net.vonforst.evmap.currencyDisplayName
|
||||
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragment
|
||||
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragmentArgs
|
||||
import net.vonforst.evmap.getPackageInfoCompat
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
@@ -57,8 +45,6 @@ import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import okhttp3.OkHttpClient
|
||||
import java.io.IOException
|
||||
import java.time.Instant
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
@ExperimentalCarApi
|
||||
class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
@@ -86,23 +72,6 @@ class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
screenManager.push(DataSettingsScreen(carContext, session))
|
||||
}
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.settings_chargeprice))
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_chargeprice
|
||||
)
|
||||
).setTint(
|
||||
CarColor.DEFAULT
|
||||
).build()
|
||||
)
|
||||
setBrowsable(true)
|
||||
setOnClickListener {
|
||||
screenManager.push(ChargepriceSettingsScreen(carContext))
|
||||
}
|
||||
}.build())
|
||||
if (supportsCarApiLevel3(carContext)) {
|
||||
addItem(
|
||||
Row.Builder()
|
||||
@@ -507,341 +476,6 @@ class ChooseDataSourceScreen(
|
||||
}
|
||||
}
|
||||
|
||||
class ChargepriceSettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
val prefs = PreferenceDataSource(carContext)
|
||||
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
return ListTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.settings_chargeprice))
|
||||
setHeaderAction(Action.BACK)
|
||||
setSingleList(ItemList.Builder().apply {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_chargeprice_native_integration))
|
||||
addText(carContext.getString(if (prefs.chargepriceNativeIntegration) R.string.pref_chargeprice_native_integration_on else R.string.pref_chargeprice_native_integration_off))
|
||||
setToggle(Toggle.Builder {
|
||||
prefs.chargepriceNativeIntegration = it
|
||||
invalidate()
|
||||
}.setChecked(prefs.chargepriceNativeIntegration).build())
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_my_vehicle))
|
||||
setBrowsable(true)
|
||||
setOnClickListener {
|
||||
screenManager.push(SelectVehiclesScreen(carContext))
|
||||
}
|
||||
setEnabled(prefs.chargepriceNativeIntegration)
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_my_tariffs))
|
||||
setBrowsable(true)
|
||||
setOnClickListener {
|
||||
screenManager.push(SelectTariffsScreen(carContext))
|
||||
}
|
||||
addText(
|
||||
if (prefs.chargepriceMyTariffsAll) {
|
||||
carContext.getString(R.string.chargeprice_all_tariffs_selected)
|
||||
} else {
|
||||
val n = prefs.chargepriceMyTariffs?.size ?: 0
|
||||
carContext.resources
|
||||
.getQuantityString(
|
||||
R.plurals.chargeprice_some_tariffs_selected,
|
||||
n,
|
||||
n
|
||||
) + "\n" + carContext.resources.getQuantityString(
|
||||
R.plurals.pref_my_tariffs_summary,
|
||||
n
|
||||
)
|
||||
}
|
||||
)
|
||||
setEnabled(prefs.chargepriceNativeIntegration)
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.settings_android_auto_chargeprice_range))
|
||||
setBrowsable(true)
|
||||
|
||||
val range = prefs.chargepriceBatteryRangeAndroidAuto
|
||||
addText(
|
||||
carContext.getString(
|
||||
R.string.chargeprice_battery_range,
|
||||
range[0],
|
||||
range[1]
|
||||
)
|
||||
)
|
||||
|
||||
setOnClickListener {
|
||||
screenManager.push(SelectChargingRangeScreen(carContext))
|
||||
}
|
||||
setEnabled(prefs.chargepriceNativeIntegration)
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_chargeprice_currency))
|
||||
|
||||
val values =
|
||||
carContext.resources.getStringArray(R.array.pref_chargeprice_currencies)
|
||||
val names = values.map(::currencyDisplayName)
|
||||
val index = values.indexOf(prefs.chargepriceCurrency)
|
||||
addText(if (index >= 0) names[index] else "")
|
||||
|
||||
setBrowsable(true)
|
||||
setOnClickListener {
|
||||
screenManager.push(SelectCurrencyScreen(carContext))
|
||||
}
|
||||
setEnabled(prefs.chargepriceNativeIntegration)
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_chargeprice_no_base_fee))
|
||||
setToggle(Toggle.Builder {
|
||||
prefs.chargepriceNoBaseFee = it
|
||||
}.setChecked(prefs.chargepriceNoBaseFee).build())
|
||||
setEnabled(prefs.chargepriceNativeIntegration)
|
||||
}.build())
|
||||
if (maxRows > 6) {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_chargeprice_show_provider_customer_tariffs))
|
||||
addText(carContext.getString(R.string.pref_chargeprice_show_provider_customer_tariffs_summary))
|
||||
setToggle(Toggle.Builder {
|
||||
prefs.chargepriceShowProviderCustomerTariffs = it
|
||||
}.setChecked(prefs.chargepriceShowProviderCustomerTariffs).build())
|
||||
setEnabled(prefs.chargepriceNativeIntegration)
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.pref_chargeprice_allow_unbalanced_load))
|
||||
addText(carContext.getString(R.string.pref_chargeprice_allow_unbalanced_load_summary))
|
||||
setToggle(Toggle.Builder {
|
||||
prefs.chargepriceAllowUnbalancedLoad = it
|
||||
}.setChecked(prefs.chargepriceAllowUnbalancedLoad).build())
|
||||
setEnabled(prefs.chargepriceNativeIntegration)
|
||||
}.build())
|
||||
}
|
||||
}.build())
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
|
||||
class SelectVehiclesScreen(ctx: CarContext) : MultiSelectSearchScreen<ChargepriceCar>(ctx) {
|
||||
private val prefs = PreferenceDataSource(carContext)
|
||||
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
|
||||
|
||||
override fun isSelected(it: ChargepriceCar): Boolean {
|
||||
return prefs.chargepriceMyVehicles.contains(it.id)
|
||||
}
|
||||
|
||||
override fun toggleSelected(item: ChargepriceCar) {
|
||||
if (isSelected(item)) {
|
||||
prefs.chargepriceMyVehicles = prefs.chargepriceMyVehicles.minus(item.id)
|
||||
} else {
|
||||
prefs.chargepriceMyVehicles = prefs.chargepriceMyVehicles.plus(item.id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLabel(it: ChargepriceCar) = "${it.brand} ${it.name}"
|
||||
|
||||
override fun getDetails(it: ChargepriceCar) = it.formatSpecs()
|
||||
|
||||
override suspend fun loadData(): List<ChargepriceCar> {
|
||||
return api.getVehicles()
|
||||
}
|
||||
}
|
||||
|
||||
class SelectTariffsScreen(ctx: CarContext) : MultiSelectSearchScreen<ChargepriceTariff>(ctx) {
|
||||
private val prefs = PreferenceDataSource(carContext)
|
||||
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
|
||||
|
||||
override fun isSelected(it: ChargepriceTariff): Boolean {
|
||||
return prefs.chargepriceMyTariffsAll or (prefs.chargepriceMyTariffs?.contains(it.id)
|
||||
?: false)
|
||||
}
|
||||
|
||||
override fun toggleSelected(item: ChargepriceTariff) {
|
||||
val tariffs = prefs.chargepriceMyTariffs ?: if (prefs.chargepriceMyTariffsAll) {
|
||||
fullList!!.map { it.id }.toSet()
|
||||
} else {
|
||||
emptySet()
|
||||
}
|
||||
if (isSelected(item)) {
|
||||
prefs.chargepriceMyTariffs = tariffs.minus(item.id)
|
||||
prefs.chargepriceMyTariffsAll = false
|
||||
} else {
|
||||
prefs.chargepriceMyTariffs = tariffs.plus(item.id)
|
||||
if (prefs.chargepriceMyTariffs == fullList!!.map { it.id }.toSet()) {
|
||||
prefs.chargepriceMyTariffsAll = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun selectAll() {
|
||||
prefs.chargepriceMyTariffsAll = true
|
||||
super.selectAll()
|
||||
}
|
||||
|
||||
override fun selectNone() {
|
||||
prefs.chargepriceMyTariffsAll = false
|
||||
prefs.chargepriceMyTariffs = emptySet()
|
||||
super.selectNone()
|
||||
}
|
||||
|
||||
override fun getLabel(it: ChargepriceTariff): String {
|
||||
return if (!it.name.lowercase().startsWith(it.provider.lowercase())) {
|
||||
"${it.provider} ${it.name}"
|
||||
} else {
|
||||
it.name
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun loadData(): List<ChargepriceTariff> {
|
||||
return api.getTariffs()
|
||||
}
|
||||
}
|
||||
|
||||
class SelectCurrencyScreen(ctx: CarContext) : MultiSelectSearchScreen<Pair<String, String>>(ctx) {
|
||||
private val prefs = PreferenceDataSource(carContext)
|
||||
override val isMultiSelect = false
|
||||
override val shouldShowSelectAll = false
|
||||
|
||||
override fun isSelected(it: Pair<String, String>): Boolean =
|
||||
prefs.chargepriceCurrency == it.second
|
||||
|
||||
override fun toggleSelected(item: Pair<String, String>) {
|
||||
prefs.chargepriceCurrency = item.second
|
||||
}
|
||||
|
||||
override fun getLabel(it: Pair<String, String>): String = it.first
|
||||
|
||||
override suspend fun loadData(): List<Pair<String, String>> {
|
||||
val values = carContext.resources.getStringArray(R.array.pref_chargeprice_currencies)
|
||||
val names = values.map(::currencyDisplayName)
|
||||
return names.zip(values)
|
||||
}
|
||||
}
|
||||
|
||||
class SelectChargingRangeScreen(ctx: CarContext) : Screen(ctx) {
|
||||
val prefs = PreferenceDataSource(carContext)
|
||||
private val maxItems = if (ctx.carAppApiLevel >= 2) {
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_GRID)
|
||||
} else 6
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
return GridTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.settings_android_auto_chargeprice_range))
|
||||
setHeaderAction(Action.BACK)
|
||||
setSingleList(
|
||||
ItemList.Builder().apply {
|
||||
addItem(GridItem.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.chargeprice_battery_range_from))
|
||||
setText(
|
||||
carContext.getString(
|
||||
R.string.percent_format,
|
||||
prefs.chargepriceBatteryRangeAndroidAuto[0]
|
||||
)
|
||||
)
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_add
|
||||
)
|
||||
).build()
|
||||
)
|
||||
setOnClickListener {
|
||||
prefs.chargepriceBatteryRangeAndroidAuto =
|
||||
prefs.chargepriceBatteryRangeAndroidAuto.toMutableList().apply {
|
||||
this[0] = min(this[1] - 5, this[0] + 5)
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
}.build())
|
||||
addItem(GridItem.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.chargeprice_battery_range_to))
|
||||
setText(
|
||||
carContext.getString(
|
||||
R.string.percent_format,
|
||||
prefs.chargepriceBatteryRangeAndroidAuto[1]
|
||||
)
|
||||
)
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_add
|
||||
)
|
||||
).build()
|
||||
)
|
||||
setOnClickListener {
|
||||
prefs.chargepriceBatteryRangeAndroidAuto =
|
||||
prefs.chargepriceBatteryRangeAndroidAuto.toMutableList().apply {
|
||||
this[1] = min(100f, this[1] + 5)
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
}.build())
|
||||
|
||||
val nSpacers = when {
|
||||
maxItems % 3 == 0 -> 1
|
||||
maxItems == 100 -> 0 // AA has increased the limit to 100 and changed the way items are laid out
|
||||
maxItems % 4 == 0 -> 2
|
||||
else -> 0
|
||||
}
|
||||
|
||||
for (i in 0..nSpacers) {
|
||||
addItem(GridItem.Builder().apply {
|
||||
setTitle(" ")
|
||||
setImage(emptyCarIcon)
|
||||
}.build())
|
||||
}
|
||||
|
||||
addItem(GridItem.Builder().apply {
|
||||
setTitle(" ")
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_remove
|
||||
)
|
||||
).build()
|
||||
)
|
||||
setOnClickListener {
|
||||
prefs.chargepriceBatteryRangeAndroidAuto =
|
||||
prefs.chargepriceBatteryRangeAndroidAuto.toMutableList().apply {
|
||||
this[0] = max(0f, this[0] - 5)
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
}.build())
|
||||
addItem(GridItem.Builder().apply {
|
||||
setTitle(" ")
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_remove
|
||||
)
|
||||
).build()
|
||||
)
|
||||
setOnClickListener {
|
||||
prefs.chargepriceBatteryRangeAndroidAuto =
|
||||
prefs.chargepriceBatteryRangeAndroidAuto.toMutableList().apply {
|
||||
this[1] = max(this[0] + 5, this[1] - 5)
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
}.build())
|
||||
}.build()
|
||||
)
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalCarApi
|
||||
class AboutScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
val prefs = PreferenceDataSource(ctx)
|
||||
|
||||
@@ -1,275 +0,0 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.transition.MaterialContainerTransform
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
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.adapter.SingleViewAdapter
|
||||
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.databinding.FragmentChargepriceHeaderBinding
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.navigation.safeNavigate
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.viewmodel.ChargepriceViewModel
|
||||
import net.vonforst.evmap.viewmodel.Status
|
||||
import net.vonforst.evmap.viewmodel.savedStateViewModelFactory
|
||||
import java.text.NumberFormat
|
||||
|
||||
class ChargepriceFragment : Fragment() {
|
||||
private lateinit var binding: FragmentChargepriceBinding
|
||||
private lateinit var headerBinding: FragmentChargepriceHeaderBinding
|
||||
private var connectionErrorSnackbar: Snackbar? = null
|
||||
|
||||
private val vm: ChargepriceViewModel by viewModels(factoryProducer = {
|
||||
savedStateViewModelFactory { state ->
|
||||
ChargepriceViewModel(
|
||||
requireActivity().application,
|
||||
getString(R.string.chargeprice_key),
|
||||
getString(R.string.chargeprice_api_url),
|
||||
state
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
sharedElementEnterTransition = MaterialContainerTransform()
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
val prefs = PreferenceDataSource(requireContext())
|
||||
prefs.chargepriceCounter += 1
|
||||
if ((prefs.chargepriceCounter).mod(30) == 0) {
|
||||
showDonationDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
vm.reloadPrefs()
|
||||
}
|
||||
|
||||
private fun showDonationDialog() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.chargeprice_donation_dialog_title)
|
||||
.setMessage(R.string.chargeprice_donation_dialog_detail)
|
||||
.setNegativeButton(R.string.ok) { di, _ ->
|
||||
di.cancel()
|
||||
}
|
||||
.setPositiveButton(R.string.donate) { di, _ ->
|
||||
di.dismiss()
|
||||
findNavController().safeNavigate(ChargepriceFragmentDirections.actionChargepriceToDonateFragment())
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = DataBindingUtil.inflate(
|
||||
inflater,
|
||||
R.layout.fragment_chargeprice, container, false
|
||||
)
|
||||
headerBinding = DataBindingUtil.inflate(
|
||||
inflater,
|
||||
R.layout.fragment_chargeprice_header, container, false
|
||||
)
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
binding.vm = vm
|
||||
headerBinding.lifecycleOwner = viewLifecycleOwner
|
||||
headerBinding.vm = vm
|
||||
|
||||
binding.toolbar.inflateMenu(R.menu.chargeprice)
|
||||
binding.toolbar.setTitle(R.string.chargeprice_title)
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.chargePricesList) { v, insets ->
|
||||
v.updatePadding(bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom)
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.toolbar.setupWithNavController(
|
||||
findNavController(),
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
|
||||
val fragmentArgs: ChargepriceFragmentArgs by navArgs()
|
||||
val charger = fragmentArgs.charger
|
||||
vm.charger.value = charger
|
||||
if (vm.chargepoint.value == null) {
|
||||
vm.chargepoint.value = charger.chargepointsMerged[0]
|
||||
}
|
||||
|
||||
val vehicleAdapter = CheckableChargepriceCarAdapter()
|
||||
headerBinding.vehicleSelection.adapter = vehicleAdapter
|
||||
val vehicleObserver: Observer<ChargepriceCar?> = Observer {
|
||||
vehicleAdapter.setCheckedItem(it)
|
||||
}
|
||||
vm.vehicle.observe(viewLifecycleOwner, vehicleObserver)
|
||||
vehicleAdapter.onCheckedItemChangedListener = {
|
||||
vm.vehicle.removeObserver(vehicleObserver)
|
||||
vm.vehicle.value = it
|
||||
vm.vehicle.observe(viewLifecycleOwner, vehicleObserver)
|
||||
}
|
||||
|
||||
val chargepriceAdapter = ChargepriceAdapter().apply {
|
||||
onClickListener = {
|
||||
(requireActivity() as MapsActivity).openUrl(it.url, binding.root)
|
||||
}
|
||||
}
|
||||
val joinedAdapter = ConcatAdapter(
|
||||
SingleViewAdapter(headerBinding.root),
|
||||
chargepriceAdapter
|
||||
)
|
||||
binding.chargePricesList.apply {
|
||||
adapter = joinedAdapter
|
||||
layoutManager =
|
||||
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
addItemDecoration(
|
||||
DividerItemDecoration(
|
||||
context, LinearLayoutManager.VERTICAL
|
||||
)
|
||||
)
|
||||
}
|
||||
vm.chargepriceMetaForChargepoint.observe(viewLifecycleOwner) {
|
||||
chargepriceAdapter.meta = it?.data
|
||||
}
|
||||
vm.myTariffs.observe(viewLifecycleOwner) {
|
||||
chargepriceAdapter.myTariffs = it
|
||||
}
|
||||
vm.myTariffsAll.observe(viewLifecycleOwner) {
|
||||
chargepriceAdapter.myTariffsAll = it
|
||||
}
|
||||
vm.chargePricesForChargepoint.observe(viewLifecycleOwner) {
|
||||
chargepriceAdapter.submitList(it?.data ?: emptyList())
|
||||
}
|
||||
|
||||
val connectorsAdapter = CheckableConnectorAdapter()
|
||||
|
||||
val observer: Observer<Chargepoint?> = Observer {
|
||||
connectorsAdapter.setCheckedItem(it)
|
||||
}
|
||||
vm.chargepoint.observe(viewLifecycleOwner, observer)
|
||||
connectorsAdapter.onCheckedItemChangedListener = {
|
||||
vm.chargepoint.removeObserver(observer)
|
||||
vm.chargepoint.value = it
|
||||
vm.chargepoint.observe(viewLifecycleOwner, observer)
|
||||
}
|
||||
|
||||
vm.vehicleCompatibleConnectors.observe(viewLifecycleOwner) { plugs ->
|
||||
connectorsAdapter.enabledConnectors =
|
||||
plugs?.flatMap { plug -> equivalentPlugTypes(plug) }
|
||||
}
|
||||
|
||||
headerBinding.connectorsList.apply {
|
||||
adapter = connectorsAdapter
|
||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
||||
}
|
||||
|
||||
binding.imgChargepriceLogo.setOnClickListener {
|
||||
(requireActivity() as MapsActivity).openUrl(
|
||||
ChargepriceApi.getPoiUrl(charger),
|
||||
binding.root
|
||||
)
|
||||
}
|
||||
|
||||
binding.btnSettings.setOnClickListener {
|
||||
findNavController().safeNavigate(ChargepriceFragmentDirections.actionChargepriceToChargepriceSettingsFragment())
|
||||
}
|
||||
|
||||
headerBinding.batteryRange.setLabelFormatter { value: Float ->
|
||||
val fmt = NumberFormat.getNumberInstance()
|
||||
fmt.maximumFractionDigits = 0
|
||||
fmt.format(value.toDouble()) + "%"
|
||||
}
|
||||
headerBinding.batteryRange.setOnTouchListener { _: View, motionEvent: MotionEvent ->
|
||||
when (motionEvent.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> vm.batteryRangeSliderDragging.value = true
|
||||
MotionEvent.ACTION_UP -> vm.batteryRangeSliderDragging.value = false
|
||||
}
|
||||
false
|
||||
}
|
||||
headerBinding.tvChargeFromTo.setOnClickListener {
|
||||
it.postDelayed({
|
||||
vm.resetBatteryRangeToDefault()
|
||||
}, 250)
|
||||
}
|
||||
|
||||
binding.toolbar.setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.menu_help -> {
|
||||
(activity as? MapsActivity)?.openUrl(
|
||||
getString(R.string.chargeprice_faq_link),
|
||||
binding.root
|
||||
)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
vm.chargePricesForChargepoint.observe(viewLifecycleOwner) { res ->
|
||||
when (res?.status) {
|
||||
Status.ERROR -> {
|
||||
if (vm.vehicle.value == null) return@observe
|
||||
connectionErrorSnackbar?.dismiss()
|
||||
connectionErrorSnackbar = Snackbar
|
||||
.make(
|
||||
view,
|
||||
R.string.chargeprice_connection_error,
|
||||
Snackbar.LENGTH_INDEFINITE
|
||||
)
|
||||
.setAction(R.string.retry) {
|
||||
connectionErrorSnackbar?.dismiss()
|
||||
vm.loadPrices()
|
||||
}
|
||||
connectionErrorSnackbar!!.show()
|
||||
}
|
||||
Status.SUCCESS, null -> {
|
||||
connectionErrorSnackbar?.dismiss()
|
||||
}
|
||||
Status.LOADING -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround for AndroidX bug: https://github.com/material-components/material-components-android/issues/1984
|
||||
view.setBackgroundColor(MaterialColors.getColor(view, android.R.attr.windowBackground))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -47,7 +47,6 @@ import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.FragmentNavigatorExtras
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
@@ -448,19 +447,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
}
|
||||
binding.detailView.btnChargeprice.setOnClickListener {
|
||||
val charger = vm.charger.value?.data ?: return@setOnClickListener
|
||||
if (prefs.chargepriceNativeIntegration) {
|
||||
val extras =
|
||||
FragmentNavigatorExtras(binding.detailView.btnChargeprice to getString(R.string.shared_element_chargeprice))
|
||||
findNavController().safeNavigate(
|
||||
MapFragmentDirections.actionMapToChargepriceFragment(charger),
|
||||
extras
|
||||
)
|
||||
} else {
|
||||
(activity as? MapsActivity)?.openUrl(
|
||||
ChargepriceApi.getPoiUrl(charger),
|
||||
binding.root
|
||||
)
|
||||
}
|
||||
(activity as? MapsActivity)?.openUrl(
|
||||
ChargepriceApi.getPoiUrl(charger),
|
||||
binding.root
|
||||
)
|
||||
}
|
||||
binding.detailView.btnChargerWebsite.setOnClickListener {
|
||||
val charger = vm.charger.value?.data ?: return@setOnClickListener
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package net.vonforst.evmap.fragment.preference
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.ui.RangeSliderPreference
|
||||
import java.text.NumberFormat
|
||||
|
||||
class AndroidAutoSettingsFragment : BaseSettingsFragment() {
|
||||
override val isTopLevel = false
|
||||
|
||||
private lateinit var rangePreference: RangeSliderPreference
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
rangePreference = findPreference("chargeprice_battery_range_android_auto")!!
|
||||
rangePreference.labelFormatter = { value: Float ->
|
||||
val fmt = NumberFormat.getNumberInstance()
|
||||
fmt.maximumFractionDigits = 0
|
||||
fmt.format(value.toDouble()) + "%"
|
||||
}
|
||||
updateRangePreferenceSummary()
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.settings_android_auto, rootKey)
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
|
||||
when (key) {
|
||||
"chargeprice_battery_range_android_auto_min", "chargeprice_battery_range_android_auto_max" -> {
|
||||
updateRangePreferenceSummary()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateRangePreferenceSummary() {
|
||||
val range = prefs.chargepriceBatteryRangeAndroidAuto
|
||||
rangePreference.summary = getString(R.string.chargeprice_battery_range, range[0], range[1])
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package net.vonforst.evmap.fragment.preference
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.RelativeSizeSpan
|
||||
import android.view.View
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.preference.CheckBoxPreference
|
||||
import androidx.preference.ListPreference
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.currencyDisplayName
|
||||
import net.vonforst.evmap.ui.MultiSelectDialogPreference
|
||||
import net.vonforst.evmap.viewmodel.SettingsViewModel
|
||||
import net.vonforst.evmap.viewmodel.viewModelFactory
|
||||
|
||||
|
||||
class ChargepriceSettingsFragment : BaseSettingsFragment() {
|
||||
override val isTopLevel = false
|
||||
|
||||
private val vm: SettingsViewModel by viewModels(factoryProducer = {
|
||||
viewModelFactory {
|
||||
SettingsViewModel(
|
||||
requireActivity().application,
|
||||
getString(R.string.chargeprice_key),
|
||||
getString(R.string.chargeprice_api_url)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
private lateinit var myVehiclePreference: MultiSelectDialogPreference
|
||||
private lateinit var myTariffsPreference: MultiSelectDialogPreference
|
||||
private lateinit var nativeIntegrationPreference: CheckBoxPreference
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
nativeIntegrationPreference = findPreference("chargeprice_native_integration")!!
|
||||
|
||||
myVehiclePreference = findPreference("chargeprice_my_vehicle")!!
|
||||
myVehiclePreference.isEnabled = false
|
||||
vm.vehicles.observe(viewLifecycleOwner) { res ->
|
||||
res.data?.let { cars ->
|
||||
val sortedCars = cars.sortedBy { it.brand }
|
||||
myVehiclePreference.entryValues = sortedCars.map { it.id }.toTypedArray()
|
||||
myVehiclePreference.entries = sortedCars.map {
|
||||
SpannableStringBuilder().apply {
|
||||
appendLine("${it.brand} ${it.name}")
|
||||
append(
|
||||
it.formatSpecs(),
|
||||
RelativeSizeSpan(0.86f),
|
||||
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
}.toTypedArray()
|
||||
myVehiclePreference.isEnabled = nativeIntegrationPreference.isChecked
|
||||
updateMyVehiclesSummary()
|
||||
}
|
||||
}
|
||||
|
||||
myTariffsPreference = findPreference("chargeprice_my_tariffs")!!
|
||||
myTariffsPreference.isEnabled = false
|
||||
vm.tariffs.observe(viewLifecycleOwner) { res ->
|
||||
res.data?.let { tariffs ->
|
||||
myTariffsPreference.entryValues = tariffs.map { it.id }.toTypedArray()
|
||||
myTariffsPreference.entries = tariffs.map {
|
||||
if (!it.name.lowercase().startsWith(it.provider.lowercase())) {
|
||||
"${it.provider} ${it.name}"
|
||||
} else {
|
||||
it.name
|
||||
}
|
||||
}.toTypedArray()
|
||||
myTariffsPreference.isEnabled = nativeIntegrationPreference.isChecked
|
||||
updateMyTariffsSummary()
|
||||
}
|
||||
}
|
||||
updateNativeIntegrationState()
|
||||
|
||||
val currencyPreference = findPreference<ListPreference>("chargeprice_currency")!!
|
||||
currencyPreference.entries = currencyPreference.entryValues.map {
|
||||
currencyDisplayName(it.toString()).replaceFirstChar { it.uppercase() }
|
||||
}.toTypedArray()
|
||||
}
|
||||
|
||||
private fun updateNativeIntegrationState() {
|
||||
for (i in 0 until preferenceScreen.preferenceCount) {
|
||||
val pref = preferenceScreen.getPreference(i)
|
||||
if (pref == nativeIntegrationPreference) {
|
||||
continue
|
||||
} else if (pref == myTariffsPreference) {
|
||||
pref.isEnabled =
|
||||
nativeIntegrationPreference.isChecked && vm.tariffs.value?.data != null
|
||||
} else if (pref == myVehiclePreference) {
|
||||
pref.isEnabled =
|
||||
nativeIntegrationPreference.isChecked && vm.tariffs.value?.data != null
|
||||
} else {
|
||||
pref.isEnabled = nativeIntegrationPreference.isChecked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMyTariffsSummary() {
|
||||
myTariffsPreference.summary =
|
||||
if (prefs.chargepriceMyTariffsAll) {
|
||||
getString(R.string.chargeprice_all_tariffs_selected)
|
||||
} else {
|
||||
val n = prefs.chargepriceMyTariffs?.size ?: 0
|
||||
requireContext().resources
|
||||
.getQuantityString(
|
||||
R.plurals.chargeprice_some_tariffs_selected,
|
||||
n,
|
||||
n
|
||||
) + "\n" + requireContext().resources
|
||||
.getQuantityString(R.plurals.pref_my_tariffs_summary, n)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMyVehiclesSummary() {
|
||||
vm.vehicles.value?.data?.let { cars ->
|
||||
val vehicles = cars.filter { it.id in prefs.chargepriceMyVehicles }
|
||||
val summary = vehicles.joinToString(", ") {
|
||||
"${it.brand} ${it.name}"
|
||||
}
|
||||
myVehiclePreference.summary = summary
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.settings_chargeprice, rootKey)
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
|
||||
when (key) {
|
||||
"chargeprice_my_vehicle" -> {
|
||||
updateMyVehiclesSummary()
|
||||
}
|
||||
|
||||
"chargeprice_my_tariffs" -> {
|
||||
updateMyTariffsSummary()
|
||||
}
|
||||
|
||||
"chargeprice_native_integration" -> {
|
||||
updateNativeIntegrationState()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package net.vonforst.evmap.fragment.preference
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.core.net.toUri
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@@ -24,7 +24,6 @@ import net.vonforst.evmap.viewmodel.viewModelFactory
|
||||
import okhttp3.OkHttpClient
|
||||
import okio.IOException
|
||||
import java.time.Instant
|
||||
import androidx.core.net.toUri
|
||||
|
||||
class DataSettingsFragment : BaseSettingsFragment() {
|
||||
override val isTopLevel = false
|
||||
@@ -33,8 +32,6 @@ class DataSettingsFragment : BaseSettingsFragment() {
|
||||
viewModelFactory {
|
||||
SettingsViewModel(
|
||||
requireActivity().application,
|
||||
getString(R.string.chargeprice_key),
|
||||
getString(R.string.chargeprice_api_url)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -152,87 +152,6 @@ class PreferenceDataSource(val context: Context) {
|
||||
sp.edit().putBoolean("update_0.6.0_androidauto_dialog_shown", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceNativeIntegration: Boolean
|
||||
get() = sp.getBoolean("chargeprice_native_integration", true)
|
||||
set(value) {
|
||||
sp.edit().putBoolean("chargeprice_native_integration", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceMyVehicles: Set<String>
|
||||
get() = try {
|
||||
sp.getStringSet("chargeprice_my_vehicle", emptySet())!!
|
||||
} catch (e: ClassCastException) {
|
||||
// backwards compatibility
|
||||
sp.getString("chargeprice_my_vehicle", null)?.let { setOf(it) } ?: emptySet()
|
||||
}
|
||||
set(value) {
|
||||
sp.edit().putStringSet("chargeprice_my_vehicle", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceLastSelectedVehicle: String?
|
||||
get() = sp.getString("chargeprice_last_vehicle", null)
|
||||
set(value) {
|
||||
sp.edit().putString("chargeprice_last_vehicle", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceMyTariffs: Set<String>?
|
||||
get() = sp.getStringSet("chargeprice_my_tariffs", null)
|
||||
set(value) {
|
||||
sp.edit().putStringSet("chargeprice_my_tariffs", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceMyTariffsAll: Boolean
|
||||
get() = sp.getBoolean("chargeprice_my_tariffs_all", true)
|
||||
set(value) {
|
||||
sp.edit().putBoolean("chargeprice_my_tariffs_all", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceNoBaseFee: Boolean
|
||||
get() = sp.getBoolean("chargeprice_no_base_fee", false)
|
||||
set(value) {
|
||||
sp.edit().putBoolean("chargeprice_no_base_fee", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceShowProviderCustomerTariffs: Boolean
|
||||
get() = sp.getBoolean("chargeprice_show_provider_customer_tariffs", false)
|
||||
set(value) {
|
||||
sp.edit().putBoolean("chargeprice_show_provider_customer_tariffs", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceAllowUnbalancedLoad: Boolean
|
||||
get() = sp.getBoolean("chargeprice_allow_unbalanced_load", false)
|
||||
set(value) {
|
||||
sp.edit().putBoolean("chargeprice_allow_unbalanced_load", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceCurrency: String
|
||||
get() = sp.getString("chargeprice_currency", null) ?: "EUR"
|
||||
set(value) {
|
||||
sp.edit().putString("chargeprice_currency", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceBatteryRange: List<Float>
|
||||
get() = listOf(
|
||||
sp.getFloat("chargeprice_battery_range_min", 20f),
|
||||
sp.getFloat("chargeprice_battery_range_max", 80f),
|
||||
)
|
||||
set(value) {
|
||||
sp.edit().putFloat("chargeprice_battery_range_min", value[0])
|
||||
.putFloat("chargeprice_battery_range_max", value[1])
|
||||
.apply()
|
||||
}
|
||||
|
||||
var chargepriceBatteryRangeAndroidAuto: List<Float>
|
||||
get() = listOf(
|
||||
sp.getFloat("chargeprice_battery_range_android_auto_min", 20f),
|
||||
sp.getFloat("chargeprice_battery_range_android_auto_max", 80f),
|
||||
)
|
||||
set(value) {
|
||||
sp.edit().putFloat("chargeprice_battery_range_android_auto_min", value[0])
|
||||
.putFloat("chargeprice_battery_range_android_auto_max", value[1])
|
||||
.apply()
|
||||
}
|
||||
|
||||
/** App start counter, introduced with Version 1.0.0 */
|
||||
var appStartCounter: Long
|
||||
get() = sp.getLong("app_start_counter", 0)
|
||||
|
||||
@@ -1,320 +0,0 @@
|
||||
package net.vonforst.evmap.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import jsonapi.Meta
|
||||
import jsonapi.Relationship
|
||||
import jsonapi.Relationships
|
||||
import jsonapi.ResourceIdentifier
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.chargeprice.ChargePrice
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceChargepointMeta
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceInclude
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceMeta
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceOptions
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceRequest
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceRequestTariffMeta
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceStation
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
class ChargepriceViewModel(
|
||||
application: Application,
|
||||
chargepriceApiKey: String,
|
||||
chargepriceApiUrl: String,
|
||||
private val state: SavedStateHandle
|
||||
) :
|
||||
AndroidViewModel(application) {
|
||||
private var api = ChargepriceApi.create(chargepriceApiKey, chargepriceApiUrl)
|
||||
private var prefs = PreferenceDataSource(application)
|
||||
|
||||
val charger: MutableLiveData<ChargeLocation> by lazy {
|
||||
state.getLiveData("charger")
|
||||
}
|
||||
|
||||
val chargepoint: MutableLiveData<Chargepoint?> by lazy {
|
||||
state.getLiveData("chargepoint")
|
||||
}
|
||||
|
||||
private val vehicleIds: MutableLiveData<Set<String>> by lazy {
|
||||
MutableLiveData<Set<String>>().apply {
|
||||
value = prefs.chargepriceMyVehicles
|
||||
}
|
||||
}
|
||||
|
||||
val vehicles: LiveData<Resource<List<ChargepriceCar>>> by lazy {
|
||||
MediatorLiveData<Resource<List<ChargepriceCar>>>().apply {
|
||||
addSource(vehicleIds.distinctUntilChanged()) { vehicleIds ->
|
||||
if (vehicleIds.isEmpty()) {
|
||||
value = Resource.success(emptyList())
|
||||
} else {
|
||||
value = Resource.loading(null)
|
||||
viewModelScope.launch {
|
||||
value = try {
|
||||
val result = api.getVehicles()
|
||||
Resource.success(result.filter {
|
||||
it.id in vehicleIds
|
||||
})
|
||||
} catch (e: IOException) {
|
||||
Resource.error(e.message, null)
|
||||
} catch (e: HttpException) {
|
||||
Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
observeForever {
|
||||
vehicle.value = it.data?.firstOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val vehicle: MutableLiveData<ChargepriceCar> by lazy {
|
||||
state.getLiveData("vehicle")
|
||||
}
|
||||
|
||||
val vehicleCompatibleConnectors: LiveData<List<String>> by lazy {
|
||||
MediatorLiveData<List<String>>().apply {
|
||||
addSource(vehicle) {
|
||||
value = it?.compatibleEvmapConnectors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val noCompatibleConnectors: LiveData<Boolean> by lazy {
|
||||
MediatorLiveData<Boolean>().apply {
|
||||
value = false
|
||||
listOf(charger, vehicleCompatibleConnectors).forEach {
|
||||
addSource(it) {
|
||||
val charger = charger.value ?: return@addSource
|
||||
val connectors = vehicleCompatibleConnectors.value ?: return@addSource
|
||||
value = !charger.chargepoints.flatMap { equivalentPlugTypes(it.type) }
|
||||
.any { it in connectors }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val batteryRange: MutableLiveData<List<Float>> by lazy {
|
||||
MutableLiveData<List<Float>>().apply {
|
||||
value = prefs.chargepriceBatteryRange
|
||||
observeForever {
|
||||
if (it[0] == it[1]) {
|
||||
value = if (it[0] < 1.0) {
|
||||
listOf(it[0], it[1] + 1)
|
||||
} else {
|
||||
listOf(it[0] - 1, it[1])
|
||||
}
|
||||
}
|
||||
prefs.chargepriceBatteryRange = value!!
|
||||
}
|
||||
}
|
||||
}
|
||||
val batteryRangeSliderDragging: MutableLiveData<Boolean> by lazy {
|
||||
MutableLiveData<Boolean>().apply {
|
||||
value = false
|
||||
}
|
||||
}
|
||||
|
||||
val chargePrices: MutableLiveData<Resource<List<ChargePrice>>> by lazy {
|
||||
MediatorLiveData<Resource<List<ChargePrice>>>().apply {
|
||||
value = state["chargePrices"] ?: Resource.loading(null)
|
||||
listOf(
|
||||
vehicle,
|
||||
batteryRange,
|
||||
batteryRangeSliderDragging,
|
||||
vehicleCompatibleConnectors,
|
||||
myTariffs, myTariffsAll, charger
|
||||
).forEach {
|
||||
addSource(it.distinctUntilChanged()) {
|
||||
if (!batteryRangeSliderDragging.value!!) {
|
||||
loadPrices()
|
||||
state["chargePrices"] = this.value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val chargePriceMeta: MutableLiveData<Resource<ChargepriceMeta>> by lazy {
|
||||
MutableLiveData<Resource<ChargepriceMeta>>().apply {
|
||||
value = Resource.loading(null)
|
||||
}
|
||||
}
|
||||
|
||||
val chargePricesForChargepoint: MediatorLiveData<Resource<List<ChargePrice>>> by lazy {
|
||||
MediatorLiveData<Resource<List<ChargePrice>>>().apply {
|
||||
listOf(chargePrices, chargepoint).forEach {
|
||||
addSource(it) {
|
||||
val cps = chargePrices.value
|
||||
val chargepoint = chargepoint.value
|
||||
if (cps == null || chargepoint == null) {
|
||||
value = null
|
||||
} else if (cps.status == Status.ERROR) {
|
||||
value = Resource.error(cps.message, null)
|
||||
} else if (cps.status == Status.LOADING) {
|
||||
value = Resource.loading(null)
|
||||
} else {
|
||||
val myTariffs = prefs.chargepriceMyTariffs
|
||||
value = Resource.success(cps.data!!.mapNotNull { cp ->
|
||||
val filteredPrices =
|
||||
cp.chargepointPrices.filter {
|
||||
it.plug == getChargepricePlugType(chargepoint) && it.power == chargepoint.power
|
||||
}
|
||||
if (filteredPrices.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
cp.copy(
|
||||
chargepointPrices = filteredPrices
|
||||
)
|
||||
}
|
||||
}
|
||||
.sortedBy { it.chargepointPrices.first().price ?: Double.MAX_VALUE }
|
||||
.sortedByDescending {
|
||||
prefs.chargepriceMyTariffsAll ||
|
||||
myTariffs != null && it.tariffId in myTariffs
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reloadPrefs() {
|
||||
vehicleIds.value = prefs.chargepriceMyVehicles
|
||||
}
|
||||
|
||||
private fun getChargepricePlugType(chargepoint: Chargepoint): String {
|
||||
val index = charger.value!!.chargepointsMerged.indexOf(chargepoint)
|
||||
val type = charger.value!!.chargepriceData!!.plugTypes?.get(index) ?: chargepoint.type
|
||||
return type
|
||||
}
|
||||
|
||||
val myTariffs: LiveData<Set<String>> by lazy {
|
||||
MutableLiveData<Set<String>>().apply {
|
||||
value = prefs.chargepriceMyTariffs
|
||||
}
|
||||
}
|
||||
val myTariffsAll: LiveData<Boolean> by lazy {
|
||||
MutableLiveData<Boolean>().apply {
|
||||
value = prefs.chargepriceMyTariffsAll
|
||||
}
|
||||
}
|
||||
|
||||
val chargepriceMetaForChargepoint: MediatorLiveData<Resource<ChargepriceChargepointMeta>> by lazy {
|
||||
MediatorLiveData<Resource<ChargepriceChargepointMeta>>().apply {
|
||||
listOf(chargePriceMeta, chargepoint).forEach {
|
||||
addSource(it) {
|
||||
val cpMeta = chargePriceMeta.value
|
||||
val chargepoint = chargepoint.value
|
||||
if (cpMeta == null || chargepoint == null) {
|
||||
value = null
|
||||
} else if (cpMeta.status == Status.ERROR) {
|
||||
value = Resource.error(cpMeta.message, null)
|
||||
} else if (cpMeta.status == Status.LOADING) {
|
||||
value = Resource.loading(null)
|
||||
} else {
|
||||
val result = cpMeta.data!!.chargePoints.filter {
|
||||
it.plug == getChargepricePlugType(
|
||||
chargepoint
|
||||
) && it.power == chargepoint.power
|
||||
}.elementAtOrNull(0)
|
||||
value = if (result != null) {
|
||||
Resource.success(result)
|
||||
} else {
|
||||
Resource.error("matching chargepoint not found", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var loadPricesJob: Job? = null
|
||||
fun loadPrices() {
|
||||
chargePrices.value = Resource.loading(null)
|
||||
chargePriceMeta.value = Resource.loading(null)
|
||||
val charger = charger.value
|
||||
val car = vehicle.value
|
||||
val compatibleConnectors = vehicleCompatibleConnectors.value
|
||||
val myTariffs = myTariffs.value
|
||||
val myTariffsAll = myTariffsAll.value
|
||||
if (charger == null || car == null || compatibleConnectors == null || myTariffsAll == null || myTariffsAll == false && myTariffs == null) {
|
||||
chargePrices.value = Resource.error(null, null)
|
||||
return
|
||||
}
|
||||
|
||||
val cpStation = ChargepriceStation.fromEvmap(charger, compatibleConnectors)
|
||||
if (cpStation.chargePoints.isEmpty()) {
|
||||
// no compatible connectors
|
||||
chargePrices.value = Resource.success(emptyList())
|
||||
chargePriceMeta.value = Resource.success(ChargepriceMeta(emptyList()))
|
||||
return
|
||||
}
|
||||
|
||||
loadPricesJob?.cancel()
|
||||
loadPricesJob = viewModelScope.launch {
|
||||
try {
|
||||
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,
|
||||
showPriceUnavailable = true
|
||||
),
|
||||
relationships = if (!myTariffsAll) {
|
||||
Relationships(
|
||||
"tariffs" to Relationship.ToMany(
|
||||
(myTariffs ?: emptySet()).map {
|
||||
ResourceIdentifier(
|
||||
"tariff",
|
||||
id = it
|
||||
)
|
||||
},
|
||||
meta = Meta.from(
|
||||
ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS),
|
||||
ChargepriceApi.moshi
|
||||
)
|
||||
)
|
||||
)
|
||||
} 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)
|
||||
chargePriceMeta.value = Resource.error(e.message, null)
|
||||
} catch (e: HttpException) {
|
||||
chargePrices.value = Resource.error(e.message, null)
|
||||
chargePriceMeta.value = Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetBatteryRangeToDefault() {
|
||||
batteryRange.value = prefs.chargepriceBatteryRangeAndroidAuto
|
||||
}
|
||||
}
|
||||
@@ -6,38 +6,16 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceTariff
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
class SettingsViewModel(
|
||||
application: Application,
|
||||
chargepriceApiKey: String,
|
||||
chargepriceApiUrl: String
|
||||
) :
|
||||
AndroidViewModel(application) {
|
||||
private val api = ChargepriceApi.create(chargepriceApiKey, chargepriceApiUrl)
|
||||
private val db = AppDatabase.getInstance(application)
|
||||
private val prefs = PreferenceDataSource(application)
|
||||
|
||||
val vehicles: MutableLiveData<Resource<List<ChargepriceCar>>> by lazy {
|
||||
MutableLiveData<Resource<List<ChargepriceCar>>>().apply {
|
||||
value = Resource.loading(null)
|
||||
loadVehicles()
|
||||
}
|
||||
}
|
||||
|
||||
val tariffs: MutableLiveData<Resource<List<ChargepriceTariff>>> by lazy {
|
||||
MutableLiveData<Resource<List<ChargepriceTariff>>>().apply {
|
||||
value = Resource.loading(null)
|
||||
loadTariffs()
|
||||
}
|
||||
}
|
||||
|
||||
val chargerCacheCount: LiveData<Long> by lazy {
|
||||
db.chargeLocationsDao().getCount()
|
||||
}
|
||||
@@ -52,32 +30,6 @@ class SettingsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadVehicles() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val result = api.getVehicles()
|
||||
vehicles.value = Resource.success(result)
|
||||
} catch (e: IOException) {
|
||||
vehicles.value = Resource.error(e.message, null)
|
||||
} catch (e: HttpException) {
|
||||
vehicles.value = Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadTariffs() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val result = api.getTariffs()
|
||||
tariffs.value = Resource.success(result)
|
||||
} catch (e: IOException) {
|
||||
tariffs.value = Resource.error(e.message, null)
|
||||
} catch (e: HttpException) {
|
||||
tariffs.value = Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteRecentSearchResults() {
|
||||
viewModelScope.launch {
|
||||
db.recentAutocompletePlaceDao().deleteAll()
|
||||
|
||||
@@ -543,7 +543,6 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/go_to_chargeprice"
|
||||
android:transitionName="@string/shared_element_chargeprice"
|
||||
app:goneUnless="@{charger.data != null && ChargepriceApi.isChargerSupported(charger.data)}"
|
||||
app:icon="@drawable/ic_chargeprice" />
|
||||
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
<?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.ChargepriceViewModel" />
|
||||
<import type="net.vonforst.evmap.viewmodel.Status" />
|
||||
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
|
||||
|
||||
<variable
|
||||
name="vm"
|
||||
type="ChargepriceViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/linearLayout5"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:transitionName="@string/shared_element_chargeprice">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/toolbar_container"
|
||||
android:layout_width="match_parent"
|
||||
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="?android:actionBarSize">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imgChargepriceLogo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/powered_by_chargeprice"
|
||||
android:focusable="true"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:layout_gravity="right"
|
||||
app:srcCompat="@drawable/ic_powered_by_chargeprice"
|
||||
app:tint="?android:textColorPrimary"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/charge_prices_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar_container"
|
||||
tools:itemCount="1"
|
||||
tools:listitem="@layout/fragment_chargeprice_preview" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar5"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="280dp"
|
||||
app:goneUnless="@{vm.chargePricesForChargepoint.status == Status.LOADING || vm.vehicles.status == Status.LOADING}"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/charge_prices_list"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
|
||||
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView8"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="280dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/chargeprice_no_tariffs_found"
|
||||
app:goneUnless="@{vm.chargePricesForChargepoint.status == Status.SUCCESS && vm.chargePricesForChargepoint.data.size() == 0}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
|
||||
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView9"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="280dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/chargeprice_no_compatible_connectors"
|
||||
app:goneUnless="@{vm.noCompatibleConnectors}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
|
||||
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="280dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/chargeprice_select_car_first"
|
||||
app:goneUnless="@{vm.vehicles.status == Status.SUCCESS && vm.vehicles.data.size() == 0}"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btnSettings"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
|
||||
app:layout_constraintTop_toTopOf="@+id/charge_prices_list"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnSettings"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/settings"
|
||||
app:goneUnless="@{vm.vehicles.status == Status.SUCCESS && vm.vehicles.data.size() == 0}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView3" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
@@ -1,122 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.ChargepriceViewModel" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.Status" />
|
||||
|
||||
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
|
||||
|
||||
<variable
|
||||
name="vm"
|
||||
type="ChargepriceViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/chargeprice_select_connector"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:textColor="?colorPrimary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/vehicle_selection" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/connectors_list"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
app:data="@{vm.charger.chargepointsMerged}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView"
|
||||
tools:itemCount="3"
|
||||
tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_connector_button"
|
||||
tools:orientation="horizontal" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvChargeFromTo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:background="?selectableItemBackground"
|
||||
android:text="@{String.format(@string/chargeprice_battery_range, vm.batteryRange[0], vm.batteryRange[1])}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:textColor="?colorPrimary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/connectors_list"
|
||||
tools:text="Charge from 20% to 80%" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView4"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{@string/chargeprice_stats(vm.chargepriceMetaForChargepoint.data.energy, BindingAdaptersKt.time((int) Math.round(vm.chargepriceMetaForChargepoint.data.duration)), vm.chargepriceMetaForChargepoint.data.energy / vm.chargepriceMetaForChargepoint.data.duration * 60)}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
app:invisibleUnlessAnimated="@{!vm.batteryRangeSliderDragging && vm.chargepriceMetaForChargepoint.status == Status.SUCCESS}"
|
||||
app:layout_constraintStart_toStartOf="@+id/tvChargeFromTo"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvChargeFromTo"
|
||||
tools:text="(18 kWh, approx. 23 min, ⌀ 50 kW)" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvVehicleHeader"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/chargeprice_vehicle"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:textColor="?colorPrimary"
|
||||
app:goneUnless="@{vm.vehicles.data != null && vm.vehicles.data.size() > 1}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/vehicle_selection"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvVehicleHeader"
|
||||
app:data="@{vm.vehicles.data}"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:goneUnless="@{vm.vehicles.data != null && vm.vehicles.data.size() > 1}"
|
||||
android:orientation="horizontal"
|
||||
tools:listitem="@layout/item_chargeprice_vehicle_chip" />
|
||||
|
||||
<com.google.android.material.slider.RangeSlider
|
||||
android:id="@+id/battery_range"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:valueFrom="0.0"
|
||||
android:valueTo="100.0"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView4"
|
||||
app:values="@={vm.batteryRange}" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
@@ -1,18 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/fragment_chargeprice_header" />
|
||||
|
||||
<include layout="@layout/item_chargeprice" />
|
||||
|
||||
<include layout="@layout/item_chargeprice" />
|
||||
|
||||
<include layout="@layout/item_chargeprice" />
|
||||
|
||||
<include layout="@layout/item_chargeprice" />
|
||||
|
||||
<include layout="@layout/item_chargeprice" />
|
||||
</LinearLayout>
|
||||
@@ -1,173 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.api.chargeprice.ChargePrice" />
|
||||
|
||||
<import type="net.vonforst.evmap.api.chargeprice.ChargepriceChargepointMeta" />
|
||||
|
||||
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
|
||||
|
||||
<import type="java.util.Set" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="ChargePrice" />
|
||||
|
||||
<variable
|
||||
name="meta"
|
||||
type="ChargepriceChargepointMeta" />
|
||||
|
||||
<variable
|
||||
name="myTariffs"
|
||||
type="Set<String>" />
|
||||
|
||||
<variable
|
||||
name="myTariffsAll"
|
||||
type="Boolean" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:background="@{BindingAdaptersKt.tariffBackground(context,!myTariffsAll && myTariffs.contains(item.tariffId), item.branding.backgroundColor)}">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtTariff"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{item.tariffName}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
||||
app:layout_constraintBottom_toTopOf="@+id/txtProvider"
|
||||
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="CheapCharge" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtProvider"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{item.provider}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
app:goneUnless="@{!item.tariffName.toLowerCase().startsWith(item.provider.toLowerCase())}"
|
||||
app:layout_constraintBottom_toTopOf="@+id/rvTags"
|
||||
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
|
||||
app:layout_constraintStart_toStartOf="@+id/txtTariff"
|
||||
app:layout_constraintTop_toBottomOf="@+id/txtTariff"
|
||||
tools:text="Cheap Charging Co." />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvTags"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:nestedScrollingEnabled="false"
|
||||
app:data="@{item.tags}"
|
||||
app:layout_constraintBottom_toTopOf="@+id/txtProviderCustomerTariff"
|
||||
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
|
||||
app:layout_constraintStart_toStartOf="@+id/txtTariff"
|
||||
app:layout_constraintTop_toBottomOf="@+id/txtProvider"
|
||||
tools:itemCount="1"
|
||||
tools:listitem="@layout/item_chargeprice_tag" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtProviderCustomerTariff"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/chargeprice_provider_customer_tariff"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
app:goneUnless="@{item.providerCustomerTariff}"
|
||||
app:layout_constraintBottom_toTopOf="@id/txtMonthlyFee"
|
||||
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="@+id/txtTariff"
|
||||
app:layout_constraintTop_toBottomOf="@+id/rvTags" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtMonthlyFee"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{item.formatMonthlyFees(context)}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
app:goneUnless="@{item.totalMonthlyFee > 0 || item.monthlyMinSales > 0}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
|
||||
app:layout_constraintStart_toStartOf="@+id/txtTariff"
|
||||
app:layout_constraintTop_toBottomOf="@+id/txtProviderCustomerTariff"
|
||||
tools:text="Base fee 1 €/month" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtPrice"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:gravity="end"
|
||||
android:text="@{item.chargepointPrices.get(0).price != null ? String.format(@string/charge_price_format, item.chargepointPrices.get(0).price, BindingAdaptersKt.currency(item.currency)) : @string/chargeprice_price_not_available}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
app:layout_constraintBottom_toTopOf="@+id/txtAveragePrice"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline5"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="1,50 €" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtAveragePrice"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:gravity="end"
|
||||
android:text="@{item.chargepointPrices.get(0).price != null ? String.format(item.chargepointPrices.get(0).priceDistribution.isOnlyKwh ? @string/charge_price_kwh_format : @string/charge_price_average_format, item.chargepointPrices.get(0).price / meta.energy, BindingAdaptersKt.currency(item.currency)) : item.chargepointPrices.get(0).noPriceReason}"
|
||||
app:goneUnless="@{item.chargepointPrices.get(0).price > 0 || item.chargepointPrices.get(0).price == null}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
app:layout_constraintBottom_toTopOf="@id/txtPriceDetails"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline5"
|
||||
app:layout_constraintTop_toBottomOf="@+id/txtPrice"
|
||||
tools:text="⌀ 0,29 €/kWh" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtPriceDetails"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:gravity="end"
|
||||
android:text="@{item.chargepointPrices.get(0).formatDistribution(context)}"
|
||||
app:goneUnless="@{!item.chargepointPrices.get(0).formatDistribution(context).empty}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline5"
|
||||
app:layout_constraintTop_toBottomOf="@+id/txtAveragePrice"
|
||||
tools:text="pro kWh + ab 4h Blockiergeb." />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline5"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="0.65" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivLogo"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_margin="8dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:invisibleUnless="@{item.branding.logoUrl != null}"
|
||||
app:imageUrl="@{item.branding.logoUrl}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline5"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:srcCompat="@tools:sample/avatars" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
@@ -1,40 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.api.chargeprice.ChargepriceTag" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="ChargepriceTag" />
|
||||
</data>
|
||||
|
||||
<net.vonforst.evmap.ui.BalancedBreakingTextView
|
||||
android:id="@+id/rvTags"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:background="@drawable/rounded_rect_16dp"
|
||||
android:maxLines="3"
|
||||
android:text="@{item.text}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:theme="@style/ThemeOverlay.Material3.Dark"
|
||||
android:gravity="center_vertical"
|
||||
android:drawablePadding="4dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingStart="3dp"
|
||||
android:paddingTop="2dp"
|
||||
android:paddingBottom="2dp"
|
||||
android:drawableTint="?android:textColorPrimary"
|
||||
android:breakStrategy="balanced"
|
||||
app:chargepriceTagColor="@{item.kind}"
|
||||
app:chargepriceTagIcon="@{item.kind}"
|
||||
tools:backgroundTint="@color/chargeprice_alert"
|
||||
tools:drawableLeft="@drawable/ic_chargeprice_alert"
|
||||
tools:text="Only for drivers of blue cars" />
|
||||
</layout>
|
||||
@@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.api.chargeprice.ChargepriceCar" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="ChargepriceCar" />
|
||||
|
||||
<variable
|
||||
name="selectedItem"
|
||||
type="ChargepriceCar" />
|
||||
</data>
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
style="@style/Widget.Material3.Chip.Filter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="4dp"
|
||||
android:layout_marginRight="4dp"
|
||||
android:text="@{item.brand + ' ' + item.name}"
|
||||
android:checked="@{item == selectedItem}"
|
||||
tools:text="Tesla Model 2" />
|
||||
</layout>
|
||||
@@ -27,9 +27,6 @@
|
||||
app:enterAnim="@animator/nav_default_enter_anim"
|
||||
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_map_to_chargepriceFragment"
|
||||
app:destination="@id/chargeprice" />
|
||||
<action
|
||||
android:id="@+id/action_map_to_opensource_donations"
|
||||
app:destination="@id/opensource_donations" />
|
||||
@@ -88,16 +85,6 @@
|
||||
app:argType="boolean"
|
||||
android:defaultValue="false" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/settings_chargeprice"
|
||||
android:name="net.vonforst.evmap.fragment.preference.ChargepriceSettingsFragment"
|
||||
android:label="@string/settings_chargeprice"
|
||||
tools:layout="@layout/fragment_preference" />
|
||||
<fragment
|
||||
android:id="@+id/settings_android_auto"
|
||||
android:name="net.vonforst.evmap.fragment.preference.AndroidAutoSettingsFragment"
|
||||
android:label="@string/settings_android_auto"
|
||||
tools:layout="@layout/fragment_preference" />
|
||||
<fragment
|
||||
android:id="@+id/settings_developer"
|
||||
android:name="net.vonforst.evmap.fragment.preference.DeveloperSettingsFragment"
|
||||
@@ -126,25 +113,6 @@
|
||||
android:name="net.vonforst.evmap.fragment.FilterProfilesFragment"
|
||||
android:label="@string/menu_manage_filter_profiles"
|
||||
tools:layout="@layout/fragment_filter_profiles" />
|
||||
<fragment
|
||||
android:id="@+id/chargeprice"
|
||||
android:name="net.vonforst.evmap.fragment.ChargepriceFragment"
|
||||
android:label="@string/chargeprice_title"
|
||||
tools:layout="@layout/fragment_chargeprice">
|
||||
<action
|
||||
android:id="@+id/action_chargeprice_to_chargepriceSettingsFragment"
|
||||
app:destination="@id/settings_chargeprice"
|
||||
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" />
|
||||
<argument
|
||||
android:name="charger"
|
||||
app:argType="net.vonforst.evmap.model.ChargeLocation" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/donate"
|
||||
android:name="net.vonforst.evmap.fragment.DonateFragment"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="shared_element_picture">picture</string>
|
||||
<string name="shared_element_chargeprice">chargeprice</string>
|
||||
<string name="github_link">https://github.com/ev-map/EVMap</string>
|
||||
<string name="twitter_handle">\@ev_map</string>
|
||||
<string name="twitter_url">https://twitter.com/ev_map</string>
|
||||
@@ -10,7 +9,6 @@
|
||||
<string name="goingelectric_forum_url"><![CDATA[https://www.goingelectric.de/forum/viewtopic.php?f=5&t=56342]]></string>
|
||||
<string name="tff_forum_url"><![CDATA[https://tff-forum.de/t/283834]]></string>
|
||||
<string name="github_sponsors_link">https://github.com/sponsors/johan12345/</string>
|
||||
<string name="chargeprice_api_url">https://api.chargeprice.app/v1/</string>
|
||||
<string name="fronyx_url">https://fronyx.io/</string>
|
||||
<string name="website_url">https://ev-map.app</string>
|
||||
<string name="about_contributors_list">
|
||||
|
||||
@@ -9,14 +9,6 @@
|
||||
android:fragment="net.vonforst.evmap.fragment.preference.DataSettingsFragment"
|
||||
android:title="@string/settings_data_sources"
|
||||
android:icon="@drawable/ic_settings_data_source" />
|
||||
<Preference
|
||||
android:fragment="net.vonforst.evmap.fragment.preference.ChargepriceSettingsFragment"
|
||||
android:title="@string/settings_chargeprice"
|
||||
android:icon="@drawable/ic_chargeprice" />
|
||||
<Preference
|
||||
android:fragment="net.vonforst.evmap.fragment.preference.AndroidAutoSettingsFragment"
|
||||
android:title="@string/settings_android_auto"
|
||||
android:icon="@drawable/ic_android_auto" />
|
||||
<Preference
|
||||
android:key="developer_options"
|
||||
android:fragment="net.vonforst.evmap.fragment.preference.DeveloperSettingsFragment"
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<net.vonforst.evmap.ui.RangeSliderPreference
|
||||
android:key="chargeprice_battery_range_android_auto"
|
||||
android:title="@string/settings_android_auto_chargeprice_range"
|
||||
android:valueFrom="0.0"
|
||||
android:valueTo="100.0"
|
||||
app:updatesContinuously="true"
|
||||
android:defaultValue="20.0,80.0"
|
||||
android:layout="@layout/preference_widget_rangeslider"
|
||||
tools:summary="@string/chargeprice_battery_range" />
|
||||
</PreferenceScreen>
|
||||
@@ -1,41 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<CheckBoxPreference
|
||||
android:key="chargeprice_native_integration"
|
||||
android:title="@string/pref_chargeprice_native_integration"
|
||||
android:summaryOn="@string/pref_chargeprice_native_integration_on"
|
||||
android:summaryOff="@string/pref_chargeprice_native_integration_off"
|
||||
app:defaultValue="true" />
|
||||
<net.vonforst.evmap.ui.MultiSelectDialogPreference
|
||||
android:key="chargeprice_my_vehicle"
|
||||
android:title="@string/pref_my_vehicle"
|
||||
app:showAllButton="false"
|
||||
app:defaultToAll="false" />
|
||||
<net.vonforst.evmap.ui.MultiSelectDialogPreference
|
||||
android:key="chargeprice_my_tariffs"
|
||||
android:title="@string/pref_my_tariffs" />
|
||||
<ListPreference
|
||||
android:key="chargeprice_currency"
|
||||
android:title="@string/pref_chargeprice_currency"
|
||||
android:entryValues="@array/pref_chargeprice_currencies"
|
||||
android:defaultValue="EUR"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<CheckBoxPreference
|
||||
android:key="chargeprice_no_base_fee"
|
||||
android:title="@string/pref_chargeprice_no_base_fee"
|
||||
android:defaultValue="false"
|
||||
app:singleLineTitle="false" />
|
||||
<CheckBoxPreference
|
||||
android:key="chargeprice_show_provider_customer_tariffs"
|
||||
android:title="@string/pref_chargeprice_show_provider_customer_tariffs"
|
||||
android:summary="@string/pref_chargeprice_show_provider_customer_tariffs_summary"
|
||||
android:defaultValue="false"
|
||||
app:singleLineTitle="false" />
|
||||
<CheckBoxPreference
|
||||
android:key="chargeprice_allow_unbalanced_load"
|
||||
android:title="@string/pref_chargeprice_allow_unbalanced_load"
|
||||
android:summary="@string/pref_chargeprice_allow_unbalanced_load_summary"
|
||||
android:defaultValue="false"
|
||||
app:singleLineTitle="false" />
|
||||
</PreferenceScreen>
|
||||
Reference in New Issue
Block a user