mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-26 00:27:45 -05:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2aa1fcf5bd | ||
|
|
221e5f49bc | ||
|
|
df6f26ad56 | ||
|
|
1210efd3b9 | ||
|
|
097be8c92b | ||
|
|
16031884ac | ||
|
|
c0b4c56eda | ||
|
|
9587ee948d | ||
|
|
890eec4419 | ||
|
|
c972c871d4 | ||
|
|
e4da902430 | ||
|
|
7a5d4b4107 | ||
|
|
80642b1731 | ||
|
|
6dab611c1b | ||
|
|
d9fc43af68 | ||
|
|
2fd0fa7e22 | ||
|
|
b04284fb16 | ||
|
|
7b3bd84d18 | ||
|
|
773d052819 | ||
|
|
4e0ad98e17 | ||
|
|
d8e572338a | ||
|
|
ff86eeff95 | ||
|
|
47f57992fb | ||
|
|
0ae59358ca | ||
|
|
576e0b9c42 | ||
|
|
3878b27154 | ||
|
|
2166ac076a | ||
|
|
c489df2aaf | ||
|
|
56712ff1af | ||
|
|
e2cf332f34 | ||
|
|
0b541d498d | ||
|
|
1bdc576300 | ||
|
|
fb5da76834 | ||
|
|
ad922f0667 | ||
|
|
773b35d9ce | ||
|
|
a3347c9d62 | ||
|
|
da671b8dd3 | ||
|
|
6d877e13e4 | ||
|
|
8ab1d7170c | ||
|
|
1f75d722cd | ||
|
|
11bd4b2cec |
@@ -8,7 +8,6 @@ apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'androidx.navigation.safeargs.kotlin'
|
||||
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
|
||||
apply plugin: 'de.timfreiheit.resourceplaceholders'
|
||||
|
||||
def supportedLocales = "en,de,fr,nb-rNO"
|
||||
|
||||
@@ -21,11 +20,11 @@ android {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
|
||||
versionCode 142
|
||||
versionName "1.4.2"
|
||||
versionCode 160
|
||||
versionName "1.4.6"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resConfigs supportedLocales.split(",")
|
||||
resConfigs supportedLocales.split(',')
|
||||
buildConfigField("String", "supportedLocales", '"' + supportedLocales + '"')
|
||||
}
|
||||
|
||||
@@ -104,9 +103,6 @@ android {
|
||||
unitTests.includeAndroidResources true
|
||||
}
|
||||
|
||||
resourcePlaceholders {
|
||||
files = ['xml/shortcuts.xml']
|
||||
}
|
||||
namespace 'net.vonforst.evmap'
|
||||
|
||||
// add API keys from environment variable if not set in apikeys.xml
|
||||
@@ -159,19 +155,19 @@ configurations {
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.6.0-rc01'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.0'
|
||||
implementation 'androidx.core:core-ktx:1.9.0'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.0'
|
||||
implementation "androidx.activity:activity-ktx:1.6.1"
|
||||
implementation "androidx.fragment:fragment-ktx:1.5.4"
|
||||
implementation "androidx.fragment:fragment-ktx:1.5.5"
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||
implementation 'com.google.android.material:material:1.7.0'
|
||||
implementation 'com.google.android.material:material:1.8.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
implementation 'androidx.browser:browser:1.4.0'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'com.github.johan12345:CustomBottomSheetBehavior:3529a5a9f1'
|
||||
implementation 'com.github.johan12345:CustomBottomSheetBehavior:f4f641aab5'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||||
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
|
||||
@@ -190,13 +186,13 @@ dependencies {
|
||||
implementation 'com.github.romandanylyk:PageIndicatorView:b1bad589b5'
|
||||
|
||||
// Android Auto
|
||||
def carAppVersion = '1.3.0-beta01'
|
||||
def carAppVersion = '1.3.0-rc01'
|
||||
googleImplementation "androidx.car.app:app:$carAppVersion"
|
||||
googleNormalImplementation "androidx.car.app:app-projected:$carAppVersion"
|
||||
googleAutomotiveImplementation "androidx.car.app:app-automotive:$carAppVersion"
|
||||
|
||||
// AnyMaps
|
||||
def anyMapsVersion = 'a9b3dd7d99'
|
||||
def anyMapsVersion = '7fdcf50fc4'
|
||||
implementation "com.github.johan12345.AnyMaps:anymaps-base:$anyMapsVersion"
|
||||
googleImplementation "com.github.johan12345.AnyMaps:anymaps-google:$anyMapsVersion"
|
||||
googleImplementation 'com.google.android.gms:play-services-maps:18.1.0'
|
||||
@@ -210,8 +206,8 @@ dependencies {
|
||||
implementation 'com.github.johan12345:mapbox-events-android:a21c324501'
|
||||
|
||||
// Google Places
|
||||
googleImplementation 'com.google.android.libraries.places:places:2.7.0'
|
||||
googleImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.1'
|
||||
googleImplementation 'com.google.android.libraries.places:places:3.0.0'
|
||||
googleImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.4'
|
||||
|
||||
// Mapbox Geocoding
|
||||
implementation 'com.mapbox.mapboxsdk:mapbox-sdk-services:5.5.0'
|
||||
@@ -226,13 +222,13 @@ dependencies {
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
|
||||
|
||||
// room library
|
||||
def room_version = "2.4.3"
|
||||
def room_version = "2.5.0"
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
implementation "androidx.room:room-ktx:$room_version"
|
||||
|
||||
// billing library
|
||||
def billing_version = "4.1.0"
|
||||
def billing_version = "5.1.0"
|
||||
googleImplementation "com.android.billingclient:billing:$billing_version"
|
||||
googleImplementation "com.android.billingclient:billing-ktx:$billing_version"
|
||||
|
||||
@@ -257,12 +253,12 @@ dependencies {
|
||||
testGoogleImplementation 'org.robolectric:robolectric:4.9'
|
||||
testGoogleImplementation 'androidx.test:core:1.5.0'
|
||||
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.4'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
|
||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.13.0"
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.0'
|
||||
}
|
||||
|
||||
private static String decode(String s, String key) {
|
||||
|
||||
@@ -2,5 +2,4 @@
|
||||
<resources>
|
||||
<string name="pref_search_provider_default" translatable="false">mapbox</string>
|
||||
<string name="pref_map_provider_default" translatable="false">mapbox</string>
|
||||
<string name="paypal_link" translatable="false">https://paypal.me/johan98</string>
|
||||
</resources>
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
<queries>
|
||||
<package android:name="com.google.android.projection.gearhead" />
|
||||
<package android:name="com.google.android.apps.automotive.templates.host" />
|
||||
</queries>
|
||||
|
||||
<application>
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.Screen
|
||||
@@ -22,7 +17,6 @@ import jsonapi.ResourceIdentifier
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.chargeprice.*
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
@@ -49,9 +43,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
private var prices: List<ChargePrice>? = null
|
||||
private var meta: ChargepriceChargepointMeta? = null
|
||||
private var chargepoint: Chargepoint? = null
|
||||
private val maxRows = if (ctx.carAppApiLevel >= 2) {
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
} else 6
|
||||
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
private var errorMessage: String? = null
|
||||
private val batteryRange = prefs.chargepriceBatteryRangeAndroidAuto
|
||||
|
||||
@@ -77,34 +69,54 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
carContext.stringProvider(),
|
||||
chargepoint.type
|
||||
)
|
||||
} ${chargepoint.formatPower()} " + carContext.getString(
|
||||
R.string.chargeprice_stats,
|
||||
meta.energy,
|
||||
time(meta.duration.roundToInt()),
|
||||
meta.energy / meta.duration * 60
|
||||
)
|
||||
} ${chargepoint.formatPower()} ${
|
||||
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 list = ItemList.Builder().apply {
|
||||
setNoItemsMessage(
|
||||
errorMessage ?: carContext.getString(R.string.chargeprice_no_tariffs_found)
|
||||
)
|
||||
prices?.take(maxRows)?.forEach { price ->
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(formatProvider(price))
|
||||
addText(formatPrice(price))
|
||||
if (carContext.carAppApiLevel >= 5) {
|
||||
setEnabled(myTariffsAll || myTariffs != null && price.tariffId in myTariffs)
|
||||
}
|
||||
}.build())
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
if (header != null && list.items.isNotEmpty()) {
|
||||
addSectionedList(SectionedItemList.create(list, header))
|
||||
} else {
|
||||
setSingleList(list)
|
||||
val list = buildPricesList(prices)
|
||||
if (header != null && list.items.isNotEmpty()) {
|
||||
addSectionedList(SectionedItemList.create(list, header))
|
||||
} else {
|
||||
setSingleList(list)
|
||||
}
|
||||
}
|
||||
}
|
||||
setActionStrip(
|
||||
@@ -117,44 +129,28 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
)
|
||||
).build()
|
||||
).setOnClickListener {
|
||||
val intent = CustomTabsIntent.Builder()
|
||||
.setDefaultColorSchemeParams(
|
||||
CustomTabColorSchemeParams.Builder()
|
||||
.setToolbarColor(
|
||||
ContextCompat.getColor(
|
||||
carContext,
|
||||
R.color.colorPrimary
|
||||
)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.build().intent
|
||||
intent.data =
|
||||
Uri.parse(ChargepriceApi.getPoiUrl(charger))
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
try {
|
||||
carContext.startActivity(intent)
|
||||
if (BuildConfig.FLAVOR_automotive != "automotive") {
|
||||
// only show the toast "opened on phone" if we're running on a phone
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.opened_on_phone,
|
||||
CarToast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.no_browser_app_found,
|
||||
CarToast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
openUrl(carContext, 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
|
||||
@@ -210,7 +206,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
}
|
||||
|
||||
private fun loadPrices(model: Model?) {
|
||||
val dataAdapter = ChargepriceApi.getDataAdapter(charger) ?: return
|
||||
val dataAdapter = ChargepriceApi.getDataAdapter(charger)
|
||||
val manufacturer = model?.manufacturer?.value
|
||||
val modelName = getVehicleModel(model?.manufacturer?.value, model?.name?.value)
|
||||
lifecycleScope.launch {
|
||||
|
||||
@@ -64,9 +64,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
private val iconGen =
|
||||
ChargerIconGenerator(carContext, null, height = imageSize)
|
||||
|
||||
private val maxRows = if (ctx.carAppApiLevel >= 2) {
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PANE)
|
||||
} else 2
|
||||
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PANE)
|
||||
private val largeImageSupported =
|
||||
ctx.carAppApiLevel >= 4 // since API 4, Row.setImage is supported
|
||||
|
||||
@@ -350,23 +348,31 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
private fun generateChargepointsText(charger: ChargeLocation): SpannableStringBuilder {
|
||||
val chargepointsText = SpannableStringBuilder()
|
||||
charger.chargepointsMerged.forEachIndexed { i, cp ->
|
||||
if (i > 0) chargepointsText.append(" · ")
|
||||
chargepointsText.append(
|
||||
"${cp.count}× "
|
||||
).append(
|
||||
nameForPlugType(carContext.stringProvider(), cp.type),
|
||||
CarIconSpan.create(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
iconForPlugType(cp.type)
|
||||
)
|
||||
).setTint(
|
||||
CarColor.createCustom(Color.WHITE, Color.BLACK)
|
||||
).build()
|
||||
),
|
||||
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
|
||||
).append(" ").append(cp.formatPower())
|
||||
chargepointsText.apply {
|
||||
if (i > 0) append(" · ")
|
||||
append("${cp.count}× ")
|
||||
val plugIcon = iconForPlugType(cp.type)
|
||||
if (plugIcon != 0) {
|
||||
append(
|
||||
nameForPlugType(carContext.stringProvider(), cp.type),
|
||||
CarIconSpan.create(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
plugIcon
|
||||
)
|
||||
).setTint(
|
||||
CarColor.createCustom(Color.WHITE, Color.BLACK)
|
||||
).build()
|
||||
),
|
||||
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
} else {
|
||||
append(nameForPlugType(carContext.stringProvider(), cp.type))
|
||||
}
|
||||
append(" ")
|
||||
append(cp.formatPower())
|
||||
}
|
||||
availability?.status?.get(cp)?.let { status ->
|
||||
chargepointsText.append(
|
||||
" (${availabilityText(status)}/${cp.count})",
|
||||
|
||||
@@ -31,10 +31,8 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
db.filterProfileDao().getProfiles(prefs.dataSource)
|
||||
}
|
||||
|
||||
private val maxRows = if (ctx.carAppApiLevel >= 2) {
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
} else 6
|
||||
|
||||
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
private val supportsRefresh = ctx.isAppDrivenRefreshSupported
|
||||
private var page = 0
|
||||
|
||||
init {
|
||||
@@ -129,10 +127,14 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
)
|
||||
setOnClickListener {
|
||||
page -= 1
|
||||
screenManager.pushForResult(DummyReturnScreen(carContext)) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
invalidate()
|
||||
if (!supportsRefresh) {
|
||||
screenManager.pushForResult(DummyReturnScreen(carContext)) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}.build())
|
||||
@@ -222,10 +224,14 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
)
|
||||
setOnClickListener {
|
||||
page += 1
|
||||
screenManager.pushForResult(DummyReturnScreen(carContext)) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
invalidate()
|
||||
if (!supportsRefresh) {
|
||||
screenManager.pushForResult(DummyReturnScreen(carContext)) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}.build())
|
||||
@@ -243,9 +249,8 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
|
||||
private val vm = FilterViewModel(carContext.applicationContext as Application)
|
||||
|
||||
private val maxRows = if (ctx.carAppApiLevel >= 2) {
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
} else 6
|
||||
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
private val supportsRefresh = ctx.isAppDrivenRefreshSupported
|
||||
|
||||
private var page = 0
|
||||
private var paginatedFilters = vm.filtersWithValue.map {
|
||||
@@ -372,10 +377,14 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
|
||||
)
|
||||
setOnClickListener {
|
||||
page -= 1
|
||||
screenManager.pushForResult(DummyReturnScreen(carContext)) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
invalidate()
|
||||
if (!supportsRefresh) {
|
||||
screenManager.pushForResult(DummyReturnScreen(carContext)) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}.build())
|
||||
@@ -454,10 +463,14 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
|
||||
)
|
||||
setOnClickListener {
|
||||
page += 1
|
||||
screenManager.pushForResult(DummyReturnScreen(carContext)) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
invalidate()
|
||||
if (!supportsRefresh) {
|
||||
screenManager.pushForResult(DummyReturnScreen(carContext)) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}.build())
|
||||
|
||||
@@ -71,6 +71,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
|
||||
private var location: Location? = null
|
||||
private var lastDistanceUpdateTime: Instant? = null
|
||||
private var lastChargersUpdateTime: Instant? = null
|
||||
private var chargers: List<ChargeLocation>? = null
|
||||
private var loadingError = false
|
||||
private var locationError = false
|
||||
@@ -81,14 +82,13 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
private val searchRadius = 5 // kilometers
|
||||
private val distanceUpdateThreshold = Duration.ofSeconds(15)
|
||||
private val availabilityUpdateThreshold = Duration.ofMinutes(1)
|
||||
private val chargersUpdateThresholdDistance = 500 // meters
|
||||
private val chargersUpdateThresholdTime = Duration.ofSeconds(30)
|
||||
private var availabilities: MutableMap<Long, Pair<ZonedDateTime, ChargeLocationStatus?>> =
|
||||
HashMap()
|
||||
private val maxRows = if (ctx.carAppApiLevel >= 2) {
|
||||
min(
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST),
|
||||
25
|
||||
)
|
||||
} else 6
|
||||
private val maxRows =
|
||||
min(ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST), 25)
|
||||
private val supportsRefresh = ctx.isAppDrivenRefreshSupported
|
||||
|
||||
private var filterStatus = prefs.filterStatus
|
||||
private var filtersWithValue: List<FilterWithValue<FilterValue>>? = null
|
||||
@@ -205,7 +205,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
).setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
screenManager.push(SettingsScreen(carContext))
|
||||
screenManager.push(SettingsScreen(carContext, session))
|
||||
session.mapScreen = null
|
||||
}
|
||||
.build())
|
||||
@@ -223,11 +223,16 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
).build()
|
||||
|
||||
)
|
||||
setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
setOnClickListener {
|
||||
if (prefs.placeSearchResultAndroidAuto != null) {
|
||||
prefs.placeSearchResultAndroidAutoName = null
|
||||
prefs.placeSearchResultAndroidAuto = null
|
||||
screenManager.pushForResult(DummyReturnScreen(carContext)) {
|
||||
if (!supportsRefresh) {
|
||||
screenManager.pushForResult(DummyReturnScreen(carContext)) {
|
||||
chargers = null
|
||||
loadChargers()
|
||||
}
|
||||
} else {
|
||||
chargers = null
|
||||
loadChargers()
|
||||
}
|
||||
@@ -243,7 +248,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
}
|
||||
session.mapScreen = null
|
||||
}
|
||||
})
|
||||
}
|
||||
}.build())
|
||||
.addAction(
|
||||
Action.Builder()
|
||||
@@ -263,7 +268,9 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
}
|
||||
.build())
|
||||
.build())
|
||||
if (carContext.carAppApiLevel >= 5) {
|
||||
if (carContext.carAppApiLevel >= 5 ||
|
||||
(BuildConfig.FLAVOR_automotive == "automotive" && carContext.carAppApiLevel >= 4)
|
||||
) {
|
||||
setOnContentRefreshListener(this@MapScreen)
|
||||
}
|
||||
}.build()
|
||||
@@ -328,7 +335,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
availabilities[charger.id]?.second?.let { av ->
|
||||
val status = av.status.values.flatten()
|
||||
val available = availabilityText(status)
|
||||
val total = charger.chargepoints.sumBy { it.count }
|
||||
val total = charger.chargepoints.sumOf { it.count }
|
||||
|
||||
if (text.isNotEmpty()) text.append(" · ")
|
||||
text.append(
|
||||
@@ -362,6 +369,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
this.location = location
|
||||
if (previousLocation == null) {
|
||||
loadChargers()
|
||||
return
|
||||
}
|
||||
|
||||
val now = Instant.now()
|
||||
@@ -372,6 +380,23 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
// update displayed distances
|
||||
invalidate()
|
||||
}
|
||||
|
||||
// if chargers are searched around current location, consider app-driven refresh
|
||||
val searchLocation =
|
||||
if (prefs.placeSearchResultAndroidAuto == null) searchLocation else null
|
||||
val distance = searchLocation?.let {
|
||||
distanceBetween(
|
||||
it.latitude, it.longitude, location.latitude, location.longitude
|
||||
)
|
||||
} ?: 0.0
|
||||
if (supportsRefresh && (lastChargersUpdateTime == null ||
|
||||
Duration.between(
|
||||
lastChargersUpdateTime,
|
||||
now
|
||||
) > chargersUpdateThresholdTime) && (distance > chargersUpdateThresholdDistance)
|
||||
) {
|
||||
onContentRefreshRequested()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadChargers() {
|
||||
@@ -411,13 +436,17 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
).awaitFinished()
|
||||
if (response.status == Status.ERROR) {
|
||||
loadingError = true
|
||||
this@MapScreen.chargers = null
|
||||
invalidate()
|
||||
return@launch
|
||||
}
|
||||
chargers = headingFilter(
|
||||
response.data?.filterIsInstance(ChargeLocation::class.java),
|
||||
searchLocation
|
||||
)
|
||||
chargers = response.data?.filterIsInstance(ChargeLocation::class.java)
|
||||
if (prefs.placeSearchResultAndroidAutoName == null) {
|
||||
chargers = headingFilter(
|
||||
chargers,
|
||||
searchLocation
|
||||
)
|
||||
}
|
||||
if (chargers == null || chargers.size >= maxRows) {
|
||||
break
|
||||
}
|
||||
@@ -426,6 +455,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
}
|
||||
|
||||
updateCoroutine = null
|
||||
lastChargersUpdateTime = Instant.now()
|
||||
lastDistanceUpdateTime = Instant.now()
|
||||
invalidate()
|
||||
} catch (e: IOException) {
|
||||
@@ -441,8 +471,12 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
private fun headingFilter(
|
||||
chargers: List<ChargeLocation>?,
|
||||
searchLocation: LatLng
|
||||
): List<ChargeLocation>? =
|
||||
heading?.orientations?.value?.get(0)?.let { heading ->
|
||||
): List<ChargeLocation>? {
|
||||
// use compass heading if available, otherwise fall back to GPS
|
||||
val location = location
|
||||
val heading = heading?.orientations?.value?.get(0)
|
||||
?: if (location?.hasBearing() == true) location.bearing else null
|
||||
return heading?.let {
|
||||
if (!prefs.showChargersAheadAndroidAuto) return@let chargers
|
||||
|
||||
chargers?.filter {
|
||||
@@ -456,6 +490,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
abs(diff) < 30
|
||||
}
|
||||
} ?: chargers
|
||||
}
|
||||
|
||||
private fun onEnergyLevelUpdated(energyLevel: EnergyLevel) {
|
||||
val isUpdate = this.energyLevel == null
|
||||
|
||||
@@ -46,7 +46,7 @@ class PermissionScreen(
|
||||
}
|
||||
|
||||
private fun requestPermissions() {
|
||||
carContext.requestPermissions(permissions) { granted, rejected ->
|
||||
carContext.requestPermissions(permissions) { granted, _ ->
|
||||
if (granted.containsAll(permissions)) {
|
||||
screenManager.pop()
|
||||
} else {
|
||||
|
||||
@@ -105,15 +105,15 @@ class PlaceSearchScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
addText(text)
|
||||
}
|
||||
|
||||
setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
val placeDetails = getDetails(place.id)
|
||||
prefs.placeSearchResultAndroidAuto = placeDetails.latLng
|
||||
prefs.placeSearchResultAndroidAutoName =
|
||||
place.primaryText.toString()
|
||||
screenManager.popTo(MapScreen.MARKER)
|
||||
}
|
||||
setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
val placeDetails = getDetails(place.id)
|
||||
prefs.placeSearchResultAndroidAuto = placeDetails.latLng
|
||||
prefs.placeSearchResultAndroidAutoName =
|
||||
place.primaryText.toString()
|
||||
screenManager.popTo(MapScreen.MARKER)
|
||||
}
|
||||
}
|
||||
}.build())
|
||||
@@ -148,6 +148,7 @@ class PlaceSearchScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx
|
||||
}
|
||||
|
||||
private suspend fun loadNewList(query: String) {
|
||||
val location = location?.let { LatLng.fromLocation(it) }
|
||||
for (provider in providers) {
|
||||
try {
|
||||
recentResults.clear()
|
||||
@@ -161,7 +162,7 @@ class PlaceSearchScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx
|
||||
}
|
||||
recentResults.addAll(recentPlaces)
|
||||
resultList =
|
||||
recentPlaces.map { it.asAutocompletePlace(LatLng.fromLocation(location)) }
|
||||
recentPlaces.map { it.asAutocompletePlace(location) }
|
||||
invalidate()
|
||||
|
||||
// if we already have enough results or the query is short, stop here
|
||||
@@ -170,7 +171,7 @@ class PlaceSearchScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx
|
||||
// then search online
|
||||
val recentIds = recentPlaces.map { it.id }
|
||||
resultList = withContext(Dispatchers.IO) {
|
||||
(resultList!! + provider.autocomplete(query, LatLng.fromLocation(location))
|
||||
(resultList!! + provider.autocomplete(query, location)
|
||||
.filter { !recentIds.contains(it.id) }).take(maxItems)
|
||||
}
|
||||
invalidate()
|
||||
|
||||
@@ -15,9 +15,7 @@ abstract class MultiSelectSearchScreen<T>(ctx: CarContext) : Screen(ctx),
|
||||
protected var fullList: List<T>? = null
|
||||
private var currentList: List<T> = emptyList()
|
||||
private var query: String = ""
|
||||
private val maxRows = if (ctx.carAppApiLevel >= 2) {
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
} else 6
|
||||
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
protected abstract val isMultiSelect: Boolean
|
||||
protected abstract val shouldShowSelectAll: Boolean
|
||||
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorManager
|
||||
import androidx.annotation.StringRes
|
||||
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.model.*
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.*
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceTariff
|
||||
@@ -18,7 +24,8 @@ import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class SettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
@ExperimentalCarApi
|
||||
class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
val prefs = PreferenceDataSource(ctx)
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
@@ -71,7 +78,7 @@ class SettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
)
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener {
|
||||
screenManager.push(VehicleDataScreen(carContext))
|
||||
screenManager.push(VehicleDataScreen(carContext, session))
|
||||
}
|
||||
.build()
|
||||
)
|
||||
@@ -81,9 +88,34 @@ class SettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
.setToggle(Toggle.Builder {
|
||||
prefs.showChargersAheadAndroidAuto = it
|
||||
}.setChecked(prefs.showChargersAheadAndroidAuto).build())
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_navigation
|
||||
)
|
||||
).setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.about))
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_about
|
||||
)
|
||||
).setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener {
|
||||
screenManager.push(AboutScreen(carContext))
|
||||
}
|
||||
.build()
|
||||
)
|
||||
}.build())
|
||||
}.build()
|
||||
}
|
||||
@@ -239,9 +271,7 @@ class ChooseDataSourceScreen(
|
||||
|
||||
class ChargepriceSettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
val prefs = PreferenceDataSource(carContext)
|
||||
private val maxRows = if (ctx.carAppApiLevel >= 2) {
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
} else 6
|
||||
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
return ListTemplate.Builder().apply {
|
||||
@@ -556,4 +586,165 @@ class SelectChargingRangeScreen(ctx: CarContext) : Screen(ctx) {
|
||||
)
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
|
||||
class AboutScreen(ctx: CarContext) : Screen(ctx) {
|
||||
val prefs = PreferenceDataSource(ctx)
|
||||
var developerOptionsCounter = 0
|
||||
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
return ListTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.about))
|
||||
setHeaderAction(Action.BACK)
|
||||
addSectionedList(SectionedItemList.create(ItemList.Builder().apply {
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.version))
|
||||
.addText(BuildConfig.VERSION_NAME)
|
||||
.addText(
|
||||
carContext.getString(R.string.copyright) + " " + carContext.getString(
|
||||
R.string.copyright_summary
|
||||
)
|
||||
)
|
||||
.setBrowsable(prefs.developerModeEnabled)
|
||||
.setOnClickListener {
|
||||
if (!prefs.developerModeEnabled) {
|
||||
developerOptionsCounter += 1
|
||||
if (developerOptionsCounter >= 7) {
|
||||
prefs.developerModeEnabled = true
|
||||
invalidate()
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
carContext.getString(R.string.developer_mode_enabled),
|
||||
CarToast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
} else {
|
||||
screenManager.pushForResult(DeveloperOptionsScreen(carContext)) {
|
||||
developerOptionsCounter = 0
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
)
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.faq))
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
openUrl(carContext, carContext.getString(R.string.faq_link))
|
||||
}).build()
|
||||
)
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.donate))
|
||||
.addText(carContext.getString(R.string.donate_desc))
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
if (BuildConfig.FLAVOR_automotive == "automotive") {
|
||||
// we can't open the donation page on the phone in this case
|
||||
openUrl(carContext, carContext.getString(R.string.paypal_link))
|
||||
} else {
|
||||
val intent = Intent(carContext, MapsActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.putExtra(EXTRA_DONATE, true)
|
||||
carContext.startActivity(intent)
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.opened_on_phone,
|
||||
CarToast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}).build()
|
||||
)
|
||||
}.build(), carContext.getString(R.string.about)))
|
||||
addSectionedList(SectionedItemList.create(ItemList.Builder().apply {
|
||||
addItem(Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.twitter))
|
||||
.addText(carContext.getString(R.string.twitter_handle))
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
openUrl(carContext, carContext.getString(R.string.twitter_url))
|
||||
}).build()
|
||||
)
|
||||
if (maxRows > 6) {
|
||||
addItem(Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.goingelectric_forum))
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
openUrl(
|
||||
carContext,
|
||||
carContext.getString(R.string.goingelectric_forum_url)
|
||||
)
|
||||
}).build()
|
||||
)
|
||||
}
|
||||
}.build(), carContext.getString(R.string.contact)))
|
||||
addSectionedList(SectionedItemList.create(ItemList.Builder().apply {
|
||||
addItem(Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.github_link_title))
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
openUrl(carContext, carContext.getString(R.string.github_link))
|
||||
}).build()
|
||||
)
|
||||
addItem(Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.privacy))
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
openUrl(carContext, carContext.getString(R.string.privacy_link))
|
||||
}).build()
|
||||
)
|
||||
}.build(), carContext.getString(R.string.other)))
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
|
||||
class DeveloperOptionsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
val prefs = PreferenceDataSource(ctx)
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
return ListTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.developer_options))
|
||||
setHeaderAction(Action.BACK)
|
||||
setSingleList(ItemList.Builder().apply {
|
||||
addItem(
|
||||
Row.Builder().apply {
|
||||
setTitle("Car app API Level: ${carContext.carAppApiLevel}")
|
||||
val hostPackage = carContext.hostInfo?.packageName
|
||||
val hostVersion = hostPackage?.let {
|
||||
try {
|
||||
carContext.packageManager.getPackageInfoCompat(it).versionName
|
||||
} catch (e: NameNotFoundException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
addText("$hostPackage $hostVersion")
|
||||
if (BuildConfig.FLAVOR_automotive == "automotive") {
|
||||
addText(
|
||||
"Sensor list: ${
|
||||
(carContext.getSystemService(Context.SENSOR_SERVICE) as SensorManager).getSensorList(
|
||||
Sensor.TYPE_ALL
|
||||
).map { it.type }.joinToString(",")
|
||||
}"
|
||||
)
|
||||
}
|
||||
}.build()
|
||||
)
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.disable_developer_mode))
|
||||
setOnClickListener {
|
||||
prefs.developerModeEnabled = false
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
carContext.getString(R.string.developer_mode_disabled),
|
||||
CarToast.LENGTH_SHORT
|
||||
).show()
|
||||
screenManager.pop()
|
||||
}
|
||||
}.build())
|
||||
}.build())
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,25 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.hardware.common.CarUnit
|
||||
import androidx.car.app.model.*
|
||||
import androidx.car.app.versioning.CarAppApiLevels
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.getPackageInfoCompat
|
||||
import java.util.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -33,6 +42,23 @@ fun carAvailabilityColor(status: List<ChargepointStatus>): CarColor {
|
||||
val CarContext.constraintManager
|
||||
get() = getCarService(CarContext.CONSTRAINT_SERVICE) as ConstraintManager
|
||||
|
||||
fun CarContext.getContentLimit(id: Int) = if (carAppApiLevel >= 2) {
|
||||
constraintManager.getContentLimit(id)
|
||||
} else {
|
||||
when (id) {
|
||||
ConstraintManager.CONTENT_LIMIT_TYPE_GRID -> 6
|
||||
ConstraintManager.CONTENT_LIMIT_TYPE_LIST -> 6
|
||||
ConstraintManager.CONTENT_LIMIT_TYPE_PANE -> 4
|
||||
ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST -> 6
|
||||
ConstraintManager.CONTENT_LIMIT_TYPE_ROUTE_LIST -> 3
|
||||
else -> throw IllegalArgumentException("unknown limit ID")
|
||||
}
|
||||
}
|
||||
|
||||
val CarContext.isAppDrivenRefreshSupported
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
get() = if (carAppApiLevel >= 6) constraintManager.isAppDrivenRefreshEnabled else false
|
||||
|
||||
fun Bitmap.asCarIcon(): CarIcon = CarIcon.Builder(IconCompat.createWithBitmap(this)).build()
|
||||
|
||||
val emptyCarIcon: CarIcon by lazy {
|
||||
@@ -171,7 +197,7 @@ fun <T> List<T>.paginate(nSingle: Int, nFirst: Int, nOther: Int, nLast: Int): Li
|
||||
}
|
||||
|
||||
fun getAndroidAutoVersion(ctx: Context): List<String> {
|
||||
val info = ctx.packageManager.getPackageInfo("com.google.android.projection.gearhead", 0)
|
||||
val info = ctx.packageManager.getPackageInfoCompat("com.google.android.projection.gearhead", 0)
|
||||
return info.versionName.split(".")
|
||||
}
|
||||
|
||||
@@ -190,6 +216,40 @@ fun supportsCarApiLevel3(ctx: CarContext): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
fun openUrl(carContext: CarContext, url: String) {
|
||||
val intent = CustomTabsIntent.Builder()
|
||||
.setDefaultColorSchemeParams(
|
||||
CustomTabColorSchemeParams.Builder()
|
||||
.setToolbarColor(
|
||||
ContextCompat.getColor(
|
||||
carContext,
|
||||
R.color.colorPrimary
|
||||
)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.build().intent
|
||||
intent.data = Uri.parse(url)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
try {
|
||||
carContext.startActivity(intent)
|
||||
if (BuildConfig.FLAVOR_automotive != "automotive") {
|
||||
// only show the toast "opened on phone" if we're running on a phone
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.opened_on_phone,
|
||||
CarToast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.no_browser_app_found,
|
||||
CarToast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
class DummyReturnScreen(ctx: CarContext) : Screen(ctx) {
|
||||
/*
|
||||
Dummy screen to get around template refresh limitations.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.car.app.CarContext
|
||||
@@ -16,10 +17,13 @@ import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.ui.CompassNeedle
|
||||
import net.vonforst.evmap.ui.Gauge
|
||||
import net.vonforst.evmap.utils.formatDecimal
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class VehicleDataScreen(ctx: CarContext) : Screen(ctx), DefaultLifecycleObserver {
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class VehicleDataScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx),
|
||||
LocationAwareScreen, DefaultLifecycleObserver {
|
||||
private val carInfo =
|
||||
(ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager).carInfo
|
||||
private val carSensors = carContext.patchedCarSensors
|
||||
@@ -27,6 +31,7 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx), DefaultLifecycleObserver
|
||||
private var energyLevel: EnergyLevel? = null
|
||||
private var speed: Speed? = null
|
||||
private var heading: Compass? = null
|
||||
private var location: Location? = null
|
||||
private var gauge = Gauge((ctx.resources.displayMetrics.density * 128).roundToInt(), ctx)
|
||||
private var compass =
|
||||
CompassNeedle((ctx.resources.displayMetrics.density * 128).roundToInt(), ctx)
|
||||
@@ -70,7 +75,11 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx), DefaultLifecycleObserver
|
||||
val energyLevel = energyLevel
|
||||
val model = model
|
||||
val speed = speed
|
||||
val heading = heading
|
||||
val location = location
|
||||
|
||||
val compassHeading = heading?.orientations?.value?.get(0)
|
||||
val gpsHeading = if (location?.hasBearing() == true) location.bearing else null
|
||||
val heading = compassHeading ?: gpsHeading
|
||||
|
||||
return GridTemplate.Builder().apply {
|
||||
setTitle(
|
||||
@@ -192,17 +201,30 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx), DefaultLifecycleObserver
|
||||
if (heading == null) {
|
||||
setLoading(true)
|
||||
} else {
|
||||
val heading = heading.orientations.value
|
||||
if (heading != null) {
|
||||
setText(
|
||||
"${heading[0].roundToInt()}°"
|
||||
val headingSource =
|
||||
if (compassHeading != null) carContext.getString(R.string.compass) else carContext.getString(
|
||||
R.string.gps
|
||||
)
|
||||
|
||||
} else {
|
||||
setText(carContext.getString(R.string.auto_no_data))
|
||||
}
|
||||
setText("${heading.roundToInt()}° ($headingSource)")
|
||||
setImage(
|
||||
compass.draw(heading?.get(0)).asCarIcon()
|
||||
compass.draw(heading).asCarIcon()
|
||||
)
|
||||
}
|
||||
}.build())
|
||||
addItem(GridItem.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.coordinates))
|
||||
if (location == null) {
|
||||
setLoading(true)
|
||||
} else {
|
||||
val dms = location.formatDecimal(4)
|
||||
setText(dms)
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_location
|
||||
)
|
||||
).setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
}
|
||||
}.build())
|
||||
@@ -229,6 +251,7 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx), DefaultLifecycleObserver
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
setupListeners()
|
||||
session.mapScreen = this
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
@@ -253,6 +276,7 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx), DefaultLifecycleObserver
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
removeListeners()
|
||||
session.mapScreen = null
|
||||
}
|
||||
|
||||
private fun removeListeners() {
|
||||
@@ -269,4 +293,8 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx), DefaultLifecycleObserver
|
||||
it
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
override fun updateLocation(location: Location) {
|
||||
this.location = location
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import android.text.style.StyleSpan
|
||||
import com.car2go.maps.google.adapter.AnyMapAdapter
|
||||
import com.car2go.maps.util.SphericalUtil
|
||||
import com.google.android.gms.common.api.ApiException
|
||||
import com.google.android.gms.common.api.CommonStatusCodes
|
||||
import com.google.android.gms.maps.model.LatLng
|
||||
import com.google.android.gms.maps.model.LatLngBounds
|
||||
import com.google.android.gms.tasks.Tasks.await
|
||||
@@ -19,6 +20,7 @@ import com.google.android.libraries.places.api.net.FindAutocompletePredictionsRe
|
||||
import com.google.android.libraries.places.api.net.PlacesStatusCodes
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import net.vonforst.evmap.R
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.ExecutionException
|
||||
import kotlin.math.sqrt
|
||||
|
||||
@@ -58,6 +60,13 @@ class GooglePlacesAutocompleteProvider(val context: Context) : AutocompleteProvi
|
||||
if (cause is ApiException) {
|
||||
if (cause.statusCode == PlacesStatusCodes.OVER_QUERY_LIMIT) {
|
||||
throw ApiUnavailableException()
|
||||
} else if (cause.statusCode in listOf(
|
||||
CommonStatusCodes.NETWORK_ERROR,
|
||||
CommonStatusCodes.TIMEOUT, CommonStatusCodes.RECONNECTION_TIMED_OUT,
|
||||
CommonStatusCodes.RECONNECTION_TIMED_OUT_DURING_UPDATE
|
||||
)
|
||||
) {
|
||||
throw IOException(cause)
|
||||
}
|
||||
}
|
||||
throw e
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.android.billingclient.api.*
|
||||
import com.android.billingclient.api.BillingClient.ProductType
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
|
||||
@@ -24,10 +25,15 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
|
||||
loadProducts()
|
||||
|
||||
// consume pending purchases
|
||||
val purchases = billingClient.queryPurchases(BillingClient.SkuType.INAPP)
|
||||
purchases.purchasesList?.forEach {
|
||||
if (!it.isAcknowledged) {
|
||||
consumePurchase(it.purchaseToken, false)
|
||||
billingClient.queryPurchasesAsync(
|
||||
QueryPurchasesParams.newBuilder()
|
||||
.setProductType(ProductType.INAPP)
|
||||
.build()
|
||||
) { _, purchasesList ->
|
||||
purchasesList.forEach {
|
||||
if (!it.isAcknowledged) {
|
||||
consumePurchase(it.purchaseToken, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,26 +42,26 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
|
||||
}
|
||||
|
||||
private fun loadProducts() {
|
||||
val params = SkuDetailsParams.newBuilder()
|
||||
.setType(BillingClient.SkuType.INAPP)
|
||||
.setSkusList(
|
||||
listOf(
|
||||
"donate_1_eur", "donate_2_eur", "donate_5_eur", "donate_10_eur"
|
||||
) +
|
||||
if (BuildConfig.DEBUG) {
|
||||
listOf(
|
||||
"android.test.purchased", "android.test.canceled",
|
||||
"android.test.item_unavailable"
|
||||
)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
val productIds = listOf(
|
||||
"donate_1_eur", "donate_2_eur", "donate_5_eur", "donate_10_eur"
|
||||
) + if (BuildConfig.DEBUG) {
|
||||
listOf(
|
||||
"android.test.purchased", "android.test.canceled",
|
||||
"android.test.item_unavailable"
|
||||
)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
val params = QueryProductDetailsParams.newBuilder()
|
||||
.setProductList(productIds.map {
|
||||
QueryProductDetailsParams.Product.newBuilder().setProductType(ProductType.INAPP)
|
||||
.setProductId(it).build()
|
||||
})
|
||||
.build()
|
||||
billingClient.querySkuDetailsAsync(params) { result, details ->
|
||||
if (result.responseCode == BillingClient.BillingResponseCode.OK && details != null) {
|
||||
billingClient.queryProductDetailsAsync(params) { result, details ->
|
||||
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
|
||||
products.postValue(Resource.success(details
|
||||
.sortedBy { it.priceAmountMicros }
|
||||
.sortedBy { it.oneTimePurchaseOfferDetails!!.priceAmountMicros }
|
||||
.map { DonationItem(it) }
|
||||
))
|
||||
} else {
|
||||
@@ -97,7 +103,13 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
|
||||
|
||||
fun startPurchase(it: DonationItem, activity: Activity) {
|
||||
val flowParams = BillingFlowParams.newBuilder()
|
||||
.setSkuDetails(it.sku)
|
||||
.setProductDetailsParamsList(
|
||||
listOf(
|
||||
BillingFlowParams.ProductDetailsParams.newBuilder()
|
||||
.setProductDetails(it.product)
|
||||
.build()
|
||||
)
|
||||
)
|
||||
.build()
|
||||
val response = billingClient.launchBillingFlow(activity, flowParams)
|
||||
if (response.responseCode != BillingClient.BillingResponseCode.OK) {
|
||||
@@ -110,4 +122,4 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
|
||||
}
|
||||
}
|
||||
|
||||
data class DonationItem(val sku: SkuDetails) : Equatable
|
||||
data class DonationItem(val product: ProductDetails) : Equatable
|
||||
@@ -28,7 +28,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@{item.sku.title}"
|
||||
android:text="@{item.product.title}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/textView21"
|
||||
@@ -41,7 +41,7 @@
|
||||
android:id="@+id/textView21"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{item.sku.price}"
|
||||
android:text="@{item.product.oneTimePurchaseOfferDetails.formattedPrice}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
||||
@@ -36,4 +36,6 @@
|
||||
<string name="sounds_cool">den er grei</string>
|
||||
<string name="auto_chargers_ahead">Kun ladere i kjøreretningen</string>
|
||||
<string name="loading">Laster inn …</string>
|
||||
<string name="auto_multipage_goto">Side %d</string>
|
||||
<string name="auto_multipage">(%d/%d)</string>
|
||||
</resources>
|
||||
@@ -40,6 +40,7 @@ const val EXTRA_CHARGER_ID = "chargerId"
|
||||
const val EXTRA_LAT = "lat"
|
||||
const val EXTRA_LON = "lon"
|
||||
const val EXTRA_FAVORITES = "favorites"
|
||||
const val EXTRA_DONATE = "donate"
|
||||
|
||||
class MapsActivity : AppCompatActivity(),
|
||||
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
|
||||
@@ -75,7 +76,7 @@ class MapsActivity : AppCompatActivity(),
|
||||
val navView = findViewById<NavigationView>(R.id.nav_view)
|
||||
navView.setupWithNavController(navController)
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(navView) { v, insets ->
|
||||
ViewCompat.setOnApplyWindowInsetsListener(navView) { _, insets ->
|
||||
val header = navView.getHeaderView(0)
|
||||
header.setPadding(0, insets.getInsets(WindowInsetsCompat.Type.statusBars()).top, 0, 0)
|
||||
insets
|
||||
@@ -188,6 +189,11 @@ class MapsActivity : AppCompatActivity(),
|
||||
.setGraph(navGraph)
|
||||
.setDestination(R.id.favs)
|
||||
.createPendingIntent()
|
||||
} else if (intent.hasExtra(EXTRA_DONATE)) {
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(navGraph)
|
||||
.setDestination(R.id.donate)
|
||||
.createPendingIntent()
|
||||
}
|
||||
|
||||
deepLink?.send()
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package net.vonforst.evmap
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Typeface
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.*
|
||||
import android.text.style.StyleSpan
|
||||
@@ -72,7 +75,7 @@ fun max(a: Int?, b: Int?): Int? {
|
||||
* otherwise the non-null value or null
|
||||
*/
|
||||
return if (a != null && b != null) {
|
||||
max(a, b)
|
||||
kotlin.math.max(a, b)
|
||||
} else {
|
||||
a ?: b
|
||||
}
|
||||
@@ -88,4 +91,11 @@ const val meterPerFt = 0.3048
|
||||
|
||||
fun shouldUseImperialUnits(): Boolean {
|
||||
return Locale.getDefault().country in listOf("US", "GB", "MM", "LR")
|
||||
}
|
||||
}
|
||||
|
||||
fun PackageManager.getPackageInfoCompat(packageName: String, flags: Int = 0): PackageInfo =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags.toLong()))
|
||||
} else {
|
||||
@Suppress("DEPRECATION") getPackageInfo(packageName, flags)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
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
|
||||
@@ -166,7 +165,7 @@ class CheckableConnectorAdapter : DataBindingAdapter<Chargepoint>() {
|
||||
root.setOnClickListener {
|
||||
root.isChecked = true
|
||||
}
|
||||
root.setOnCheckedChangeListener { v: View, checked: Boolean ->
|
||||
root.setOnCheckedChangeListener { _, checked: Boolean ->
|
||||
if (checked) {
|
||||
checkedItem = holder.bindingAdapterPosition.takeIf { it != -1 }
|
||||
root.post {
|
||||
@@ -205,7 +204,7 @@ class CheckableChargepriceCarAdapter : DataBindingAdapter<ChargepriceCar>() {
|
||||
root.setOnClickListener {
|
||||
root.isChecked = true
|
||||
}
|
||||
root.setOnCheckedChangeListener { v: View, checked: Boolean ->
|
||||
root.setOnCheckedChangeListener { _, checked: Boolean ->
|
||||
if (checked && item != checkedItem) {
|
||||
checkedItem = item
|
||||
root.post {
|
||||
|
||||
@@ -10,6 +10,8 @@ import net.vonforst.evmap.model.ChargeCardId
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.OpeningHoursDays
|
||||
import net.vonforst.evmap.plus
|
||||
import net.vonforst.evmap.utils.formatDMS
|
||||
import net.vonforst.evmap.utils.formatDecimal
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
|
||||
@@ -25,7 +25,7 @@ class FilterProfilesAdapter(
|
||||
super.bind(holder, item)
|
||||
|
||||
val binding = holder.binding as ItemFilterProfileBinding
|
||||
binding.handle.setOnTouchListener { v, event ->
|
||||
binding.handle.setOnTouchListener { _, event ->
|
||||
if (event?.action == MotionEvent.ACTION_DOWN) {
|
||||
dragHelper.startDrag(holder)
|
||||
}
|
||||
|
||||
@@ -71,19 +71,19 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
|
||||
connectors: Map<Long, Pair<Double, String>>,
|
||||
chargepoints: List<Chargepoint>
|
||||
): Map<Chargepoint, Set<Long>> {
|
||||
var chargepoints = chargepoints
|
||||
var cpts = chargepoints
|
||||
|
||||
// iterate over each connector type
|
||||
val types = connectors.map { it.value.second }.distinct().toSet()
|
||||
val equivalentTypes = types.map { equivalentPlugTypes(it).plus(it) }.cartesianProduct()
|
||||
var geTypes = chargepoints.map { it.type }.distinct().toSet()
|
||||
var geTypes = cpts.map { it.type }.distinct().toSet()
|
||||
if (!equivalentTypes.any { it == geTypes } && geTypes.size > 1 && geTypes.contains(
|
||||
Chargepoint.SCHUKO
|
||||
)) {
|
||||
// If charger has household plugs and other plugs, try removing the household plugs
|
||||
// (common e.g. in Hamburg -> 2x Type 2 + 2x Schuko, but NM only lists Type 2)
|
||||
geTypes = geTypes.filter { it != Chargepoint.SCHUKO }.toSet()
|
||||
chargepoints = chargepoints.filter { it.type != Chargepoint.SCHUKO }
|
||||
cpts = cpts.filter { it.type != Chargepoint.SCHUKO }
|
||||
}
|
||||
if (!equivalentTypes.any { it == geTypes }) throw AvailabilityDetectorException("chargepoints do not match")
|
||||
return types.flatMap { type ->
|
||||
@@ -93,14 +93,14 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
|
||||
val powers = connsOfType.map { it.value.first }.distinct().sorted()
|
||||
// find corresponding powers in GE data
|
||||
val gePowers =
|
||||
chargepoints.filter { equivalentPlugTypes(it.type).any { it == type } }
|
||||
cpts.filter { equivalentPlugTypes(it.type).any { it == type } }
|
||||
.mapNotNull { it.power }.distinct().sorted()
|
||||
|
||||
// if the distinct number of powers is the same, try to match.
|
||||
if (powers.size == gePowers.size) {
|
||||
gePowers.zip(powers).map { (gePower, power) ->
|
||||
val chargepoint =
|
||||
chargepoints.find { equivalentPlugTypes(it.type).any { it == type } && it.power == gePower }!!
|
||||
cpts.find { equivalentPlugTypes(it.type).any { it == type } && it.power == gePower }!!
|
||||
val ids = connsOfType.filter { it.value.first == power }.keys
|
||||
if (chargepoint.count != ids.size) {
|
||||
throw AvailabilityDetectorException("chargepoints do not match")
|
||||
@@ -108,7 +108,7 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
|
||||
chargepoint to ids
|
||||
}
|
||||
} else if (powers.size == 1 && gePowers.size == 2
|
||||
&& chargepoints.sumOf { it.count } == connsOfType.size
|
||||
&& cpts.sumOf { it.count } == connsOfType.size
|
||||
) {
|
||||
// special case: dual charger(s) with load balancing
|
||||
// GoingElectric shows 2 different powers, NewMotion just one
|
||||
@@ -116,7 +116,7 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
|
||||
var i = 0
|
||||
gePowers.map { gePower ->
|
||||
val chargepoint =
|
||||
chargepoints.find { it.type in equivalentPlugTypes(type) && it.power == gePower }!!
|
||||
cpts.find { it.type in equivalentPlugTypes(type) && it.power == gePower }!!
|
||||
val ids = allIds.subList(i, i + chargepoint.count).toSet()
|
||||
i += chargepoint.count
|
||||
chargepoint to ids
|
||||
|
||||
@@ -51,7 +51,7 @@ interface ChargecloudApi {
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun create(client: OkHttpClient, baseUrl: String? = null): ChargecloudApi {
|
||||
fun create(client: OkHttpClient, baseUrl: String): ChargecloudApi {
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
.addConverterFactory(MoshiConverterFactory.create())
|
||||
|
||||
@@ -140,7 +140,7 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
|
||||
connectorStatus.forEach { (connector, statusStr, evseId) ->
|
||||
val id = connector.uid
|
||||
val power = connector.electricalProperties.getPower()
|
||||
val type = when (connector.connectorType.toLowerCase(Locale.ROOT)) {
|
||||
val type = when (connector.connectorType.lowercase(Locale.ROOT)) {
|
||||
"type3" -> Chargepoint.TYPE_3
|
||||
"type2" -> Chargepoint.TYPE_2_UNKNOWN
|
||||
"type1" -> Chargepoint.TYPE_1
|
||||
|
||||
@@ -14,13 +14,18 @@ import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface FronyxApi {
|
||||
private interface FronyxApiRetrofit {
|
||||
@GET("predictions/evse-id/{evseId}")
|
||||
suspend fun getPredictionsForEvseId(
|
||||
@Path("evseId") evseId: String,
|
||||
@Query("timeframe") timeframe: Int? = null
|
||||
): FronyxEvseIdResponse
|
||||
|
||||
@GET("predictions/evses")
|
||||
suspend fun getPredictionsForEvseIds(
|
||||
@Query("evseIds", encoded = true) evseIds: String // comma-separated
|
||||
): List<FronyxEvseIdResponse>
|
||||
|
||||
companion object {
|
||||
private val cacheSize = 1L * 1024 * 1024 // 1MB
|
||||
|
||||
@@ -32,7 +37,7 @@ interface FronyxApi {
|
||||
apikey: String,
|
||||
baseurl: String = "https://api.fronyx.io/api/",
|
||||
context: Context? = null
|
||||
): FronyxApi {
|
||||
): FronyxApiRetrofit {
|
||||
val client = OkHttpClient.Builder().apply {
|
||||
addInterceptor { chain ->
|
||||
// add API key to every request
|
||||
@@ -56,9 +61,28 @@ interface FronyxApi {
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.client(client)
|
||||
.build()
|
||||
return retrofit.create(FronyxApi::class.java)
|
||||
return retrofit.create(FronyxApiRetrofit::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FronyxApi(
|
||||
apikey: String,
|
||||
baseurl: String = "https://api.fronyx.io/api/",
|
||||
context: Context? = null
|
||||
) {
|
||||
private val api = FronyxApiRetrofit.create(apikey, baseurl, context)
|
||||
|
||||
suspend fun getPredictionsForEvseId(
|
||||
evseId: String,
|
||||
timeframe: Int? = null
|
||||
): FronyxEvseIdResponse = api.getPredictionsForEvseId(evseId, timeframe)
|
||||
|
||||
suspend fun getPredictionsForEvseIds(
|
||||
evseIds: List<String>
|
||||
): List<FronyxEvseIdResponse> = api.getPredictionsForEvseIds(evseIds.joinToString(","))
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Checks if a chargepoint is supported by Fronyx.
|
||||
*
|
||||
@@ -6,7 +6,8 @@ import java.time.ZonedDateTime
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FronyxEvseIdResponse(
|
||||
val evseId: String,
|
||||
val predictions: List<FronyxPrediction>
|
||||
val predictions: List<FronyxPrediction>,
|
||||
val locationId: String?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
||||
@@ -71,10 +71,10 @@ internal class JsonObjectOrFalseAdapter<T> private constructor(
|
||||
private val clazz: Class<*>
|
||||
) : JsonAdapter<T>() {
|
||||
|
||||
class Factory() : JsonAdapter.Factory {
|
||||
class Factory : JsonAdapter.Factory {
|
||||
override fun create(
|
||||
type: Type,
|
||||
annotations: Set<Annotation>?,
|
||||
annotations: Set<Annotation>,
|
||||
moshi: Moshi
|
||||
): JsonAdapter<Any>? {
|
||||
val clazz = Types.getRawType(type)
|
||||
|
||||
@@ -399,10 +399,10 @@ class GoingElectricApiWrapper(
|
||||
referenceData: ReferenceData,
|
||||
sp: StringProvider
|
||||
): List<Filter<FilterValue>> {
|
||||
val referenceData = referenceData as GEReferenceData
|
||||
val plugs = referenceData.plugs
|
||||
val networks = referenceData.networks
|
||||
val chargeCards = referenceData.chargecards
|
||||
val refData = referenceData as GEReferenceData
|
||||
val plugs = refData.plugs
|
||||
val networks = refData.networks
|
||||
val chargeCards = refData.chargecards
|
||||
|
||||
val plugMap = plugs.map { plug ->
|
||||
plug to nameForPlugType(sp, GEChargepoint.convertTypeFromGE(plug))
|
||||
|
||||
@@ -120,7 +120,7 @@ class OpenChargeMapApiWrapper(
|
||||
zoom: Float,
|
||||
filters: FilterValues?,
|
||||
): Resource<List<ChargepointListItem>> {
|
||||
val referenceData = referenceData as OCMReferenceData
|
||||
val refData = referenceData as OCMReferenceData
|
||||
|
||||
val minPower = filters?.getSliderValue("min_power")?.toDouble()
|
||||
val minConnectors = filters?.getSliderValue("min_connectors")
|
||||
@@ -133,7 +133,7 @@ class OpenChargeMapApiWrapper(
|
||||
}
|
||||
val connectors = formatMultipleChoice(connectorsVal)
|
||||
|
||||
val operatorsVal = filters?.getMultipleChoiceValue("operators")!!
|
||||
val operatorsVal = filters?.getMultipleChoiceValue("operators")
|
||||
if (operatorsVal != null && operatorsVal.values.isEmpty() && !operatorsVal.all) {
|
||||
// no operators chosen
|
||||
return Resource.success(emptyList())
|
||||
@@ -160,7 +160,7 @@ class OpenChargeMapApiWrapper(
|
||||
minPower,
|
||||
connectorsVal,
|
||||
minConnectors,
|
||||
referenceData,
|
||||
refData,
|
||||
zoom
|
||||
)
|
||||
return Resource.success(result)
|
||||
@@ -176,7 +176,7 @@ class OpenChargeMapApiWrapper(
|
||||
zoom: Float,
|
||||
filters: FilterValues?
|
||||
): Resource<List<ChargepointListItem>> {
|
||||
val referenceData = referenceData as OCMReferenceData
|
||||
val refData = referenceData as OCMReferenceData
|
||||
|
||||
val minPower = filters?.getSliderValue("min_power")?.toDouble()
|
||||
val minConnectors = filters?.getSliderValue("min_connectors")
|
||||
@@ -214,7 +214,7 @@ class OpenChargeMapApiWrapper(
|
||||
minPower,
|
||||
connectorsVal,
|
||||
minConnectors,
|
||||
referenceData,
|
||||
refData,
|
||||
zoom
|
||||
)
|
||||
return Resource.success(result)
|
||||
@@ -254,11 +254,11 @@ class OpenChargeMapApiWrapper(
|
||||
referenceData: ReferenceData,
|
||||
id: Long
|
||||
): Resource<ChargeLocation> {
|
||||
val referenceData = referenceData as OCMReferenceData
|
||||
val refData = referenceData as OCMReferenceData
|
||||
try {
|
||||
val response = api.getChargepointDetail(id)
|
||||
if (response.isSuccessful && response.body()?.size == 1) {
|
||||
return Resource.success(response.body()!![0].convert(referenceData, true))
|
||||
return Resource.success(response.body()!![0].convert(refData, true))
|
||||
} else {
|
||||
return Resource.error(response.message(), null)
|
||||
}
|
||||
@@ -284,10 +284,10 @@ class OpenChargeMapApiWrapper(
|
||||
referenceData: ReferenceData,
|
||||
sp: StringProvider
|
||||
): List<Filter<FilterValue>> {
|
||||
val referenceData = referenceData as OCMReferenceData
|
||||
val refData = referenceData as OCMReferenceData
|
||||
|
||||
val operatorsMap = referenceData.operators.map { it.id.toString() to it.title }.toMap()
|
||||
val plugMap = referenceData.connectionTypes.map { it.id.toString() to it.title }.toMap()
|
||||
val operatorsMap = refData.operators.map { it.id.toString() to it.title }.toMap()
|
||||
val plugMap = refData.connectionTypes.map { it.id.toString() to it.title }.toMap()
|
||||
|
||||
return listOf(
|
||||
// supported by OCM API
|
||||
|
||||
@@ -167,7 +167,7 @@ class ChargepriceFragment : Fragment() {
|
||||
chargepriceAdapter.myTariffsAll = it
|
||||
}
|
||||
vm.chargePricesForChargepoint.observe(viewLifecycleOwner) {
|
||||
chargepriceAdapter.submitList(it.data)
|
||||
it?.data?.let { chargepriceAdapter.submitList(it) }
|
||||
}
|
||||
|
||||
val connectorsAdapter = CheckableConnectorAdapter()
|
||||
|
||||
@@ -44,8 +44,7 @@ class FavoritesFragment : Fragment() {
|
||||
private val vm: FavoritesViewModel by viewModels(factoryProducer = {
|
||||
viewModelFactory {
|
||||
FavoritesViewModel(
|
||||
requireActivity().application,
|
||||
getString(R.string.goingelectric_key)
|
||||
requireActivity().application
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -98,6 +98,12 @@ class FilterFragment : Fragment(), MenuProvider {
|
||||
saveProfile()
|
||||
true
|
||||
}
|
||||
R.id.menu_reset -> {
|
||||
lifecycleScope.launch {
|
||||
vm.resetValues()
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
@@ -114,7 +120,7 @@ class FilterFragment : Fragment(), MenuProvider {
|
||||
|
||||
dialog.setTitle(R.string.save_as_profile)
|
||||
.setMessage(R.string.save_profile_enter_name)
|
||||
.setPositiveButton(R.string.ok) { di, button ->
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
if (input.text.isBlank()) {
|
||||
saveProfile(true)
|
||||
} else {
|
||||
@@ -124,7 +130,7 @@ class FilterFragment : Fragment(), MenuProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { di, button ->
|
||||
.setNegativeButton(R.string.cancel) { _, _ ->
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,12 +188,12 @@ class FilterProfilesFragment : Fragment() {
|
||||
|
||||
dialog.setTitle(R.string.rename)
|
||||
.setMessage(R.string.save_profile_enter_name)
|
||||
.setPositiveButton(R.string.ok) { di, button ->
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
lifecycleScope.launch {
|
||||
vm.update(fp.copy(name = input.text.toString()))
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { di, button ->
|
||||
.setNegativeButton(R.string.cancel) { _, _ ->
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ import kotlin.collections.component1
|
||||
import kotlin.collections.component2
|
||||
import kotlin.collections.contains
|
||||
import kotlin.collections.set
|
||||
import kotlin.math.min
|
||||
|
||||
|
||||
class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallback, MenuProvider {
|
||||
@@ -197,7 +198,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(
|
||||
binding.root
|
||||
) { v, insets ->
|
||||
) { _, insets ->
|
||||
ViewCompat.onApplyWindowInsets(binding.root, insets)
|
||||
|
||||
val systemWindowInsetTop = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
|
||||
@@ -465,7 +466,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
)
|
||||
}
|
||||
binding.search.onFocusChangeListener = View.OnFocusChangeListener { v, hasFocus ->
|
||||
binding.search.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus ->
|
||||
if (hasFocus) {
|
||||
binding.search.keyListener = searchKeyListener
|
||||
binding.search.text = binding.search.text // workaround to fix copy/paste
|
||||
@@ -553,7 +554,17 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
bottomSheetBehavior.addBottomSheetCallback(object :
|
||||
BottomSheetBehaviorGoogleMapsLike.BottomSheetCallback() {
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||
|
||||
if (bottomSheetBehavior.state == STATE_HIDDEN) {
|
||||
map?.setPadding(0, mapTopPadding, 0, 0)
|
||||
} else {
|
||||
val height = binding.root.height - bottomSheet.top
|
||||
map?.setPadding(
|
||||
0,
|
||||
mapTopPadding,
|
||||
0,
|
||||
min(bottomSheetBehavior.peekHeight, height)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
@@ -561,9 +572,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
updateBackPressedCallback()
|
||||
|
||||
if (vm.layersMenuOpen.value!! && newState !in listOf(
|
||||
BottomSheetBehaviorGoogleMapsLike.STATE_SETTLING,
|
||||
BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN,
|
||||
BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
|
||||
STATE_SETTLING,
|
||||
STATE_HIDDEN,
|
||||
STATE_COLLAPSED
|
||||
)
|
||||
) {
|
||||
closeLayersMenu()
|
||||
@@ -1189,6 +1200,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
)
|
||||
popup.menuInflater.inflate(R.menu.popup_filter, popup.menu)
|
||||
MenuCompat.setGroupDividerEnabled(popup.menu, true)
|
||||
popup.setForceShowIcon(true)
|
||||
popup.setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.menu_edit_filters -> {
|
||||
|
||||
@@ -71,7 +71,7 @@ class MultiSelectDialog : MaterialDialogFragment() {
|
||||
binding.btnAll.visibility = if (showAllButton) View.VISIBLE else View.INVISIBLE
|
||||
|
||||
items = data.entries.toList()
|
||||
.sortedBy { it.value.toLowerCase(Locale.getDefault()) }
|
||||
.sortedBy { it.value.lowercase(Locale.getDefault()) }
|
||||
.sortedBy {
|
||||
when {
|
||||
selected.contains(it.key) && commonChoices?.contains(it.key) == true -> 0
|
||||
@@ -117,7 +117,7 @@ private fun search(
|
||||
): List<MultiSelectItem> {
|
||||
return items.filter { item ->
|
||||
// search for string within name
|
||||
text.toLowerCase(Locale.getDefault()) in item.name.toLowerCase(Locale.getDefault())
|
||||
text.lowercase(Locale.getDefault()) in item.name.lowercase(Locale.getDefault())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@ import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.floor
|
||||
|
||||
sealed class ChargepointListItem
|
||||
|
||||
@@ -113,7 +111,7 @@ data class ChargeLocation(
|
||||
|
||||
// check if there is more than one plug for any connector type
|
||||
val chargepointsPerConnector =
|
||||
connectors.map { conn -> chargepoints.filter { it.type == conn }.sumBy { it.count } }
|
||||
connectors.map { conn -> chargepoints.filter { it.type == conn }.sumOf { it.count } }
|
||||
return chargepointsPerConnector.any { it > 1 }
|
||||
}
|
||||
|
||||
@@ -129,13 +127,13 @@ data class ChargeLocation(
|
||||
return variants.map { variant ->
|
||||
val count = chargepoints
|
||||
.filter { it.type == variant.type && it.power == variant.power }
|
||||
.sumBy { it.count }
|
||||
.sumOf { it.count }
|
||||
Chargepoint(variant.type, variant.power, count)
|
||||
}
|
||||
}
|
||||
|
||||
val totalChargepoints: Int
|
||||
get() = chargepoints.sumBy { it.count }
|
||||
get() = chargepoints.sumOf { it.count }
|
||||
|
||||
fun formatChargepoints(sp: StringProvider): String {
|
||||
return chargepointsMerged.map {
|
||||
@@ -343,28 +341,7 @@ data class ChargeLocationCluster(
|
||||
) : ChargepointListItem()
|
||||
|
||||
@Parcelize
|
||||
data class Coordinate(val lat: Double, val lng: Double) : Parcelable {
|
||||
fun formatDMS(): String {
|
||||
return "${dms(lat, false)}, ${dms(lng, true)}"
|
||||
}
|
||||
|
||||
private fun dms(value: Double, lon: Boolean): String {
|
||||
val hemisphere = if (lon) {
|
||||
if (value >= 0) "E" else "W"
|
||||
} else {
|
||||
if (value >= 0) "N" else "S"
|
||||
}
|
||||
val d = abs(value)
|
||||
val degrees = floor(d).toInt()
|
||||
val minutes = floor((d - degrees) * 60).toInt()
|
||||
val seconds = ((d - degrees) * 60 - minutes) * 60
|
||||
return "%d°%02d'%02.1f\"%s".format(Locale.ENGLISH, degrees, minutes, seconds, hemisphere)
|
||||
}
|
||||
|
||||
fun formatDecimal(): String {
|
||||
return "%.6f, %.6f".format(Locale.ENGLISH, lat, lng)
|
||||
}
|
||||
}
|
||||
data class Coordinate(val lat: Double, val lng: Double) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class Address(
|
||||
|
||||
@@ -1,29 +1,49 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.lifecycle.liveData
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.room.*
|
||||
import net.vonforst.evmap.model.*
|
||||
|
||||
@Dao
|
||||
abstract class FilterValueDao {
|
||||
@Query("SELECT * FROM booleanfiltervalue WHERE profile = :profile AND dataSource = :dataSource")
|
||||
protected abstract suspend fun getBooleanFilterValues(
|
||||
protected abstract suspend fun getBooleanFilterValuesAsync(
|
||||
profile: Long,
|
||||
dataSource: String
|
||||
): List<BooleanFilterValue>
|
||||
|
||||
@Query("SELECT * FROM multiplechoicefiltervalue WHERE profile = :profile AND dataSource = :dataSource")
|
||||
protected abstract suspend fun getMultipleChoiceFilterValues(
|
||||
protected abstract suspend fun getMultipleChoiceFilterValuesAsync(
|
||||
profile: Long,
|
||||
dataSource: String
|
||||
): List<MultipleChoiceFilterValue>
|
||||
|
||||
@Query("SELECT * FROM sliderfiltervalue WHERE profile = :profile AND dataSource = :dataSource")
|
||||
protected abstract suspend fun getSliderFilterValues(
|
||||
protected abstract suspend fun getSliderFilterValuesAsync(
|
||||
profile: Long,
|
||||
dataSource: String
|
||||
): List<SliderFilterValue>
|
||||
|
||||
@Query("SELECT * FROM booleanfiltervalue WHERE profile = :profile AND dataSource = :dataSource")
|
||||
protected abstract fun getBooleanFilterValues(
|
||||
profile: Long,
|
||||
dataSource: String
|
||||
): LiveData<List<BooleanFilterValue>>
|
||||
|
||||
@Query("SELECT * FROM multiplechoicefiltervalue WHERE profile = :profile AND dataSource = :dataSource")
|
||||
protected abstract fun getMultipleChoiceFilterValues(
|
||||
profile: Long,
|
||||
dataSource: String
|
||||
): LiveData<List<MultipleChoiceFilterValue>>
|
||||
|
||||
@Query("SELECT * FROM sliderfiltervalue WHERE profile = :profile AND dataSource = :dataSource")
|
||||
protected abstract fun getSliderFilterValues(
|
||||
profile: Long,
|
||||
dataSource: String
|
||||
): LiveData<List<SliderFilterValue>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
protected abstract suspend fun insert(vararg values: BooleanFilterValue)
|
||||
|
||||
@@ -58,15 +78,32 @@ abstract class FilterValueDao {
|
||||
if (filterStatus == FILTERS_DISABLED || filterStatus == FILTERS_FAVORITES) {
|
||||
emptyList()
|
||||
} else {
|
||||
getBooleanFilterValues(filterStatus, dataSource) +
|
||||
getMultipleChoiceFilterValues(filterStatus, dataSource) +
|
||||
getSliderFilterValues(filterStatus, dataSource)
|
||||
getBooleanFilterValuesAsync(filterStatus, dataSource) +
|
||||
getMultipleChoiceFilterValuesAsync(filterStatus, dataSource) +
|
||||
getSliderFilterValuesAsync(filterStatus, dataSource)
|
||||
}
|
||||
|
||||
open fun getFilterValues(filterStatus: Long, dataSource: String) = liveData {
|
||||
emit(null)
|
||||
emit(getFilterValuesAsync(filterStatus, dataSource))
|
||||
}
|
||||
open fun getFilterValues(filterStatus: Long, dataSource: String): LiveData<List<FilterValue>?> =
|
||||
if (filterStatus == FILTERS_DISABLED || filterStatus == FILTERS_FAVORITES) {
|
||||
MutableLiveData(emptyList())
|
||||
} else {
|
||||
MediatorLiveData<List<FilterValue>?>().apply {
|
||||
value = null
|
||||
val sources = listOf(
|
||||
getBooleanFilterValues(filterStatus, dataSource),
|
||||
getMultipleChoiceFilterValues(filterStatus, dataSource),
|
||||
getSliderFilterValues(filterStatus, dataSource)
|
||||
)
|
||||
for (source in sources) {
|
||||
addSource(source) {
|
||||
val values = sources.map { it.value }
|
||||
if (values.all { it != null }) {
|
||||
value = values.filterNotNull().flatten()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Transaction
|
||||
open suspend fun insert(vararg values: FilterValue) {
|
||||
|
||||
@@ -255,4 +255,10 @@ class PreferenceDataSource(val context: Context) {
|
||||
|
||||
val predictionEnabled: Boolean
|
||||
get() = sp.getBoolean("prediction_enabled", true)
|
||||
|
||||
var developerModeEnabled: Boolean
|
||||
get() = sp.getBoolean("dev_mode_enabled", false)
|
||||
set(value) {
|
||||
sp.edit().putBoolean("dev_mode_enabled", value).apply()
|
||||
}
|
||||
}
|
||||
@@ -211,12 +211,12 @@ class BarGraphView(context: Context, attrs: AttributeSet) : View(context, attrs)
|
||||
private fun drawBubble(canvas: Canvas, data: SortedMap<ZonedDateTime, Int>, maxValue: Int) {
|
||||
val bubbleBounds = bubbleBounds ?: return
|
||||
val graphBounds = graphBounds ?: return
|
||||
val data = data.toList()
|
||||
val d = data.toList()
|
||||
|
||||
if (data.size <= selectedBar) return
|
||||
if (d.size <= selectedBar) return
|
||||
canvas.apply {
|
||||
val center = graphBounds.left + selectedBar * (barWidth + barMargin) + barWidth * 0.5f
|
||||
val (t, v) = data[selectedBar]
|
||||
val (t, v) = d[selectedBar]
|
||||
val tformat = context.getString(
|
||||
R.string.prediction_time_colon,
|
||||
t.withZoneSameInstant(ZoneId.systemDefault()).format(timeFormat)
|
||||
|
||||
@@ -121,6 +121,7 @@ private fun activeTint(
|
||||
}
|
||||
|
||||
@BindingAdapter("data")
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> setRecyclerViewData(recyclerView: RecyclerView, items: List<T>?) {
|
||||
if (recyclerView.adapter is ListAdapter<*, *>) {
|
||||
(recyclerView.adapter as ListAdapter<T, *>).submitList(items)
|
||||
@@ -128,6 +129,7 @@ fun <T> setRecyclerViewData(recyclerView: RecyclerView, items: List<T>?) {
|
||||
}
|
||||
|
||||
@BindingAdapter("data")
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> setRecyclerViewData(recyclerView: ViewPager2, items: List<T>?) {
|
||||
if (recyclerView.adapter is ListAdapter<*, *>) {
|
||||
(recyclerView.adapter as ListAdapter<T, *>).submitList(items)
|
||||
@@ -325,10 +327,10 @@ fun distance(meters: Number?): String? {
|
||||
}
|
||||
}
|
||||
|
||||
@InverseBindingAdapter(attribute = "app:values")
|
||||
@InverseBindingAdapter(attribute = "values")
|
||||
fun getRangeSliderValue(slider: RangeSlider) = slider.values
|
||||
|
||||
@BindingAdapter("app:valuesAttrChanged")
|
||||
@BindingAdapter("valuesAttrChanged")
|
||||
fun setRangeSliderListeners(slider: RangeSlider, attrChange: InverseBindingListener) {
|
||||
slider.addOnChangeListener { _, _, _ ->
|
||||
attrChange.onChange()
|
||||
@@ -348,7 +350,7 @@ fun colorEnabled(ctx: Context, enabled: Boolean): Int {
|
||||
return color
|
||||
}
|
||||
|
||||
@BindingAdapter("app:tint")
|
||||
@BindingAdapter("tint")
|
||||
fun setImageTintList(view: ImageView, @ColorInt color: Int) {
|
||||
view.imageTintList = ColorStateList.valueOf(color)
|
||||
}
|
||||
|
||||
@@ -89,12 +89,12 @@ class RangeSliderPreference(context: Context, attrs: AttributeSet) : Preference(
|
||||
slider.valueTo = valueTo
|
||||
stepSize?.let { slider.stepSize = it }
|
||||
|
||||
slider.addOnChangeListener { slider, value, fromUser ->
|
||||
slider.addOnChangeListener { slider, _, fromUser ->
|
||||
if (fromUser && (updatesContinuously || !dragging)) {
|
||||
syncValueInternal(slider)
|
||||
}
|
||||
}
|
||||
slider.setOnTouchListener { v, event ->
|
||||
slider.setOnTouchListener { _, event ->
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> dragging = true
|
||||
MotionEvent.ACTION_UP -> dragging = false
|
||||
|
||||
@@ -8,6 +8,8 @@ import android.location.Location
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import net.vonforst.evmap.model.Coordinate
|
||||
import java.util.*
|
||||
import kotlin.math.*
|
||||
|
||||
/**
|
||||
@@ -48,7 +50,7 @@ fun distanceBetween(
|
||||
|
||||
|
||||
fun bearingBetween(startLat: Double, startLng: Double, endLat: Double, endLng: Double): Double {
|
||||
val dLon = Math.toRadians(-endLng) - Math.toRadians(-startLng)
|
||||
val dLon = Math.toRadians(endLng) - Math.toRadians(startLng)
|
||||
val originLat = Math.toRadians(startLat)
|
||||
val destinationLat = Math.toRadians(endLat)
|
||||
|
||||
@@ -111,4 +113,33 @@ fun Context.checkAnyLocationPermission() = ContextCompat.checkSelfPermission(
|
||||
fun Context.checkFineLocationPermission() = ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
fun Coordinate.formatDMS(): String {
|
||||
return "${dms(lat, false)}, ${dms(lng, true)}"
|
||||
}
|
||||
|
||||
fun Location.formatDMS(): String {
|
||||
return "${dms(latitude, false)}, ${dms(longitude, true)}"
|
||||
}
|
||||
|
||||
private fun dms(value: Double, lon: Boolean): String {
|
||||
val hemisphere = if (lon) {
|
||||
if (value >= 0) "E" else "W"
|
||||
} else {
|
||||
if (value >= 0) "N" else "S"
|
||||
}
|
||||
val d = abs(value)
|
||||
val degrees = floor(d).toInt()
|
||||
val minutes = floor((d - degrees) * 60).toInt()
|
||||
val seconds = ((d - degrees) * 60 - minutes) * 60
|
||||
return "%d°%02d'%02.1f\"%s".format(Locale.ENGLISH, degrees, minutes, seconds, hemisphere)
|
||||
}
|
||||
|
||||
fun Coordinate.formatDecimal(accuracy: Int = 6): String {
|
||||
return "%.${accuracy}f, %.${accuracy}f".format(Locale.ENGLISH, lat, lng)
|
||||
}
|
||||
|
||||
fun Location.formatDecimal(accuracy: Int = 6): String {
|
||||
return "%.${accuracy}f, %.${accuracy}f".format(Locale.ENGLISH, latitude, longitude)
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import net.vonforst.evmap.model.FavoriteWithDetail
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
|
||||
class FavoritesViewModel(application: Application, geApiKey: String) :
|
||||
class FavoritesViewModel(application: Application) :
|
||||
AndroidViewModel(application) {
|
||||
private var db = AppDatabase.getInstance(application)
|
||||
|
||||
@@ -69,7 +69,7 @@ class FavoritesViewModel(application: Application, geApiKey: String) :
|
||||
FavoritesListItem(
|
||||
favorite,
|
||||
totalAvailable(charger.id),
|
||||
charger.chargepoints.sumBy { it.count },
|
||||
charger.chargepoints.sumOf { it.count },
|
||||
location.value.let { loc ->
|
||||
if (loc == null) null else {
|
||||
distanceBetween(
|
||||
|
||||
@@ -92,4 +92,8 @@ class FilterViewModel(application: Application) : AndroidViewModel(application)
|
||||
prefs.filterStatus = FILTERS_DISABLED
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun resetValues() {
|
||||
db.filterValueDao().deleteFilterValuesForProfile(FILTERS_CUSTOM, prefs.dataSource)
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import com.car2go.maps.AnyMap
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -229,7 +230,7 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
}
|
||||
}
|
||||
|
||||
val predictionApi = FronyxApi.create(application.getString(R.string.fronyx_key))
|
||||
val predictionApi = FronyxApi(application.getString(R.string.fronyx_key))
|
||||
|
||||
val prediction: LiveData<Resource<List<FronyxEvseIdResponse>>> by lazy {
|
||||
availability.switchMap { av ->
|
||||
@@ -249,14 +250,13 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
).any { filtered.contains(it) }
|
||||
} ?: true
|
||||
}.flatMap { it.value }
|
||||
|
||||
try {
|
||||
val result = allEvseIds.map {
|
||||
predictionApi.getPredictionsForEvseId(it)
|
||||
val result = predictionApi.getPredictionsForEvseIds(allEvseIds)
|
||||
if (result.size == allEvseIds.size) {
|
||||
emit(Resource.success(result))
|
||||
} else {
|
||||
emit(Resource.error("not all EVSEIDs found", null))
|
||||
}
|
||||
|
||||
emit(Resource.success(result))
|
||||
println(result)
|
||||
} catch (e: IOException) {
|
||||
emit(Resource.error(e.message, null))
|
||||
e.printStackTrace()
|
||||
@@ -266,6 +266,10 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
} catch (e: AvailabilityDetectorException) {
|
||||
emit(Resource.error(e.message, null))
|
||||
e.printStackTrace()
|
||||
} catch (e: JsonDataException) {
|
||||
// malformed JSON response from fronyx API
|
||||
emit(Resource.error(e.message, null))
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
} ?: liveData { emit(Resource.success(null)) }
|
||||
|
||||
@@ -13,7 +13,7 @@ import java.util.concurrent.atomic.AtomicBoolean
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <VM : ViewModel> viewModelFactory(crossinline f: () -> VM) =
|
||||
object : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(aClass: Class<T>): T = f() as T
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = f() as T
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@@ -106,7 +106,8 @@ fun <T> throttleLatest(
|
||||
}
|
||||
}
|
||||
|
||||
public suspend fun <T> LiveData<T>.await(): T {
|
||||
@ExperimentalCoroutinesApi
|
||||
suspend fun <T> LiveData<T>.await(): T {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
val observer = object : Observer<T> {
|
||||
override fun onChanged(value: T?) {
|
||||
@@ -124,7 +125,8 @@ public suspend fun <T> LiveData<T>.await(): T {
|
||||
}
|
||||
}
|
||||
|
||||
public suspend fun <T> LiveData<Resource<T>>.awaitFinished(): Resource<T> {
|
||||
@ExperimentalCoroutinesApi
|
||||
suspend fun <T> LiveData<Resource<T>>.awaitFinished(): Resource<T> {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
val observer = object : Observer<Resource<T>> {
|
||||
override fun onChanged(value: Resource<T>) {
|
||||
|
||||
10
app/src/main/res/drawable/ic_filter_no.xml
Normal file
10
app/src/main/res/drawable/ic_filter_no.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24"
|
||||
android:width="24dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M10.83,8H21V6H8.83L10.83,8zM15.83,13H18v-2h-4.17L15.83,13zM14,16.83V18h-4v-2h3.17l-3,-3H6v-2h2.17l-3,-3H3V6h0.17L1.39,4.22l1.41,-1.41l18.38,18.38l-1.41,1.41L14,16.83z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_manage_filter_profiles.xml
Normal file
10
app/src/main/res/drawable/ic_manage_filter_profiles.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24"
|
||||
android:width="24dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3,10h11v2H3V10zM3,8h11V6H3V8zM3,16h7v-2H3V16zM18.01,12.87l0.71,-0.71c0.39,-0.39 1.02,-0.39 1.41,0l0.71,0.71c0.39,0.39 0.39,1.02 0,1.41l-0.71,0.71L18.01,12.87zM17.3,13.58l-5.3,5.3V21h2.12l5.3,-5.3L17.3,13.58z" />
|
||||
</vector>
|
||||
@@ -230,6 +230,7 @@
|
||||
android:theme="@style/ThemeOverlay.Material3.Dark.ActionBar" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
style="@style/Widget.Material3.FloatingActionButton.Small.Surface"
|
||||
android:id="@+id/fab_layers"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -239,9 +240,7 @@
|
||||
android:layout_marginEnd="20dp"
|
||||
android:layout_marginTop="@dimen/layers_fab_top_padding"
|
||||
app:tint="?android:colorControlNormal"
|
||||
app:backgroundTint="?android:colorBackground"
|
||||
app:borderWidth="0dp"
|
||||
app:fabSize="mini"
|
||||
app:srcCompat="@drawable/ic_layers"
|
||||
app:layout_behavior="@string/hide_on_scroll_fab_behavior"
|
||||
android:theme="@style/NoElevationOverlay" />
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/menu_reset"
|
||||
android:title="@string/menu_reset"
|
||||
android:icon="@drawable/ic_filter_no"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_save_profile"
|
||||
android:title="@string/menu_save_profile"
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
<item
|
||||
android:id="@+id/menu_edit_filters"
|
||||
android:title="@string/menu_edit_filters"
|
||||
android:menuCategory="secondary" />
|
||||
android:menuCategory="secondary"
|
||||
android:icon="@drawable/ic_edit" />
|
||||
<item
|
||||
android:id="@+id/menu_manage_filter_profiles"
|
||||
android:title="@string/menu_manage_filter_profiles"
|
||||
android:menuCategory="secondary" />
|
||||
android:menuCategory="secondary"
|
||||
android:icon="@drawable/ic_manage_filter_profiles" />
|
||||
</menu>
|
||||
@@ -143,13 +143,14 @@
|
||||
<string name="category_caravan_site">Wohnmobilstellplatz</string>
|
||||
<string name="menu_apply">Filter anwenden</string>
|
||||
<string name="menu_save_profile">Als Profil speichern</string>
|
||||
<string name="menu_reset">Filter zurücksetzen</string>
|
||||
<string name="no_filters">Keine Filter</string>
|
||||
<string name="filter_custom">Verändertes Filterprofil</string>
|
||||
<string name="filter_favorites">Favoriten</string>
|
||||
<string name="reorder">Reihenfolge ändern</string>
|
||||
<string name="delete">Löschen</string>
|
||||
<string name="save_as_profile">Als Profil speichern</string>
|
||||
<string name="save_profile_enter_name">Geben Sie den Namen des Filterprofils ein:</string>
|
||||
<string name="save_profile_enter_name">Gib den Namen des Filterprofils ein:</string>
|
||||
<string name="filterprofiles_empty_state">Du hast keine Filterprofile gespeichert</string>
|
||||
<string name="welcome_to_evmap">Willkommen bei EVMap</string>
|
||||
<string name="welcome_1">Finde Ladestationen für Elektroautos in deiner Nähe</string>
|
||||
@@ -286,4 +287,12 @@
|
||||
<string name="data_source_switched_to">Datenquelle zu %s umgeschaltet</string>
|
||||
<string name="pref_applink_associate">Unterstützte Links öffnen</string>
|
||||
<string name="pref_applink_associate_summary">von goingelectric.de und openchargemap.org</string>
|
||||
<string name="chargeprice_header_my_tariffs">Meine Tarife</string>
|
||||
<string name="chargeprice_header_other_tariffs">Andere Tarife</string>
|
||||
<string name="developer_mode_enabled">Entwicklermodus aktiviert</string>
|
||||
<string name="developer_options">Entwicklereinstellungen</string>
|
||||
<string name="disable_developer_mode">Entwicklermodus deaktivieren</string>
|
||||
<string name="developer_mode_disabled">Entwicklermodus deaktiviert</string>
|
||||
<string name="gps">GPS</string>
|
||||
<string name="compass">Kompass</string>
|
||||
</resources>
|
||||
@@ -284,4 +284,16 @@
|
||||
<string name="pref_prediction_enabled">Vis bruksprognoser</string>
|
||||
<string name="pref_prediction_enabled_summary">for støttede ladere
|
||||
\n(foreløpig kun for likestrøm i Tyskland)</string>
|
||||
<string name="chargeprice_price_not_available">Pris ikke tilgjengelig</string>
|
||||
<string name="developer_mode_disabled">Utviklermodus avslått</string>
|
||||
<string name="gps">GPS</string>
|
||||
<string name="compass">Kompass</string>
|
||||
<string name="pref_applink_associate">Åpne støttede lenker</string>
|
||||
<string name="pref_applink_associate_summary">fra goingelectric.de og openchargemap.org</string>
|
||||
<string name="chargeprice_header_other_tariffs">Andre ladeabonnementer</string>
|
||||
<string name="disable_developer_mode">Skru av utviklermodus</string>
|
||||
<string name="chargeprice_header_my_tariffs">Mine ladeabonnementer</string>
|
||||
<string name="developer_options">Utvikleralternativer</string>
|
||||
<string name="data_source_switched_to">Datakilde byttet til %s</string>
|
||||
<string name="developer_mode_enabled">Utviklermodus påslått</string>
|
||||
</resources>
|
||||
@@ -23,4 +23,5 @@
|
||||
nautilusx
|
||||
</string>
|
||||
<string name="hide_on_scroll_fab_behavior">net.vonforst.evmap.ui.HideOnScrollFabBehavior</string>
|
||||
<string name="paypal_link" translatable="false">https://paypal.me/johan98</string>
|
||||
</resources>
|
||||
@@ -142,6 +142,7 @@
|
||||
<string name="category_caravan_site">Caravan site</string>
|
||||
<string name="menu_apply">Apply filters</string>
|
||||
<string name="menu_save_profile">Save as profile</string>
|
||||
<string name="menu_reset">Reset filter settings</string>
|
||||
<string name="no_filters">No filters</string>
|
||||
<string name="filter_custom">Modified filter</string>
|
||||
<string name="filter_favorites">Favorites</string>
|
||||
@@ -285,4 +286,12 @@
|
||||
<string name="data_source_switched_to">Data source switched to %s</string>
|
||||
<string name="pref_applink_associate">Open supported links</string>
|
||||
<string name="pref_applink_associate_summary">from goingelectric.de and openchargemap.org</string>
|
||||
<string name="chargeprice_header_my_tariffs">My plans</string>
|
||||
<string name="chargeprice_header_other_tariffs">Other plans</string>
|
||||
<string name="developer_mode_enabled">Developer mode enabled</string>
|
||||
<string name="developer_options">Developer options</string>
|
||||
<string name="disable_developer_mode">Disable developer mode</string>
|
||||
<string name="developer_mode_disabled">Developer mode disabled</string>
|
||||
<string name="gps">GPS</string>
|
||||
<string name="compass">Compass</string>
|
||||
</resources>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
(e.g. in the debug version). -->
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:targetPackage="${applicationId}"
|
||||
android:targetPackage="net.vonforst.evmap"
|
||||
android:targetClass="net.vonforst.evmap.MapsActivity">
|
||||
<extra
|
||||
android:name="favorites"
|
||||
|
||||
@@ -20,7 +20,7 @@ class FronyxApiTest {
|
||||
webServer.start()
|
||||
|
||||
val apikey = ""
|
||||
fronyx = FronyxApi.create(
|
||||
fronyx = FronyxApi(
|
||||
apikey,
|
||||
webServer.url("/").toString()
|
||||
)
|
||||
@@ -36,6 +36,14 @@ class FronyxApiTest {
|
||||
val id = segments[2]
|
||||
return okResponse("/fronyx/${id.replace("*", "_")}.json")
|
||||
}
|
||||
"predictions/evses" -> {
|
||||
val ids = request.requestUrl!!.queryParameter("evseIds")!!.split(",")
|
||||
return okResponse(
|
||||
"/fronyx/${
|
||||
ids.map { it.replace("*", "_") }.joinToString(",")
|
||||
}.json"
|
||||
)
|
||||
}
|
||||
else -> return notFoundResponse
|
||||
}
|
||||
}
|
||||
@@ -43,7 +51,7 @@ class FronyxApiTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun apiTest() {
|
||||
fun apiTestSingle() {
|
||||
val evseId = "DE*ION*E202102"
|
||||
|
||||
runBlocking {
|
||||
@@ -57,4 +65,25 @@ class FronyxApiTest {
|
||||
assertEquals(FronyxStatus.AVAILABLE, result.predictions[0].status)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun apiTestMultiple() {
|
||||
val evseIds = listOf("DE*ION*E202101", "DE*ION*E202102")
|
||||
|
||||
runBlocking {
|
||||
val results = fronyx.getPredictionsForEvseIds(evseIds)
|
||||
results.forEachIndexed { i, result ->
|
||||
assertEquals(result.evseId, evseIds[i])
|
||||
assertEquals(25, result.predictions.size)
|
||||
assertEquals(
|
||||
ZonedDateTime.of(2022, 11, 16, 18, 0, 0, 0, ZoneOffset.UTC),
|
||||
result.predictions[0].timestamp
|
||||
)
|
||||
assertEquals(
|
||||
if (i == 0) FronyxStatus.UNAVAILABLE else FronyxStatus.AVAILABLE,
|
||||
result.predictions[0].status
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
214
app/src/test/resources/fronyx/DE_ION_E202101,DE_ION_E202102.json
Normal file
214
app/src/test/resources/fronyx/DE_ION_E202101,DE_ION_E202102.json
Normal file
@@ -0,0 +1,214 @@
|
||||
[
|
||||
{
|
||||
"evseId": "DE*ION*E202101",
|
||||
"locationId": "DE-ION-03e2876e-0fd0-4e9d-abc1-d2caa1473947",
|
||||
"predictions": [
|
||||
{
|
||||
"timestamp": "2022-11-16T18:00:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T18:15:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T18:30:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T18:45:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T19:00:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T19:15:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T19:30:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T19:45:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T20:00:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T20:15:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T20:30:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T20:45:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T21:00:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T21:15:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T21:30:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T21:45:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T22:00:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T22:15:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T22:30:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T22:45:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T23:00:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T23:15:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T23:30:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T23:45:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-17T00:00:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"evseId": "DE*ION*E202102",
|
||||
"locationId": "DE-ION-03e2876e-0fd0-4e9d-abc1-d2caa1473947",
|
||||
"predictions": [
|
||||
{
|
||||
"timestamp": "2022-11-16T18:00:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T18:15:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T18:30:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T18:45:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T19:00:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T19:15:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T19:30:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T19:45:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T20:00:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T20:15:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T20:30:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T20:45:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T21:00:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T21:15:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T21:30:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T21:45:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T22:00:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T22:15:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T22:30:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T22:45:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T23:00:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T23:15:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T23:30:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T23:45:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-17T00:00:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,7 +1,7 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.7.10'
|
||||
ext.kotlin_version = '1.7.21'
|
||||
ext.about_libs_version = '8.9.4'
|
||||
ext.nav_version = '2.5.3'
|
||||
repositories {
|
||||
@@ -10,11 +10,10 @@ buildscript {
|
||||
gradlePluginPortal()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.3.1'
|
||||
classpath 'com.android.tools.build:gradle:7.4.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libs_version"
|
||||
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
|
||||
classpath "de.timfreiheit.resourceplaceholders:placeholders:0.4"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
||||
8
fastlane/metadata/android/de-DE/changelogs/146.txt
Normal file
8
fastlane/metadata/android/de-DE/changelogs/146.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
Verbesserungen:
|
||||
- Laden der Verfügbarkeitsprognosen beschleunigt
|
||||
- Android Auto: "Meine Tarife" im Preisvergleich hervorheben
|
||||
|
||||
Fehler behoben:
|
||||
- Android Automotive: Aktualisieren-Button fehlte
|
||||
- Darstellungsfehler nach dem Scrollen der Detailansicht behoben
|
||||
- Abstürze behoben
|
||||
9
fastlane/metadata/android/de-DE/changelogs/148.txt
Normal file
9
fastlane/metadata/android/de-DE/changelogs/148.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
Verbesserungen:
|
||||
- Laden der Verfügbarkeitsprognosen beschleunigt
|
||||
- Android Auto: "Meine Tarife" im Preisvergleich hervorheben
|
||||
|
||||
Fehler behoben:
|
||||
- Android Automotive: Aktualisieren-Button fehlte
|
||||
- Android Auto: Klick auf Suchergebnis funktioniert manchmal nicht
|
||||
- Darstellungsfehler nach dem Scrollen der Detailansicht behoben
|
||||
- Abstürze behoben
|
||||
5
fastlane/metadata/android/de-DE/changelogs/156.txt
Normal file
5
fastlane/metadata/android/de-DE/changelogs/156.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Verbesserungen:
|
||||
- Android Auto: Suchbutton während der Fahrt freigeschaltet (ggf. ohne Tastatur)
|
||||
|
||||
Fehler behoben:
|
||||
- Abstürze behoben
|
||||
7
fastlane/metadata/android/de-DE/changelogs/158.txt
Normal file
7
fastlane/metadata/android/de-DE/changelogs/158.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Verbesserungen:
|
||||
- Neuer Knopf zum Zurücksetzen der Filtereinstellungen
|
||||
- Filtermenü mit neuen Icons
|
||||
- Übersetzungen aktualisiert
|
||||
|
||||
Fehler behoben:
|
||||
- Abstürze behoben
|
||||
2
fastlane/metadata/android/de-DE/changelogs/160.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/160.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Fehler behoben:
|
||||
- Abstürze behoben
|
||||
8
fastlane/metadata/android/en-US/changelogs/146.txt
Normal file
8
fastlane/metadata/android/en-US/changelogs/146.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
Improvements:
|
||||
- Faster loading of availability prediction
|
||||
- Android Auto: highlight "my plans" in price comparison
|
||||
|
||||
Bugfixes:
|
||||
- Android Automotive: refresh button was missing
|
||||
- fixed visual bug after scrolling detail view
|
||||
- fixed crashes
|
||||
9
fastlane/metadata/android/en-US/changelogs/148.txt
Normal file
9
fastlane/metadata/android/en-US/changelogs/148.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
Improvements:
|
||||
- Faster loading of availability prediction
|
||||
- Android Auto: highlight "my plans" in price comparison
|
||||
|
||||
Bugfixes:
|
||||
- Android Automotive: refresh button was missing
|
||||
- Android Auto: Clicking search result sometimes not working
|
||||
- fixed visual bug after scrolling detail view
|
||||
- fixed crashes
|
||||
5
fastlane/metadata/android/en-US/changelogs/156.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/156.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Improvements:
|
||||
- Android Auto: Search button available while driving (possibly without keyboard)
|
||||
|
||||
Bugs fixed:
|
||||
- Fixed crashes
|
||||
7
fastlane/metadata/android/en-US/changelogs/158.txt
Normal file
7
fastlane/metadata/android/en-US/changelogs/158.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Improvements:
|
||||
- New button to reset filter setting
|
||||
- Filter menu with new icons
|
||||
- Updated translations
|
||||
|
||||
Bugfixes:
|
||||
- Fixed crashes
|
||||
2
fastlane/metadata/android/en-US/changelogs/160.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/160.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Bugfixes:
|
||||
- Fixed crashes
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
#Sat Aug 06 15:33:46 CEST 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
Reference in New Issue
Block a user