diff --git a/.travis.yml b/.travis.yml index e4a2a017..4057a40d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ env: - secure: KYdFlMarsyXw+OHht1Atp+Kirbw9O09Ck14EjFuKb1eNtknurZ/tGEXuD+8xWh1W8W21kgHEG7s3rzru53t29buz+FW9f+ZmhEWXFP3OydyvXLw4BAVVOjm6xG2uHX/8MOGLJNM7cfaF25EPQ+kznHe84R29KaLH90mNRr2lPa4VnfbcnvDStiVaez/vJ72UoYSP5HICAzoF70yC3ZvvCK1hZv71UIysCbFE2IkxvMhG9OOGebdnRmFssaRCrvfRLjitobcLzkPWzZZIqdjNASf8/iAxX8VgGBYfVj8ID06AfMrtgXNJRCvcD0LICraQ+WPUbikMunRieGO8PNHSB5vKdPoC50aLUa0RoRb4G3QM1pR2A8xAFlIJFX2R7iY+2t24L9hRFqB98+QoQzutfkAI1T0rzem/wtpZpuan+bDawDJEHGCeYbE0aPDAl6lytgrEE9fRgV3c1jJLQzu0xIWG8YLl3iMg0hL+c0wCKXoeqrfCFS6kYmmG7W+rQp4tCZifvRbWfAXwIPQieffKxqdEuUwiUsYxdzCu9v9uU3nflEOLLuRgeMP3gV8mpur9b5GztpkfgfzcAqsF+NiY01kYgGtrgCYlMy0TxASE+UuALrtkQtU01wwhs9RH7Az0Ib3C+MT5DTjxHQCYETIViocmNEG2vfAbgHazCpGAhcY= - secure: Hoko50vP+Mwm/O4CWvPvjMxd1gGhi+Bultjyy1WpludSmZFCfKz45Mj1EqzeYk6MeMLvGOEkSLB5wjdXdgJ9j5gEOF5K34k4vESJA7+DqDO3I7Xw9cnWgOXdFqB0qGHar0TVP3Dfg7ZcRgtmeX6t2uoELFLiS9GvTnbZXk4PCUybd979Xi8XHjEQV7+3EZbSjtsI4GFeIK1rrjd0I+UM88zYrWnz1KhdCjWvQb4iZjo+ib6NmGGEMqR7jJPRZz3KB01Y+n5h21qq9/Nv31zQIqt2B5nRxy3vBvvqKapgprIk+hVOpNnBU8w89uWUU6tYUeFk0t7z1TYWjgaBrMmGCM+aKkQ2q5F/ygNzDwB+KkpJx709Yhf0ZX8g2yvkdz+Ok7moYuvmrOPOf4E/U3BlfZxbGtRD2bGYbDgHLFYlTn6v5J2kJHDJAz31yvF5jvJejDaPp2IBVfoMRy7ZJFmGUNHGd9Se6bRwxS++AoobP5WDrBiUXNe2KKDMs3e7vbaO+hLbZ9XHpjeWIJGUfvtTee8EHZqF/8A3ju53V4/R0ehlOv2UZbpYNqcmwrsy9/R4pgMfDkG3Q054LmYrxD8DIC9b8excVMwWRP5aQ1TqZnxO2B1/vJU87RcnGl3jekeHTdHXbRq9BMV4dAdPfB9X3nGIi2GgV0iTBk+24xccOc0= - secure: RsN/2nBVv0byMzwchuAhDti1AWKECg3Uzqk6Ahgaqg02zx8GHj60j0qNax7KuMUg70X3G4b3m8ZzndAU0wcJ98UyIku7ofUYgXHm0XYKTJwiyyhrKJ8pZ4qoeCoRkitIGIitlb84fSufVamcoLyLNbLUsO5OzL+Uscyhq/BwAhyOhj5FB9DM+GE9ntanQ4m3k4guMyMctR9CGS6Tk4LKJ1WCm0AnjTPalc+we+7yORY2J/9d6VHLKfaFXbpuWmrdnFfDZqxqcUODsxPVE0LuUtXyKQDTjjfQc8106Z/z19AJS+oLm/2UND84PD3MqsjX6EX0/9k3fY0OsoCuQAivDRmtevQ0bDQrTAyeBLcfnPCw/MYiJWyBcaYBAYK42EAfsFTDBRIduFB/Dpvg+8GuLZSdm4xVYpTlQ2pMtlGNWIGIRQsjk9LZ8swq8QBMiiF/bpMGKdfmnyQj8jkEOCWaAzkR9O+4E4Y7PuBENef9XuV0hLMryCrML2YXigxAKkEUOPhfnG+AbEY+g2kAMp+2EtXaG3tsGxoMhEYA+uRd3o2cacQMMwRpnePH5DYg6F05mrvdHPpt+P9UR1iHaHmjBPAYeksmrdP8bS088zcnePgiL6+6N0m3+l8Krihmxg5RyWjnH18IwX4RO+xg4x3cW+zaScCDbbotDMEYtChF+Hs= + - secure: LQHMdhaPUlCuJPFrCPpUphJSY6xzAFI/7RrcAVLtLcPhGdS+MeNifIkkAH7MeitTHroOC0dGkZ4bg/8/7bKfgwY4vPH9P50kZcnX5mI6zfBHgNYJzuthj+vJH9RAtkdQOW9Fe1uPIx8R9GUWUOVnkoJh0PQ1gDXdZW5fePqUtn1kYrcCCBE+Bhe3wz6QzTBqGS1nsVRTxQfSJNGi9uH1oi9kQGgQFuCCiJ/P0A6MIhSItkOfuggx/iorA+iASbhWkB4nXYQBbFe/ZhFJWbVfgYlOM0HtpKh8B2AqKw21Em32JoovCbUof4adkY7cH8/4Rt9SujC9YOw+a6oM+e//jJT0sie77V7zl670j+qODTuNvV4qVUwtoxShyc1Sfbd+Xb0xn/OC7DzBg97YuYCF/84yyuq12rl/cofynWE1L5YvGNSJk241XUw98Bvl0MK4VIfQvG9zJP0HnQZcWKt6kFOIEJSCRbmkd2tPPAZFBXBQf/bvpULOoKwneGJZBSapRoCyGwemM+EAzVB9UOXAqsXZ4FHkt1SSJVrTVwgxvXpCfmF6LZPhbz6nvouRWGsC/GdWjrHtdW5lEOvS27qKEL5rXwQ0o+71ZICGo8j4E0GOHXyi857qZhvO7cbOnts+iiawXiWzPXv2gGGabuqPwcU8JPEoWdaiIaeGUczfjBU= - ANDROID_HOME=$HOME/android-sdk before_install: - openssl aes-256-cbc -K $encrypted_53968681344a_key -iv $encrypted_53968681344a_iv -in _ci/keystore.jks.enc -out _ci/keystore.jks -d diff --git a/_img/powered_by_chargeprice.svg b/_img/powered_by_chargeprice.svg new file mode 100644 index 00000000..d97f8e43 --- /dev/null +++ b/_img/powered_by_chargeprice.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/build.gradle b/app/build.gradle index 045b3054..59dc70b6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -85,6 +85,10 @@ android { if (mapboxKey != null) { variant.resValue "string", "mapbox_key", mapboxKey } + def chargepriceKey = env.CHARGEPRICE_API_KEY ?: project.findProperty("CHARGEPRICE_API_KEY") + if (chargepriceKey != null) { + variant.resValue "string", "chargeprice_key", chargepriceKey + } } } @@ -108,6 +112,8 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:4.9.0' implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.9.0' implementation 'com.squareup.moshi:moshi-kotlin:1.9.2' + implementation 'moe.banana:moshi-jsonapi:3.5.0' + implementation 'moe.banana:moshi-jsonapi-retrofit-converter:3.5.0' implementation 'io.coil-kt:coil:1.1.0' implementation 'com.github.MikeOrtiz:TouchImageView:3.0.3' implementation "com.mikepenz:aboutlibraries-core:$about_libs_version" diff --git a/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt b/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt index 702c7db5..f52334de 100644 --- a/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt +++ b/app/src/main/java/net/vonforst/evmap/adapter/DataBindingAdapters.kt @@ -1,16 +1,24 @@ 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 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.ChargepriceChargepointMeta +import net.vonforst.evmap.api.chargeprice.ChargepriceTag import net.vonforst.evmap.api.goingelectric.Chargepoint +import net.vonforst.evmap.databinding.ItemChargepriceBinding +import net.vonforst.evmap.databinding.ItemConnectorButtonBinding +import net.vonforst.evmap.ui.CheckableConstraintLayout import net.vonforst.evmap.viewmodel.FavoritesViewModel interface Equatable { @@ -88,4 +96,73 @@ class FavoritesAdapter(val vm: FavoritesViewModel) : override fun getItemViewType(position: Int): Int = R.layout.item_favorite override fun getItemId(position: Int): Long = getItem(position).charger.id +} + +class ChargepriceAdapter() : + DataBindingAdapter() { + + val viewPool = RecyclerView.RecycledViewPool(); + var meta: ChargepriceChargepointMeta? = null + set(value) { + field = value + notifyDataSetChanged() + } + + override fun getItemViewType(position: Int): Int = R.layout.item_chargeprice + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + 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, item: ChargePrice) { + super.bind(holder, item) + (holder.binding as ItemChargepriceBinding).meta = meta + } +} + +class CheckableConnectorAdapter : DataBindingAdapter() { + private var checkedItem: Int = 0 + + override fun getItemViewType(position: Int): Int = R.layout.item_connector_button + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + super.bind(holder, getItem(position)) + val binding = holder.binding as ItemConnectorButtonBinding + val root = binding.root as CheckableConstraintLayout + root.isChecked = checkedItem == position + root.setOnClickListener { + root.isChecked = true + } + root.setOnCheckedChangeListener { v: View, checked: Boolean -> + if (checked) { + checkedItem = position + notifyDataSetChanged() + onCheckedItemChangedListener?.invoke(getCheckedItem()) + } + } + } + + fun getCheckedItem(): Chargepoint = getItem(checkedItem) + + fun setCheckedItem(item: Chargepoint) { + checkedItem = currentList.indexOf(item) + } + + var onCheckedItemChangedListener: ((Chargepoint) -> Unit)? = null +} + +class ChargepriceTagsAdapter() : + DataBindingAdapter() { + override fun getItemViewType(position: Int): Int = R.layout.item_chargeprice_tag } \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceApi.kt b/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceApi.kt new file mode 100644 index 00000000..24c410b9 --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceApi.kt @@ -0,0 +1,75 @@ +package net.vonforst.evmap.api.chargeprice + +import android.content.Context +import com.facebook.stetho.okhttp3.StethoInterceptor +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import moe.banana.jsonapi2.ArrayDocument +import moe.banana.jsonapi2.JsonApiConverterFactory +import moe.banana.jsonapi2.ResourceAdapterFactory +import net.vonforst.evmap.BuildConfig +import okhttp3.Cache +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.POST + +interface ChargepriceApi { + @POST("charge_prices") + suspend fun getChargePrices( + @Body request: ChargepriceRequest, + @Header("Accept-Language") language: String + ): ArrayDocument + + @GET("vehicles") + suspend fun getVehicles(): ArrayDocument + + companion object { + private val cacheSize = 1L * 1024 * 1024 // 1MB + val supportedLanguages = setOf("de", "en", "fr", "nl") + + private val jsonApiAdapterFactory = ResourceAdapterFactory.builder() + .add(ChargepriceRequest::class.java) + .add(ChargepriceTariff::class.java) + .add(ChargepriceBrand::class.java) + .add(ChargePrice::class.java) + .add(ChargepriceCar::class.java) + .build() + val moshi = Moshi.Builder() + .add(jsonApiAdapterFactory) + .add(KotlinJsonAdapterFactory()) + .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) { + addNetworkInterceptor(StethoInterceptor()) + } + if (context != null) { + cache(Cache(context.getCacheDir(), cacheSize)) + } + }.build() + + val retrofit = Retrofit.Builder() + .baseUrl(baseurl) + .addConverterFactory(JsonApiConverterFactory.create(moshi)) + .client(client) + .build() + return retrofit.create(ChargepriceApi::class.java) + } + } +} diff --git a/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceModel.kt b/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceModel.kt new file mode 100644 index 00000000..0235b596 --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceModel.kt @@ -0,0 +1,280 @@ +package net.vonforst.evmap.api.chargeprice + +import android.content.Context +import com.squareup.moshi.Json +import moe.banana.jsonapi2.HasMany +import moe.banana.jsonapi2.HasOne + +import moe.banana.jsonapi2.JsonApi +import moe.banana.jsonapi2.Resource +import net.vonforst.evmap.R +import net.vonforst.evmap.adapter.Equatable +import net.vonforst.evmap.api.goingelectric.ChargeLocation +import net.vonforst.evmap.ui.currency +import kotlin.math.ceil +import kotlin.math.floor + + +@JsonApi(type = "charge_price_request") +class ChargepriceRequest : Resource() { + @field:Json(name = "data_adapter") + lateinit var dataAdapter: String + lateinit var station: ChargepriceStation + lateinit var options: ChargepriceOptions + var tariffs: HasMany? = null + var vehicle: HasOne? = null +} + +data class ChargepriceStation( + val longitude: Double, + val latitude: Double, + val country: String?, + val network: String?, + @Json(name = "charge_points") val chargePoints: List +) { + companion object { + fun fromGoingelectric(geCharger: ChargeLocation): ChargepriceStation { + return ChargepriceStation( + geCharger.coordinates.lng, + geCharger.coordinates.lat, + geCharger.address.country, + geCharger.network, + geCharger.chargepoints.map { + ChargepriceChargepoint(it.power, it.type) + } + ) + } + } +} + +data class ChargepriceChargepoint( + val power: Double, + val plug: String +) + +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? = 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 +) + +@JsonApi(type = "tariff") +data class ChargepriceTariff( + val provider: String, + val name: String, + @field:Json(name = "direct_payment") val directPayment: Boolean, + @field:Json(name = "provider_customer_tariff") val providerCustomerTariff: Boolean, + @field:Json(name = "charge_card_id") val chargeCardId: String // GE charge card ID +) : Resource() + +@JsonApi(type = "car") +class ChargepriceCar : Resource() { + lateinit var name: String + lateinit var brand: String + + @field:Json(name = "dc_charge_ports") + lateinit var dcChargePorts: List + lateinit var manufacturer: HasOne + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + if (!super.equals(other)) return false + + other as ChargepriceCar + + if (name != other.name) return false + if (brand != other.brand) return false + if (dcChargePorts != other.dcChargePorts) return false + if (manufacturer != other.manufacturer) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + brand.hashCode() + result = 31 * result + dcChargePorts.hashCode() + result = 31 * result + manufacturer.hashCode() + return result + } +} + +@JsonApi(type = "brand") +class ChargepriceBrand : Resource() + +@JsonApi(type = "charge_price") +class ChargePrice : Resource(), Equatable, Cloneable { + lateinit var provider: String + + @field:Json(name = "tariff_name") + lateinit var tariffName: String + lateinit var url: String + + @field:Json(name = "monthly_min_sales") + var monthlyMinSales: Double = 0.0 + + @field:Json(name = "total_monthly_fee") + var totalMonthlyFee: Double = 0.0 + + @field:Json(name = "flat_rate") + var flatRate: Boolean = false + + @field:Json(name = "direct_payment") + var directPayment: Boolean = false + + @field:Json(name = "provider_customer_tariff") + var providerCustomerTariff: Boolean = false + lateinit var currency: String + + @field:Json(name = "start_time") + var startTime: Int = 0 + lateinit var tags: List + + @field:Json(name = "charge_point_prices") + lateinit var chargepointPrices: List + + + 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(", ") + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + if (!super.equals(other)) return false + + other as ChargePrice + + if (provider != other.provider) return false + if (tariffName != other.tariffName) return false + if (url != other.url) return false + if (monthlyMinSales != other.monthlyMinSales) return false + if (totalMonthlyFee != other.totalMonthlyFee) return false + if (flatRate != other.flatRate) return false + if (directPayment != other.directPayment) return false + if (providerCustomerTariff != other.providerCustomerTariff) return false + if (currency != other.currency) return false + if (startTime != other.startTime) return false + if (tags != other.tags) return false + if (chargepointPrices != other.chargepointPrices) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + provider.hashCode() + result = 31 * result + tariffName.hashCode() + result = 31 * result + url.hashCode() + result = 31 * result + monthlyMinSales.hashCode() + result = 31 * result + totalMonthlyFee.hashCode() + result = 31 * result + flatRate.hashCode() + result = 31 * result + directPayment.hashCode() + result = 31 * result + providerCustomerTariff.hashCode() + result = 31 * result + currency.hashCode() + result = 31 * result + startTime + result = 31 * result + tags.hashCode() + result = 31 * result + chargepointPrices.hashCode() + return result + } + + public override fun clone(): ChargePrice { + return ChargePrice().apply { + chargepointPrices = this@ChargePrice.chargepointPrices + currency = this@ChargePrice.currency + directPayment = this@ChargePrice.directPayment + flatRate = this@ChargePrice.flatRate + monthlyMinSales = this@ChargePrice.monthlyMinSales + provider = this@ChargePrice.provider + providerCustomerTariff = this@ChargePrice.providerCustomerTariff + startTime = this@ChargePrice.startTime + tags = this@ChargePrice.tags + tariffName = this@ChargePrice.tariffName + totalMonthlyFee = this@ChargePrice.totalMonthlyFee + url = this@ChargePrice.url + } + } +} + +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? +) { + 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(); + if (h == 0 && min > 0) return "${min}min"; + // be slightly sloppy (3:01 is shown as 3h) to save space + else if (h > 0 && (min == 0 || min == 1)) return "${h}h"; + else return "%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") + } + } +} + +data class PriceDistribution(val kwh: Double?, val session: Double?, val minute: Double?) { + val isOnlyKwh = + kwh != null && kwh > 0 && (session == null || session == 0.0) && (minute == null || minute == 0.0) +} + +data class ChargepriceTag(val kind: String, val text: String, val url: String?) : Equatable + +data class ChargepriceMeta( + @Json(name = "charge_points") val chargePoints: List +) + +data class ChargepriceChargepointMeta( + val power: Double, + val plug: String, + val energy: Double, + val duration: Double +) \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricApi.kt b/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricApi.kt index f6333be6..6c3f502f 100644 --- a/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricApi.kt +++ b/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricApi.kt @@ -69,6 +69,13 @@ interface GoingElectricApi { companion object { private val cacheSize = 10L * 1024 * 1024 // 10MB + val moshi = Moshi.Builder() + .add(ChargepointListItemJsonAdapterFactory()) + .add(JsonObjectOrFalseAdapter.Factory()) + .add(HoursAdapter()) + .add(InstantAdapter()) + .build() + fun create( apikey: String, baseurl: String = "https://api.goingelectric.de", @@ -90,13 +97,6 @@ interface GoingElectricApi { } }.build() - val moshi = Moshi.Builder() - .add(ChargepointListItemJsonAdapterFactory()) - .add(JsonObjectOrFalseAdapter.Factory()) - .add(HoursAdapter()) - .add(InstantAdapter()) - .build() - val retrofit = Retrofit.Builder() .baseUrl(baseurl) .addConverterFactory(MoshiConverterFactory.create(moshi)) diff --git a/app/src/main/java/net/vonforst/evmap/fragment/ChargepriceFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/ChargepriceFragment.kt new file mode 100644 index 00000000..28937a0f --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/fragment/ChargepriceFragment.kt @@ -0,0 +1,163 @@ +package net.vonforst.evmap.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer +import androidx.navigation.fragment.findNavController +import androidx.navigation.ui.setupWithNavController +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import net.vonforst.evmap.MapsActivity +import net.vonforst.evmap.R +import net.vonforst.evmap.adapter.ChargepriceAdapter +import net.vonforst.evmap.adapter.CheckableConnectorAdapter +import net.vonforst.evmap.api.goingelectric.ChargeLocation +import net.vonforst.evmap.api.goingelectric.Chargepoint +import net.vonforst.evmap.api.goingelectric.GoingElectricApi +import net.vonforst.evmap.databinding.FragmentChargepriceBinding +import net.vonforst.evmap.viewmodel.ChargepriceViewModel +import net.vonforst.evmap.viewmodel.viewModelFactory +import java.text.NumberFormat + +class ChargepriceFragment : DialogFragment() { + private lateinit var binding: FragmentChargepriceBinding + + private val vm: ChargepriceViewModel by viewModels(factoryProducer = { + viewModelFactory { + ChargepriceViewModel( + requireActivity().application, + getString(R.string.chargeprice_key) + ) + } + }) + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + dialog?.window?.attributes?.windowAnimations = R.style.ChargepriceDialogAnimation + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = DataBindingUtil.inflate( + inflater, + R.layout.fragment_chargeprice, container, false + ) + binding.lifecycleOwner = this + binding.vm = vm + + binding.toolbar.inflateMenu(R.menu.chargeprice) + binding.toolbar.setTitle(R.string.chargeprice_title) + + return binding.root + } + + override fun onStart() { + super.onStart() + + // dialog with 95% screen height + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + (resources.displayMetrics.heightPixels * 0.95).toInt() + ) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val toolbar = view.findViewById(R.id.toolbar) as Toolbar + + val navController = findNavController() + toolbar.setupWithNavController( + navController, + (requireActivity() as MapsActivity).appBarConfiguration + ) + + val jsonAdapter = GoingElectricApi.moshi.adapter(ChargeLocation::class.java) + val charger = jsonAdapter.fromJson(requireArguments().getString(ARG_CHARGER)!!)!! + vm.charger.value = charger + if (vm.chargepoint.value == null) { + vm.chargepoint.value = charger.chargepointsMerged.get(0) + } + + val chargepriceAdapter = ChargepriceAdapter().apply { + onClickListener = { + (requireActivity() as MapsActivity).openUrl(it.url) + } + } + binding.chargePricesList.apply { + adapter = chargepriceAdapter + layoutManager = + LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) + addItemDecoration( + DividerItemDecoration( + context, LinearLayoutManager.VERTICAL + ) + ) + } + vm.chargepriceMetaForChargepoint.observe(viewLifecycleOwner) { + chargepriceAdapter.meta = it.data + } + + val connectorsAdapter = CheckableConnectorAdapter() + binding.connectorsList.apply { + adapter = connectorsAdapter + layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) + } + + val observer: Observer = Observer { + connectorsAdapter.setCheckedItem(it) + } + vm.chargepoint.observe(viewLifecycleOwner, observer) + connectorsAdapter.onCheckedItemChangedListener = { + vm.chargepoint.removeObserver(observer) + vm.chargepoint.value = it + vm.chargepoint.observe(viewLifecycleOwner, observer) + } + + binding.imgChargepriceLogo.setOnClickListener { + (requireActivity() as MapsActivity).openUrl("https://www.chargeprice.app/?poi_id=${charger.id}&poi_source=going_electric") + } + + binding.btnSettings.setOnClickListener { + navController.navigate(R.id.action_chargeprice_to_settingsFragment) + } + + binding.batteryRange.setLabelFormatter { value: Float -> + val fmt = NumberFormat.getNumberInstance() + fmt.maximumFractionDigits = 0 + fmt.format(value.toDouble()) + } + + binding.toolbar.setOnMenuItemClickListener { + when (it.itemId) { + R.id.menu_close -> { + dismiss() + true + } + else -> false + } + } + } + + companion object { + val ARG_CHARGER = "charger" + + fun showCharger(charger: ChargeLocation): Bundle { + return Bundle().apply { + putString( + ARG_CHARGER, + GoingElectricApi.moshi.adapter(ChargeLocation::class.java).toJson(charger) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt index 4f1a2278..5525ce96 100644 --- a/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt +++ b/app/src/main/java/net/vonforst/evmap/fragment/MapFragment.kt @@ -296,8 +296,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac } binding.detailView.btnChargeprice.setOnClickListener { val charger = vm.charger.value?.data ?: return@setOnClickListener - (activity as? MapsActivity)?.openUrl( - "https://www.chargeprice.app/?poi_id=${charger.id}&poi_source=going_electric" + findNavController().navigate( + R.id.action_map_to_chargepriceFragment, + ChargepriceFragment.showCharger(charger) ) } binding.detailView.topPart.setOnClickListener { diff --git a/app/src/main/java/net/vonforst/evmap/fragment/SettingsFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/SettingsFragment.kt index 148dbb37..7d85892c 100644 --- a/app/src/main/java/net/vonforst/evmap/fragment/SettingsFragment.kt +++ b/app/src/main/java/net/vonforst/evmap/fragment/SettingsFragment.kt @@ -4,20 +4,33 @@ import android.content.SharedPreferences import android.os.Bundle import android.view.View import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.navigation.ui.setupWithNavController +import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import net.vonforst.evmap.MapsActivity import net.vonforst.evmap.R import net.vonforst.evmap.storage.PreferenceDataSource import net.vonforst.evmap.ui.updateNightMode +import net.vonforst.evmap.viewmodel.SettingsViewModel +import net.vonforst.evmap.viewmodel.viewModelFactory class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener { private lateinit var prefs: PreferenceDataSource + private val vm: SettingsViewModel by viewModels(factoryProducer = { + viewModelFactory { + SettingsViewModel( + requireActivity().application, + getString(R.string.chargeprice_key) + ) + } + }) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val toolbar = view.findViewById(R.id.toolbar) as Toolbar @@ -28,6 +41,20 @@ class SettingsFragment : PreferenceFragmentCompat(), navController, (requireActivity() as MapsActivity).appBarConfiguration ) + + val 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 { "${it.brand} ${it.name}" }.toTypedArray() + myVehiclePreference.isEnabled = true + myVehiclePreference.summary = cars.find { it.id == prefs.chargepriceMyVehicle } + ?.let { "${it.brand} ${it.name}" } + } + } } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { diff --git a/app/src/main/java/net/vonforst/evmap/storage/PreferenceDataSource.kt b/app/src/main/java/net/vonforst/evmap/storage/PreferenceDataSource.kt index 782aba1a..486b7dc7 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/PreferenceDataSource.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/PreferenceDataSource.kt @@ -97,4 +97,22 @@ class PreferenceDataSource(val context: Context) { set(value) { sp.edit().putBoolean("update_0.6.0_androidauto_dialog_shown", value).apply() } + + var chargepriceMyVehicle: String? + get() = sp.getString("chargeprice_my_vehicle", null) + set(value) { + sp.edit().putString("chargeprice_my_vehicle", 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() + } } \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/ui/BalancedBreakingTextView.kt b/app/src/main/java/net/vonforst/evmap/ui/BalancedBreakingTextView.kt new file mode 100644 index 00000000..844f84ec --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/ui/BalancedBreakingTextView.kt @@ -0,0 +1,33 @@ +package net.vonforst.evmap.ui + +import android.content.Context +import android.text.Layout +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView +import kotlin.math.ceil + +class BalancedBreakingTextView(context: Context, attrs: AttributeSet) : + AppCompatTextView(context, attrs) { + + @Override + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + + if (layout != null) { + val width = + ceil(getMaxLineWidth(layout)).toInt() + compoundPaddingLeft + compoundPaddingRight + val height = measuredHeight + setMeasuredDimension(width, height) + } + } + + private fun getMaxLineWidth(layout: Layout): Float { + var maxWidth = 0.0f + for (i in 0 until layout.lineCount) { + if (layout.getLineWidth(i) > maxWidth) { + maxWidth = layout.getLineWidth(i) + } + } + return maxWidth + } +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/ui/BindingAdapters.kt b/app/src/main/java/net/vonforst/evmap/ui/BindingAdapters.kt index 68f6d60c..9d3cc87c 100644 --- a/app/src/main/java/net/vonforst/evmap/ui/BindingAdapters.kt +++ b/app/src/main/java/net/vonforst/evmap/ui/BindingAdapters.kt @@ -11,10 +11,13 @@ import androidx.core.content.ContextCompat import androidx.core.content.res.use import androidx.core.text.HtmlCompat import androidx.databinding.BindingAdapter +import androidx.databinding.InverseBindingAdapter +import androidx.databinding.InverseBindingListener import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.google.android.material.slider.RangeSlider import net.vonforst.evmap.R import net.vonforst.evmap.api.availability.ChargepointStatus import net.vonforst.evmap.api.iconForPlugType @@ -177,6 +180,35 @@ fun setLinkify(textView: TextView, oldValue: Int, newValue: Int) { } } +@BindingAdapter("chargepriceTagColor") +fun setChargepriceTagColor(view: TextView, kind: String) { + view.backgroundTintList = ColorStateList.valueOf( + ContextCompat.getColor( + view.context, + when (kind) { + "star" -> R.color.chargeprice_star + "alert" -> R.color.chargeprice_alert + "info" -> R.color.chargeprice_info + "lock" -> R.color.chargeprice_lock + else -> R.color.chip_background + } + ) + ) +} + +@BindingAdapter("chargepriceTagIcon") +fun setChargepriceTagIcon(view: TextView, kind: String) { + view.setCompoundDrawablesRelativeWithIntrinsicBounds( + when (kind) { + "star" -> R.drawable.ic_chargeprice_star + "alert" -> R.drawable.ic_chargeprice_alert + "info" -> R.drawable.ic_chargeprice_info + "lock" -> R.drawable.ic_chargeprice_lock + else -> 0 + }, 0, 0, 0 + ) +} + private fun availabilityColor( status: List?, context: Context @@ -210,4 +242,31 @@ fun availabilityText(status: List?): String? { fun flatten(it: Iterable>?): List? { return it?.flatten() +} + +fun currency(currency: String): String { + // shorthands for currencies + return when (currency) { + "EUR" -> "€" + "USD" -> "$" + "DKK", "SEK", "NOK" -> "kr." + "PLN" -> "zł" + "CHF" -> "Fr." + "CZK" -> "Kč" + "GBP" -> "£" + "HRK" -> "kn" + "HUF" -> "Ft" + "ISK" -> "Kr" + else -> currency + } +} + +@InverseBindingAdapter(attribute = "app:values") +fun getRangeSliderValue(slider: RangeSlider) = slider.values + +@BindingAdapter("app:valuesAttrChanged") +fun setRangeSliderListeners(slider: RangeSlider, attrChange: InverseBindingListener) { + slider.addOnChangeListener { _, _, _ -> + attrChange.onChange() + } } \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/ui/CheckableConstraintLayout.kt b/app/src/main/java/net/vonforst/evmap/ui/CheckableConstraintLayout.kt new file mode 100644 index 00000000..92443f65 --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/ui/CheckableConstraintLayout.kt @@ -0,0 +1,48 @@ +package net.vonforst.evmap.ui + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.Checkable +import androidx.constraintlayout.widget.ConstraintLayout + + +class CheckableConstraintLayout(ctx: Context, attrs: AttributeSet) : ConstraintLayout(ctx, attrs), + Checkable { + private var onCheckedChangeListener: ((View, Boolean) -> Unit)? = null + private var checked = false + private val CHECKED_STATE_SET = intArrayOf(android.R.attr.state_checked) + + override fun setChecked(b: Boolean) { + if (b != checked) { + checked = b; + refreshDrawableState(); + onCheckedChangeListener?.invoke(this, checked); + } + } + + override fun isChecked(): Boolean { + return checked + } + + override fun toggle() { + checked = !checked + } + + override fun onCreateDrawableState(extraSpace: Int): IntArray? { + val drawableState = super.onCreateDrawableState(extraSpace + 1) + if (isChecked) { + mergeDrawableStates(drawableState, CHECKED_STATE_SET) + } + return drawableState + } + + /** + * Register a callback to be invoked when the checked state of this view changes. + * + * @param listener the callback to call on checked state change + */ + fun setOnCheckedChangeListener(listener: (View, Boolean) -> Unit) { + onCheckedChangeListener = listener + } +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/ChargepriceViewModel.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/ChargepriceViewModel.kt new file mode 100644 index 00000000..e3d423bb --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/ChargepriceViewModel.kt @@ -0,0 +1,152 @@ +package net.vonforst.evmap.viewmodel + +import android.app.Application +import androidx.lifecycle.* +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import moe.banana.jsonapi2.HasOne +import net.vonforst.evmap.api.chargeprice.* +import net.vonforst.evmap.api.goingelectric.ChargeLocation +import net.vonforst.evmap.api.goingelectric.Chargepoint +import net.vonforst.evmap.storage.PreferenceDataSource +import java.io.IOException +import java.util.* + +class ChargepriceViewModel(application: Application, chargepriceApiKey: String) : + AndroidViewModel(application) { + private var api = ChargepriceApi.create(chargepriceApiKey) + private var prefs = PreferenceDataSource(application) + + val charger: MutableLiveData by lazy { + MutableLiveData() + } + + val chargepoint: MutableLiveData by lazy { + MutableLiveData() + } + + val vehicle: LiveData by lazy { + MutableLiveData().apply { + value = prefs.chargepriceMyVehicle?.let { ChargepriceCar().apply { id = it } } + } + } + + val batteryRange: MutableLiveData> by lazy { + MutableLiveData>().apply { + value = listOf(20f, 80f) + } + } + + val chargePrices: MediatorLiveData>> by lazy { + MediatorLiveData>>().apply { + value = Resource.loading(null) + listOf(charger, vehicle, batteryRange).forEach { + addSource(it) { + loadPrices() + } + } + } + } + + val chargePriceMeta: MutableLiveData> by lazy { + MutableLiveData>().apply { + value = Resource.loading(null) + } + } + + val chargePricesForChargepoint: MediatorLiveData>> by lazy { + MediatorLiveData>>().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 { + value = Resource.success(cps.data!!.map { cp -> + val filteredPrices = + cp.chargepointPrices.filter { it.plug == chargepoint.type && it.power == chargepoint.power } + if (filteredPrices.isEmpty()) { + null + } else { + cp.clone().apply { + chargepointPrices = filteredPrices + } + } + }.filterNotNull().sortedBy { it.chargepointPrices.first().price }) + } + } + } + } + } + + val chargepriceMetaForChargepoint: MediatorLiveData> by lazy { + MediatorLiveData>().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 { + value = + Resource.success(cpMeta.data!!.chargePoints.filter { it.plug == chargepoint.type && it.power == chargepoint.power }[0]) + } + } + } + } + } + + private var loadPricesJob: Job? = null + private fun loadPrices() { + chargePrices.value = Resource.loading(null) + val geCharger = charger.value + val car = vehicle.value + if (geCharger == null || car == null) { + chargePrices.value = Resource.error(null, null) + return + } + + loadPricesJob?.cancel() + loadPricesJob = viewModelScope.launch { + delay(800) + try { + val result = api.getChargePrices(ChargepriceRequest().apply { + dataAdapter = "going_electric" + station = ChargepriceStation.fromGoingelectric(geCharger) + vehicle = HasOne(car) + options = ChargepriceOptions( + batteryRange = batteryRange.value!!.map { it.toDouble() }, + providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs, + maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null + ) + }, getChargepriceLanguage()) + val meta = + result.meta.get(ChargepriceApi.moshi.adapter(ChargepriceMeta::class.java)) as ChargepriceMeta + chargePrices.value = Resource.success(result) + chargePriceMeta.value = Resource.success(meta) + } catch (e: IOException) { + chargePrices.value = Resource.error(e.message, null) + chargePriceMeta.value = Resource.error(e.message, null) + } + } + } + + private fun getChargepriceLanguage(): String { + val locale = Locale.getDefault().language + return if (ChargepriceApi.supportedLanguages.contains(locale)) { + locale + } else { + "en" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/SettingsViewModel.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/SettingsViewModel.kt new file mode 100644 index 00000000..e5123e72 --- /dev/null +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/SettingsViewModel.kt @@ -0,0 +1,33 @@ +package net.vonforst.evmap.viewmodel + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +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 java.io.IOException + +class SettingsViewModel(application: Application, chargepriceApiKey: String) : + AndroidViewModel(application) { + private var api = ChargepriceApi.create(chargepriceApiKey) + + val vehicles: MutableLiveData>> by lazy { + MutableLiveData>>().apply { + value = Resource.loading(null) + loadVehicles() + } + } + + 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) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/anim/chargeprice_dialog_enter.xml b/app/src/main/res/anim/chargeprice_dialog_enter.xml new file mode 100644 index 00000000..1c6531f0 --- /dev/null +++ b/app/src/main/res/anim/chargeprice_dialog_enter.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/chargeprice_dialog_exit.xml b/app/src/main/res/anim/chargeprice_dialog_exit.xml new file mode 100644 index 00000000..a30075e0 --- /dev/null +++ b/app/src/main/res/anim/chargeprice_dialog_exit.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_outline.xml b/app/src/main/res/drawable/button_outline.xml new file mode 100644 index 00000000..bb4c2ade --- /dev/null +++ b/app/src/main/res/drawable/button_outline.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_chargeprice_alert.xml b/app/src/main/res/drawable/ic_chargeprice_alert.xml new file mode 100644 index 00000000..2484c15a --- /dev/null +++ b/app/src/main/res/drawable/ic_chargeprice_alert.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_chargeprice_info.xml b/app/src/main/res/drawable/ic_chargeprice_info.xml new file mode 100644 index 00000000..ddb2b894 --- /dev/null +++ b/app/src/main/res/drawable/ic_chargeprice_info.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_chargeprice_lock.xml b/app/src/main/res/drawable/ic_chargeprice_lock.xml new file mode 100644 index 00000000..fb4d8fbe --- /dev/null +++ b/app/src/main/res/drawable/ic_chargeprice_lock.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_chargeprice_star.xml b/app/src/main/res/drawable/ic_chargeprice_star.xml new file mode 100644 index 00000000..c1ee35ac --- /dev/null +++ b/app/src/main/res/drawable/ic_chargeprice_star.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 00000000..95cc170f --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_powered_by_chargeprice.xml b/app/src/main/res/drawable/ic_powered_by_chargeprice.xml new file mode 100644 index 00000000..51606907 --- /dev/null +++ b/app/src/main/res/drawable/ic_powered_by_chargeprice.xml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/rounded_rect_16dp.xml b/app/src/main/res/drawable/rounded_rect_16dp.xml new file mode 100644 index 00000000..ac4e6385 --- /dev/null +++ b/app/src/main/res/drawable/rounded_rect_16dp.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_chargeprice.xml b/app/src/main/res/layout/fragment_chargeprice.xml new file mode 100644 index 00000000..51c1846a --- /dev/null +++ b/app/src/main/res/layout/fragment_chargeprice.xml @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +