diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..58a75a66
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+_img/screenshots/**/*.png filter=lfs diff=lfs merge=lfs -text
+fastlane/metadata/android/**/images/**/*.png filter=lfs diff=lfs merge=lfs -text
diff --git a/README.md b/README.md
index 0bed6d19..4935db85 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@ Features
Screenshots
-----------
-
+
Development setup
-----------------
diff --git a/_img/screenshots/android_auto/de/11_android_auto_map.png b/_img/screenshots/android_auto/de/11_android_auto_map.png
new file mode 100644
index 00000000..304149b6
--- /dev/null
+++ b/_img/screenshots/android_auto/de/11_android_auto_map.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0731d286fe0dd41c068cba6b32b55c6560c2ce9e04f89837a91af4fb76c57861
+size 198603
diff --git a/_img/screenshots/android_auto/de/12_android_auto_detail.png b/_img/screenshots/android_auto/de/12_android_auto_detail.png
new file mode 100644
index 00000000..5ef5387a
--- /dev/null
+++ b/_img/screenshots/android_auto/de/12_android_auto_detail.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:63e95826a0206522c83ec61084715866076b19dd6d29812e7b50abb0ca248a58
+size 95959
diff --git a/_img/screenshots/android_auto/de/13_android_auto_prices.png b/_img/screenshots/android_auto/de/13_android_auto_prices.png
new file mode 100644
index 00000000..3f971af7
--- /dev/null
+++ b/_img/screenshots/android_auto/de/13_android_auto_prices.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:649ec77837aa0322583a83fbd675f62b771032aa3690a7de7bb5e4b4e97da31e
+size 90238
diff --git a/_img/screenshots/android_auto/de/14_vehicle_data.png b/_img/screenshots/android_auto/de/14_vehicle_data.png
new file mode 100644
index 00000000..4000ea76
--- /dev/null
+++ b/_img/screenshots/android_auto/de/14_vehicle_data.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dc3f077c912439554cd2e5bea86621985c50721aaa7ac445bc7d6cfb5b47bde8
+size 46030
diff --git a/_img/screenshots/android_auto/en/11_android_auto_map.png b/_img/screenshots/android_auto/en/11_android_auto_map.png
new file mode 100644
index 00000000..2d771735
--- /dev/null
+++ b/_img/screenshots/android_auto/en/11_android_auto_map.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:11de773f36770cbd0249dee34f965d1b6568d53bb73cc8671824be2bc82b294e
+size 248261
diff --git a/_img/screenshots/android_auto/en/12_android_auto_detail.png b/_img/screenshots/android_auto/en/12_android_auto_detail.png
new file mode 100644
index 00000000..55c3f359
--- /dev/null
+++ b/_img/screenshots/android_auto/en/12_android_auto_detail.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:575eb7389a2334e579457ab3aa939ce300862d4df137384cd68b9d279915930a
+size 91867
diff --git a/_img/screenshots/android_auto/en/13_android_auto_prices.png b/_img/screenshots/android_auto/en/13_android_auto_prices.png
new file mode 100644
index 00000000..676f3956
--- /dev/null
+++ b/_img/screenshots/android_auto/en/13_android_auto_prices.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:db0d2ca1156283aa0ae81a958994fe6224d91d8002bf50fbf362bcd56efd56eb
+size 90128
diff --git a/_img/screenshots/android_auto/en/14_vehicle_data.png b/_img/screenshots/android_auto/en/14_vehicle_data.png
new file mode 100644
index 00000000..44941762
--- /dev/null
+++ b/_img/screenshots/android_auto/en/14_vehicle_data.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bb0fe97239d8421babbf81a6828e37f154443c42472465c9e723f38eeff0cc2e
+size 42451
diff --git a/_img/screenshots/phone/01_main.png b/_img/screenshots/phone/01_main.png
deleted file mode 100644
index 3eb6b280..00000000
Binary files a/_img/screenshots/phone/01_main.png and /dev/null differ
diff --git a/_img/screenshots/phone/02_detail.png b/_img/screenshots/phone/02_detail.png
deleted file mode 100644
index a15d01bb..00000000
Binary files a/_img/screenshots/phone/02_detail.png and /dev/null differ
diff --git a/_img/screenshots/phone/11_android_auto_detail.png b/_img/screenshots/phone/11_android_auto_detail.png
deleted file mode 100644
index c94567b6..00000000
Binary files a/_img/screenshots/phone/11_android_auto_detail.png and /dev/null differ
diff --git a/_img/screenshots/phone/11_android_auto_map.png b/_img/screenshots/phone/11_android_auto_map.png
deleted file mode 100644
index 07cb6ee7..00000000
Binary files a/_img/screenshots/phone/11_android_auto_map.png and /dev/null differ
diff --git a/_img/screenshots/phone/de/google/01_map.png b/_img/screenshots/phone/de/google/01_map.png
new file mode 100644
index 00000000..d2185f8a
--- /dev/null
+++ b/_img/screenshots/phone/de/google/01_map.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e3c7ec5fad1b3c9ee419c4fd3f0f3595bfe20f8a169d96e7b1f1305bf3f1e51b
+size 1111142
diff --git a/_img/screenshots/phone/de/google/02_detail.png b/_img/screenshots/phone/de/google/02_detail.png
new file mode 100644
index 00000000..a32ca05a
--- /dev/null
+++ b/_img/screenshots/phone/de/google/02_detail.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2b82858437977781d4e33d7943a00f8bb42e5c2198a9d757346eb5bc1dc4aa4e
+size 995603
diff --git a/_img/screenshots/phone/de/google/03_prices.png b/_img/screenshots/phone/de/google/03_prices.png
new file mode 100644
index 00000000..cf22538c
--- /dev/null
+++ b/_img/screenshots/phone/de/google/03_prices.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2d9427caf64d0603f4ab24c90e7fb5469c3af880cb2eeeba2fc572ce5ca8aab7
+size 360777
diff --git a/_img/screenshots/phone/de/google/04_favorites.png b/_img/screenshots/phone/de/google/04_favorites.png
new file mode 100644
index 00000000..256211d4
--- /dev/null
+++ b/_img/screenshots/phone/de/google/04_favorites.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1f62adf4c68e939a8963f124c291ec7441ea05aa9b17835226a0a23555cd89ce
+size 83183
diff --git a/_img/screenshots/phone/de/google/05_filters.png b/_img/screenshots/phone/de/google/05_filters.png
new file mode 100644
index 00000000..990b9495
--- /dev/null
+++ b/_img/screenshots/phone/de/google/05_filters.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6ca6b6505c2b4401dfbca5add2fe6e092e77d917cc6ca1b6264f7b764dd5ff8c
+size 115657
diff --git a/_img/screenshots/phone/de/mapbox/01_map.png b/_img/screenshots/phone/de/mapbox/01_map.png
new file mode 100644
index 00000000..be1d0c75
--- /dev/null
+++ b/_img/screenshots/phone/de/mapbox/01_map.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4fec0a34114d957c75fe02cb7e972a752c704b41a162fcd61018fb94b2f51499
+size 895589
diff --git a/_img/screenshots/phone/de/mapbox/02_detail.png b/_img/screenshots/phone/de/mapbox/02_detail.png
new file mode 100644
index 00000000..cb504b52
--- /dev/null
+++ b/_img/screenshots/phone/de/mapbox/02_detail.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2d96236006f8a71429080ead33352b51cdc24dac997e13bb21cac9be33c88f01
+size 857431
diff --git a/_img/screenshots/phone/de/mapbox/03_prices.png b/_img/screenshots/phone/de/mapbox/03_prices.png
new file mode 100644
index 00000000..e91f346a
--- /dev/null
+++ b/_img/screenshots/phone/de/mapbox/03_prices.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4f1561eeceaaf11dfc513b3e94fdb87e33363aca6afdff99ed9058bb2549ea59
+size 348799
diff --git a/_img/screenshots/phone/de/mapbox/04_favorites.png b/_img/screenshots/phone/de/mapbox/04_favorites.png
new file mode 100644
index 00000000..24f2ee89
--- /dev/null
+++ b/_img/screenshots/phone/de/mapbox/04_favorites.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:94c75119d726f2926f788266e2bf1d2572079cdeb24a7721eec90b38d94b7d57
+size 96031
diff --git a/_img/screenshots/phone/de/mapbox/05_filters.png b/_img/screenshots/phone/de/mapbox/05_filters.png
new file mode 100644
index 00000000..9731c360
--- /dev/null
+++ b/_img/screenshots/phone/de/mapbox/05_filters.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:498c57fade7895b6c714088c489d829d44da75d8f69b8ecab6c8f998f74411b5
+size 137330
diff --git a/_img/screenshots/phone/en/google/01_map.png b/_img/screenshots/phone/en/google/01_map.png
new file mode 100644
index 00000000..37cf1812
--- /dev/null
+++ b/_img/screenshots/phone/en/google/01_map.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:92778f3df0ba65e46ddc2ea79dff544d45eee3b029d01b519b55a1c29c2cfb6b
+size 1096287
diff --git a/_img/screenshots/phone/en/google/02_detail.png b/_img/screenshots/phone/en/google/02_detail.png
new file mode 100644
index 00000000..a79a9f24
--- /dev/null
+++ b/_img/screenshots/phone/en/google/02_detail.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4f68bddaf0d4922934a6c28c8542c9ef4942930fadb0b2d4a70e5a5000b3a66a
+size 995021
diff --git a/_img/screenshots/phone/en/google/03_prices.png b/_img/screenshots/phone/en/google/03_prices.png
new file mode 100644
index 00000000..9f9910ec
--- /dev/null
+++ b/_img/screenshots/phone/en/google/03_prices.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bff00f4f84431c9e2d90a123abcc5b25938f188b47d46d07bc7015f622f794f4
+size 351111
diff --git a/_img/screenshots/phone/en/google/04_favorites.png b/_img/screenshots/phone/en/google/04_favorites.png
new file mode 100644
index 00000000..3e9167ec
--- /dev/null
+++ b/_img/screenshots/phone/en/google/04_favorites.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5bfb430a70fd66b2bf71dd48a2ce47419f9b2a269490235f062f6b6ec683bc47
+size 85445
diff --git a/_img/screenshots/phone/en/google/05_filters.png b/_img/screenshots/phone/en/google/05_filters.png
new file mode 100644
index 00000000..d556c625
--- /dev/null
+++ b/_img/screenshots/phone/en/google/05_filters.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7527bf765e8132223ca35021de4e4f5335fd220726d205da12d667fbd348772c
+size 108988
diff --git a/_img/screenshots/phone/en/mapbox/01_map.png b/_img/screenshots/phone/en/mapbox/01_map.png
new file mode 100644
index 00000000..b018a345
--- /dev/null
+++ b/_img/screenshots/phone/en/mapbox/01_map.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:aaa0032bd567bd9f6257bf17ca72915f973d676102c66d532d12309bc901cb5e
+size 884287
diff --git a/_img/screenshots/phone/en/mapbox/02_detail.png b/_img/screenshots/phone/en/mapbox/02_detail.png
new file mode 100644
index 00000000..5b0f4f0b
--- /dev/null
+++ b/_img/screenshots/phone/en/mapbox/02_detail.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:170fc47f88b2515f0b7d1c9b8e87c3fb905324ec32f6e25937f2fc241fbe1bbb
+size 857298
diff --git a/_img/screenshots/phone/en/mapbox/03_prices.png b/_img/screenshots/phone/en/mapbox/03_prices.png
new file mode 100644
index 00000000..68912eee
--- /dev/null
+++ b/_img/screenshots/phone/en/mapbox/03_prices.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:aaf780abbad2388002fc9afc9d6b1534061be9a6bab96bd3c166b3317d5332ff
+size 338280
diff --git a/_img/screenshots/phone/en/mapbox/04_favorites.png b/_img/screenshots/phone/en/mapbox/04_favorites.png
new file mode 100644
index 00000000..0e35823a
--- /dev/null
+++ b/_img/screenshots/phone/en/mapbox/04_favorites.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f5b58726094bb4f0d29229b729a3c369bf9f9b5ad7a9f355dbf20430b656e2ad
+size 97536
diff --git a/_img/screenshots/phone/en/mapbox/05_filters.png b/_img/screenshots/phone/en/mapbox/05_filters.png
new file mode 100644
index 00000000..c55b913d
--- /dev/null
+++ b/_img/screenshots/phone/en/mapbox/05_filters.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3b12d10d0eef40fabc9221bdc45460c7fe607ff9b254e59ecf92123413f3f22c
+size 124451
diff --git a/app/build.gradle b/app/build.gradle
index 6cdc6e3c..8c47b577 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -6,14 +6,14 @@ apply plugin: 'androidx.navigation.safeargs.kotlin'
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
android {
- compileSdkVersion 30
+ compileSdkVersion 31
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "net.vonforst.evmap"
minSdkVersion 21
targetSdkVersion 30
- versionCode 57
+ versionCode 58
versionName "0.9.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -136,7 +136,8 @@ dependencies {
implementation 'com.github.romandanylyk:PageIndicatorView:b1bad589b5'
// Android Auto
- googleImplementation 'androidx.car.app:app:1.0.0'
+ googleImplementation 'androidx.car.app:app:1.1.0-beta01'
+ googleImplementation 'androidx.car.app:app-projected:1.1.0-beta01'
// AnyMaps
def anyMapsVersion = '95ddd6c083'
diff --git a/app/src/google/AndroidManifest.xml b/app/src/google/AndroidManifest.xml
index 10a8b42b..bfed96af 100644
--- a/app/src/google/AndroidManifest.xml
+++ b/app/src/google/AndroidManifest.xml
@@ -5,8 +5,10 @@
+
+
-
+
+
+
-
-
\ No newline at end of file
diff --git a/app/src/google/java/net/vonforst/evmap/auto/CarAppService.kt b/app/src/google/java/net/vonforst/evmap/auto/CarAppService.kt
index a92ecb8b..d51d7632 100644
--- a/app/src/google/java/net/vonforst/evmap/auto/CarAppService.kt
+++ b/app/src/google/java/net/vonforst/evmap/auto/CarAppService.kt
@@ -6,22 +6,25 @@ import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.location.Location
import android.os.IBinder
+import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.Session
-import androidx.car.app.model.*
+import androidx.car.app.hardware.CarHardwareManager
+import androidx.car.app.hardware.info.CarHardwareLocation
+import androidx.car.app.hardware.info.CarSensors
import androidx.car.app.validation.HostValidator
+import androidx.car.app.versioning.CarAppApiLevels
import androidx.core.content.ContextCompat
-import androidx.lifecycle.*
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleObserver
+import androidx.lifecycle.OnLifecycleEvent
import androidx.localbroadcastmanager.content.LocalBroadcastManager
-import kotlinx.coroutines.*
-import net.vonforst.evmap.*
interface LocationAwareScreen {
fun updateLocation(location: Location)
}
-@androidx.car.app.annotations.ExperimentalCarApi
class CarAppService : androidx.car.app.CarAppService() {
override fun createHostValidator(): HostValidator {
return if (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0) {
@@ -38,7 +41,6 @@ class CarAppService : androidx.car.app.CarAppService() {
}
}
-@androidx.car.app.annotations.ExperimentalCarApi
class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
var mapScreen: LocationAwareScreen? = null
set(value) {
@@ -47,6 +49,9 @@ class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
}
private var location: Location? = null
private var locationService: CarLocationService? = null
+ private val hardwareMan: CarHardwareManager by lazy {
+ carContext.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
+ }
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, ibinder: IBinder) {
@@ -65,14 +70,10 @@ class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
}
override fun onCreateScreen(intent: Intent): Screen {
- return if (locationPermissionGranted()) {
- WelcomeScreen(carContext, this)
- } else {
- PermissionScreen(carContext, this)
- }
+ return WelcomeScreen(carContext, this)
}
- private fun locationPermissionGranted() =
+ fun locationPermissionGranted() =
ContextCompat.checkSelfPermission(
carContext,
Manifest.permission.ACCESS_FINE_LOCATION
@@ -81,29 +82,50 @@ class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
private val locationReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val location = intent.getParcelableExtra(CarLocationService.EXTRA_LOCATION) as Location?
- val mapScreen = this@EVMapSession.mapScreen
- if (location != null && mapScreen != null) {
- mapScreen.updateLocation(location)
- }
- this@EVMapSession.location = location
+ updateLocation(location)
}
}
+ private fun updateLocation(location: Location?) {
+ val mapScreen = mapScreen
+ if (location != null && mapScreen != null) {
+ mapScreen.updateLocation(location)
+ }
+ this.location = location
+ }
+
+ private fun onCarHardwareLocationReceived(loc: CarHardwareLocation) {
+ updateLocation(loc.location.value)
+ }
+
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun bindLocationService() {
if (!locationPermissionGranted()) return
- cas.bindService(
- Intent(cas, CarLocationService::class.java),
- serviceConnection,
- Context.BIND_AUTO_CREATE
- )
+ if (carContext.carAppApiLevel >= CarAppApiLevels.LEVEL_3) {
+ val exec = ContextCompat.getMainExecutor(carContext)
+ hardwareMan.carSensors.addCarHardwareLocationListener(
+ CarSensors.UPDATE_RATE_NORMAL,
+ exec,
+ ::onCarHardwareLocationReceived
+ )
+ } else {
+ cas.bindService(
+ Intent(cas, CarLocationService::class.java),
+ serviceConnection,
+ Context.BIND_AUTO_CREATE
+ )
+ }
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
private fun unbindLocationService() {
- locationService?.let { service ->
- service.removeLocationUpdates()
- cas.unbindService(serviceConnection)
+ if (carContext.carAppApiLevel >= CarAppApiLevels.LEVEL_3) {
+ locationService?.let { service ->
+ service.removeLocationUpdates()
+ cas.unbindService(serviceConnection)
+ }
+ } else {
+ hardwareMan.carSensors.removeCarHardwareLocationListener(::onCarHardwareLocationReceived)
}
}
diff --git a/app/src/google/java/net/vonforst/evmap/auto/ChargepriceScreen.kt b/app/src/google/java/net/vonforst/evmap/auto/ChargepriceScreen.kt
new file mode 100644
index 00000000..fd0384d9
--- /dev/null
+++ b/app/src/google/java/net/vonforst/evmap/auto/ChargepriceScreen.kt
@@ -0,0 +1,264 @@
+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
+import androidx.car.app.hardware.CarHardwareManager
+import androidx.car.app.hardware.info.Model
+import androidx.car.app.model.*
+import androidx.car.app.versioning.CarAppApiLevels
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.drawable.IconCompat
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.launch
+import moe.banana.jsonapi2.HasOne
+import net.vonforst.evmap.*
+import net.vonforst.evmap.api.chargeprice.*
+import net.vonforst.evmap.model.ChargeLocation
+import net.vonforst.evmap.storage.AppDatabase
+import net.vonforst.evmap.storage.PreferenceDataSource
+import net.vonforst.evmap.ui.currency
+
+class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(ctx) {
+ private val prefs = PreferenceDataSource(ctx)
+ private val db = AppDatabase.getInstance(carContext)
+ private val api by lazy {
+ ChargepriceApi.create(carContext.getString(R.string.chargeprice_key))
+ }
+ private var prices: List? = null
+ private var meta: ChargepriceChargepointMeta? = null
+ private val maxRows = 6
+ private var errorMessage: String? = null
+ private val batteryRange = listOf(20.0, 80.0)
+
+ override fun onGetTemplate(): Template {
+ if (prices == null) loadData()
+
+ return ListTemplate.Builder().apply {
+ setTitle(
+ carContext.getString(
+ R.string.chargeprice_battery_range,
+ batteryRange[0],
+ batteryRange[1]
+ ) + " · " + carContext.getString(R.string.powered_by_chargeprice)
+ )
+ setHeaderAction(Action.BACK)
+ if (prices == null && errorMessage == null) {
+ setLoading(true)
+ } else {
+ setSingleList(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))
+ }.build())
+ }
+ }.build())
+ }
+ setActionStrip(
+ ActionStrip.Builder().addAction(
+ Action.Builder().setIcon(
+ CarIcon.Builder(
+ IconCompat.createWithResource(
+ carContext,
+ R.drawable.ic_chargeprice
+ )
+ ).build()
+ ).setOnClickListener {
+ val intent = CustomTabsIntent.Builder()
+ .setDefaultColorSchemeParams(
+ CustomTabColorSchemeParams.Builder()
+ .setToolbarColor(
+ ContextCompat.getColor(
+ carContext,
+ R.color.colorPrimary
+ )
+ )
+ .build()
+ )
+ .build().intent
+ intent.data =
+ Uri.parse("https://www.chargeprice.app/?poi_id=${charger.id}&poi_source=${getDataAdapter()}")
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ try {
+ carContext.startActivity(intent)
+ 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()
+ }
+ }.build()
+ ).build()
+ )
+ }.build()
+ }
+
+ private fun formatProvider(price: ChargePrice): String {
+ if (!price.tariffName.startsWith(price.provider)) {
+ return price.provider + " " + price.tariffName
+ } else {
+ return price.tariffName
+ }
+ }
+
+ private fun formatPrice(price: ChargePrice): String {
+ val totalPrice = carContext.getString(
+ R.string.charge_price_format,
+ price.chargepointPrices.first().price,
+ currency(price.currency)
+ )
+ val kwhPrice = if (price.chargepointPrices.first().price > 0f) {
+ carContext.getString(
+ if (price.chargepointPrices[0].priceDistribution.isOnlyKwh) {
+ R.string.charge_price_kwh_format
+ } else {
+ R.string.charge_price_average_format
+ },
+ price.chargepointPrices.get(0).price / meta!!.energy,
+ currency(price.currency)
+ )
+ } else null
+ val monthlyFees = if (price.totalMonthlyFee > 0 || price.monthlyMinSales > 0) {
+ price.formatMonthlyFees(carContext)
+ } else null
+ var text = totalPrice
+ if (kwhPrice != null && monthlyFees != null) {
+ text += " ($kwhPrice, $monthlyFees)"
+ } else if (kwhPrice != null) {
+ text += " ($kwhPrice)"
+ } else if (monthlyFees != null) {
+ text += " ($monthlyFees)"
+ }
+ return text
+ }
+
+ private fun loadData() {
+ if (carContext.carAppApiLevel >= CarAppApiLevels.LEVEL_3) {
+ val exec = ContextCompat.getMainExecutor(carContext)
+ val hardwareMan =
+ carContext.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
+ hardwareMan.carInfo.fetchModel(exec) { model ->
+ loadPrices(model)
+ }
+ } else {
+ loadPrices(null)
+ }
+ }
+
+ private fun loadPrices(model: Model?) {
+ val dataAdapter = getDataAdapter() ?: return
+ val manufacturer = model?.manufacturer?.value
+ val modelName = model?.name?.value
+ lifecycleScope.launch {
+ var vehicles = api.getVehicles().filter {
+ it.id in prefs.chargepriceMyVehicles
+ }
+ if (vehicles.isEmpty()) {
+ errorMessage = carContext.getString(R.string.chargeprice_select_car_first)
+ invalidate()
+ return@launch
+ } else if (vehicles.size > 1) {
+ if (manufacturer != null && modelName != null) {
+ vehicles = vehicles.filter {
+ it.brand == manufacturer && it.name.startsWith(modelName)
+ }
+ if (vehicles.isEmpty()) {
+ errorMessage = carContext.getString(
+ R.string.auto_chargeprice_vehicle_unknown,
+ manufacturer,
+ modelName
+ )
+ invalidate()
+ return@launch
+ } else if (vehicles.size > 1) {
+ errorMessage = carContext.getString(
+ R.string.auto_chargeprice_vehicle_ambiguous,
+ manufacturer,
+ modelName
+ )
+ invalidate()
+ return@launch
+ }
+ } else {
+ errorMessage =
+ carContext.getString(R.string.auto_chargeprice_vehicle_unavailable)
+ invalidate()
+ return@launch
+ }
+ }
+ val car = vehicles[0]
+
+ val cpStation = ChargepriceStation.fromEvmap(charger, car.compatibleEvmapConnectors)
+ val result = api.getChargePrices(ChargepriceRequest().apply {
+ this.dataAdapter = dataAdapter
+ station = cpStation
+ vehicle = HasOne(car)
+ options = ChargepriceOptions(
+ batteryRange = batteryRange,
+ providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
+ maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
+ currency = prefs.chargepriceCurrency
+ )
+ }, ChargepriceApi.getChargepriceLanguage())
+
+ val myTariffs = prefs.chargepriceMyTariffs
+
+ // choose the highest power chargepoint compatible with the car
+ val chargepoint = cpStation.chargePoints.filterIndexed { i, cp ->
+ charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors
+ }.maxByOrNull { it.power }
+ if (chargepoint == null) {
+ errorMessage = carContext.getString(R.string.chargeprice_no_compatible_connectors)
+ invalidate()
+ return@launch
+ }
+ meta =
+ (result.meta.get(ChargepriceApi.moshi.adapter(ChargepriceMeta::class.java)) as ChargepriceMeta).chargePoints.filterIndexed { i, cp ->
+ charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors
+ }.maxByOrNull {
+ it.power
+ }
+
+ prices = result.map { cp ->
+ val filteredPrices =
+ cp.chargepointPrices.filter {
+ it.plug == chargepoint.plug && it.power == chargepoint.power
+ }
+ if (filteredPrices.isEmpty()) {
+ null
+ } else {
+ cp.clone().apply {
+ chargepointPrices = filteredPrices
+ }
+ }
+ }.filterNotNull()
+ .sortedBy { it.chargepointPrices.first().price }
+ .sortedByDescending {
+ prefs.chargepriceMyTariffsAll ||
+ myTariffs != null && it.tariff?.get()?.id in myTariffs
+ }
+ invalidate()
+ }
+ }
+
+ private fun getDataAdapter(): String? = when (charger.dataSource) {
+ "goingelectric" -> ChargepriceApi.DATA_SOURCE_GOINGELECTRIC
+ "openchargemap" -> ChargepriceApi.DATA_SOURCE_OPENCHARGEMAP
+ else -> null
+ }
+}
\ No newline at end of file
diff --git a/app/src/google/java/net/vonforst/evmap/auto/ChargerDetailScreen.kt b/app/src/google/java/net/vonforst/evmap/auto/ChargerDetailScreen.kt
index 60fbe9a7..e1e1c791 100644
--- a/app/src/google/java/net/vonforst/evmap/auto/ChargerDetailScreen.kt
+++ b/app/src/google/java/net/vonforst/evmap/auto/ChargerDetailScreen.kt
@@ -20,6 +20,7 @@ import kotlinx.coroutines.withContext
import net.vonforst.evmap.*
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.getAvailability
+import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.nameForPlugType
import net.vonforst.evmap.api.stringProvider
@@ -48,7 +49,10 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
}
private val referenceData = api.getReferenceData(lifecycleScope, carContext)
- private val iconGen = ChargerIconGenerator(carContext, null, oversize = 1.4f, height = 64)
+ private val imageSize = 128 // images should be 128dp according to docs
+
+ private val iconGen =
+ ChargerIconGenerator(carContext, null, oversize = 1.4f, height = imageSize)
init {
referenceData.observe(this) {
@@ -147,29 +151,49 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
navigateToCharger(charger)
}
.build())
- addAction(
- Action.Builder()
- .setTitle(carContext.getString(R.string.open_in_app))
- .setOnClickListener(ParkedOnlyOnClickListener.create {
- val intent = Intent(carContext, MapsActivity::class.java)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- .putExtra(EXTRA_CHARGER_ID, charger.id)
- .putExtra(EXTRA_LAT, charger.coordinates.lat)
- .putExtra(EXTRA_LON, charger.coordinates.lng)
- carContext.startActivity(intent)
- CarToast.makeText(
- carContext,
- R.string.opened_on_phone,
- CarToast.LENGTH_LONG
- ).show()
- })
- .build()
- )
+ charger.chargepriceData?.country?.let { country ->
+ if (ChargepriceApi.isCountrySupported(country, charger.dataSource)) {
+ addAction(Action.Builder()
+ .setIcon(
+ CarIcon.Builder(
+ IconCompat.createWithResource(
+ carContext,
+ R.drawable.ic_chargeprice
+ )
+ ).build()
+ )
+ .setTitle(carContext.getString(R.string.auto_prices))
+ .setOnClickListener {
+ screenManager.push(ChargepriceScreen(carContext, charger))
+ }
+ .build())
+ }
+ }
} ?: setLoading(true)
}.build()
).apply {
setTitle(chargerSparse.name)
setHeaderAction(Action.BACK)
+ setActionStrip(
+ ActionStrip.Builder().addAction(
+ Action.Builder()
+ .setTitle(carContext.getString(R.string.open_in_app))
+ .setOnClickListener(ParkedOnlyOnClickListener.create {
+ val intent = Intent(carContext, MapsActivity::class.java)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .putExtra(EXTRA_CHARGER_ID, chargerSparse.id)
+ .putExtra(EXTRA_LAT, chargerSparse.coordinates.lat)
+ .putExtra(EXTRA_LON, chargerSparse.coordinates.lng)
+ carContext.startActivity(intent)
+ CarToast.makeText(
+ carContext,
+ R.string.opened_on_phone,
+ CarToast.LENGTH_LONG
+ ).show()
+ })
+ .build()
+ ).build()
+ )
}.build()
}
diff --git a/app/src/google/java/net/vonforst/evmap/auto/FilterScreen.kt b/app/src/google/java/net/vonforst/evmap/auto/FilterScreen.kt
index cae3a35b..d6a51886 100644
--- a/app/src/google/java/net/vonforst/evmap/auto/FilterScreen.kt
+++ b/app/src/google/java/net/vonforst/evmap/auto/FilterScreen.kt
@@ -26,15 +26,11 @@ class FilterScreen(ctx: CarContext) : Screen(ctx) {
init {
val size = (ctx.resources.displayMetrics.density * 24).roundToInt()
- emptyIcon = CarIcon.Builder(
- IconCompat.createWithBitmap(
- Bitmap.createBitmap(
- size,
- size,
- Bitmap.Config.ARGB_8888
- )
- )
- ).build()
+ emptyIcon = Bitmap.createBitmap(
+ size,
+ size,
+ Bitmap.Config.ARGB_8888
+ ).asCarIcon()
}
init {
diff --git a/app/src/google/java/net/vonforst/evmap/auto/MapScreen.kt b/app/src/google/java/net/vonforst/evmap/auto/MapScreen.kt
index ccd8590b..230f9c19 100644
--- a/app/src/google/java/net/vonforst/evmap/auto/MapScreen.kt
+++ b/app/src/google/java/net/vonforst/evmap/auto/MapScreen.kt
@@ -6,6 +6,7 @@ import android.text.Spanned
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.model.*
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
@@ -39,12 +40,15 @@ import kotlin.math.roundToInt
/**
* Main map screen showing either nearby chargers or favorites
*/
-@androidx.car.app.annotations.ExperimentalCarApi
class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boolean = false) :
Screen(ctx), LocationAwareScreen {
private var updateCoroutine: Job? = null
private var numUpdates = 0
- private val maxNumUpdates = 3
+
+ /* Updating map contents is disabled - if the user uses Chargeprice from the charger
+ detail screen, this already means 4 steps, after which the app would crash.
+ follow https://issuetracker.google.com/issues/176694222 for updates how to solve this. */
+ private val maxNumUpdates = 1
private var location: Location? = null
private var lastUpdateLocation: Location? = null
@@ -59,7 +63,9 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
private val availabilityUpdateThreshold = Duration.ofMinutes(1)
private var availabilities: MutableMap> =
HashMap()
- private val maxRows = 6
+ private val maxRows = if (ctx.carAppApiLevel >= 2) {
+ ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST)
+ } else 6
private val referenceData = api.getReferenceData(lifecycleScope, carContext)
private val filterStatus = MutableLiveData().apply {
@@ -244,8 +250,8 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
numUpdates++
println(numUpdates)
if (numUpdates > maxNumUpdates) {
- CarToast.makeText(carContext, R.string.auto_no_refresh_possible, CarToast.LENGTH_LONG)
- .show()
+ /*CarToast.makeText(carContext, R.string.auto_no_refresh_possible, CarToast.LENGTH_LONG)
+ .show()*/
return
}
updateCoroutine = lifecycleScope.launch {
@@ -268,7 +274,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
)
chargers = response.data?.filterIsInstance(ChargeLocation::class.java)
chargers?.let {
- if (it.size < 6) {
+ if (it.size < maxRows) {
// try again with larger radius
val response = api.getChargepointsRadius(
referenceData,
diff --git a/app/src/google/java/net/vonforst/evmap/auto/PermissionActivity.kt b/app/src/google/java/net/vonforst/evmap/auto/PermissionActivity.kt
deleted file mode 100644
index 13a5a468..00000000
--- a/app/src/google/java/net/vonforst/evmap/auto/PermissionActivity.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-package net.vonforst.evmap.auto
-
-import android.Manifest
-import android.app.Activity
-import android.content.pm.PackageManager
-import android.os.Bundle
-import android.os.ResultReceiver
-import androidx.core.app.ActivityCompat
-import androidx.core.content.ContextCompat
-
-
-class PermissionActivity : Activity() {
- companion object {
- const val EXTRA_RESULT_RECEIVER = "result_receiver";
- const val RESULT_GRANTED = "granted"
- }
-
- private lateinit var resultReceiver: ResultReceiver
- private val permissions = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
- private val requestCode = 1
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- if (intent != null) {
- resultReceiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER)!!
- if (!hasPermissions(permissions)) {
- ActivityCompat.requestPermissions(this, permissions, requestCode)
- } else {
- onComplete(
- requestCode,
- permissions,
- intArrayOf(PackageManager.PERMISSION_GRANTED)
- )
- }
- } else {
- finish()
- }
- }
-
- private fun onComplete(requestCode: Int, permissions: Array?, grantResults: IntArray) {
- val bundle = Bundle()
- bundle.putBoolean(
- RESULT_GRANTED,
- grantResults.all { it == PackageManager.PERMISSION_GRANTED })
- resultReceiver.send(requestCode, bundle)
- finish()
- }
-
- private fun hasPermissions(permissions: Array): Boolean {
- var result = true
- for (permission in permissions) {
- if (ContextCompat.checkSelfPermission(
- this,
- permission
- ) != PackageManager.PERMISSION_GRANTED
- ) {
- result = false
- break
- }
- }
- return result
- }
-
- override fun onRequestPermissionsResult(
- requestCode: Int,
- permissions: Array,
- grantResults: IntArray
- ) {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults)
- onComplete(requestCode, permissions, grantResults)
- }
-}
\ No newline at end of file
diff --git a/app/src/google/java/net/vonforst/evmap/auto/PermissionScreen.kt b/app/src/google/java/net/vonforst/evmap/auto/PermissionScreen.kt
index 09517273..8ed475fb 100644
--- a/app/src/google/java/net/vonforst/evmap/auto/PermissionScreen.kt
+++ b/app/src/google/java/net/vonforst/evmap/auto/PermissionScreen.kt
@@ -1,21 +1,21 @@
package net.vonforst.evmap.auto
-import android.content.Intent
-import android.os.Bundle
-import android.os.ResultReceiver
+import androidx.annotation.StringRes
import androidx.car.app.CarContext
-import androidx.car.app.CarToast
import androidx.car.app.Screen
import androidx.car.app.model.*
import net.vonforst.evmap.R
/**
- * Screen to grant location permission
+ * Screen to grant permission
*/
-@androidx.car.app.annotations.ExperimentalCarApi
-class PermissionScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
+class PermissionScreen(
+ ctx: CarContext,
+ @StringRes val message: Int,
+ val permissions: List
+) : Screen(ctx) {
override fun onGetTemplate(): Template {
- return MessageTemplate.Builder(carContext.getString(R.string.auto_location_permission_needed))
+ return MessageTemplate.Builder(carContext.getString(message))
.setTitle(carContext.getString(R.string.app_name))
.setHeaderAction(Action.APP_ICON)
.addAction(
@@ -23,32 +23,7 @@ class PermissionScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx)
.setTitle(carContext.getString(R.string.grant_on_phone))
.setBackgroundColor(CarColor.PRIMARY)
.setOnClickListener(ParkedOnlyOnClickListener.create {
- val intent = Intent(carContext, PermissionActivity::class.java)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- .putExtra(
- PermissionActivity.EXTRA_RESULT_RECEIVER,
- object : ResultReceiver(null) {
- override fun onReceiveResult(
- resultCode: Int,
- resultData: Bundle?
- ) {
- if (resultData!!.getBoolean(PermissionActivity.RESULT_GRANTED)) {
- session.bindLocationService()
- screenManager.push(
- WelcomeScreen(
- carContext,
- session
- )
- )
- }
- }
- })
- carContext.startActivity(intent)
- CarToast.makeText(
- carContext,
- R.string.opened_on_phone,
- CarToast.LENGTH_LONG
- ).show()
+ requestPermissions()
})
.build()
)
@@ -62,4 +37,14 @@ class PermissionScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx)
)
.build()
}
+
+ private fun requestPermissions() {
+ carContext.requestPermissions(permissions) { granted, rejected ->
+ if (granted.containsAll(permissions)) {
+ screenManager.pop()
+ } else {
+ requestPermissions()
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/google/java/net/vonforst/evmap/auto/Utils.kt b/app/src/google/java/net/vonforst/evmap/auto/Utils.kt
index 47392f92..69b16723 100644
--- a/app/src/google/java/net/vonforst/evmap/auto/Utils.kt
+++ b/app/src/google/java/net/vonforst/evmap/auto/Utils.kt
@@ -1,7 +1,14 @@
package net.vonforst.evmap.auto
+import android.graphics.Bitmap
+import androidx.car.app.CarContext
+import androidx.car.app.constraints.ConstraintManager
+import androidx.car.app.hardware.common.CarUnit
import androidx.car.app.model.CarColor
+import androidx.car.app.model.CarIcon
+import androidx.core.graphics.drawable.IconCompat
import net.vonforst.evmap.api.availability.ChargepointStatus
+import java.util.*
fun carAvailabilityColor(status: List): CarColor {
val unknown = status.any { it == ChargepointStatus.UNKNOWN }
@@ -17,4 +24,48 @@ fun carAvailabilityColor(status: List): CarColor {
} else {
CarColor.BLUE
}
+}
+
+val CarContext.constraintManager
+ get() = getCarService(CarContext.CONSTRAINT_SERVICE) as ConstraintManager
+
+fun Bitmap.asCarIcon(): CarIcon = CarIcon.Builder(IconCompat.createWithBitmap(this)).build()
+
+private const val kmPerMile = 1.609344
+
+fun getDefaultDistanceUnit(): Int {
+ return when (Locale.getDefault().country) {
+ "US", "GB", "MM", "LR" -> CarUnit.MILE
+ else -> CarUnit.KILOMETER
+ }
+}
+
+fun getDefaultSpeedUnit(): Int {
+ return when (Locale.getDefault().country) {
+ "US", "GB", "MM", "LR" -> CarUnit.MILES_PER_HOUR
+ else -> CarUnit.KILOMETERS_PER_HOUR
+ }
+}
+
+fun formatCarUnitDistance(value: Float?, unit: Int?): String {
+ if (value == null) return ""
+ return when (unit ?: getDefaultDistanceUnit()) {
+ // distance units: base unit is meters
+ CarUnit.METER -> "%.0f m".format(value)
+ CarUnit.KILOMETER -> "%.1f km".format(value / 1000)
+ CarUnit.MILLIMETER -> "%.0f mm".format(value * 1000) // whoever uses that...
+ CarUnit.MILE -> "%.1f mi".format(value / 1000 / kmPerMile)
+ else -> ""
+ }
+}
+
+fun formatCarUnitSpeed(value: Float?, unit: Int?): String {
+ if (value == null) return ""
+ return when (unit ?: getDefaultSpeedUnit()) {
+ // speed units: base unit is meters per second
+ CarUnit.METERS_PER_SEC -> "%.0f m/s".format(value)
+ CarUnit.KILOMETERS_PER_HOUR -> "%.0f km/h".format(value * 3.6)
+ CarUnit.MILES_PER_HOUR -> "%.0f mph".format(value * 3.6 / kmPerMile)
+ else -> ""
+ }
}
\ No newline at end of file
diff --git a/app/src/google/java/net/vonforst/evmap/auto/VehicleDataScreen.kt b/app/src/google/java/net/vonforst/evmap/auto/VehicleDataScreen.kt
new file mode 100644
index 00000000..21be0af8
--- /dev/null
+++ b/app/src/google/java/net/vonforst/evmap/auto/VehicleDataScreen.kt
@@ -0,0 +1,215 @@
+package net.vonforst.evmap.auto
+
+import android.content.pm.PackageManager
+import android.os.Handler
+import android.os.Looper
+import androidx.car.app.CarContext
+import androidx.car.app.Screen
+import androidx.car.app.hardware.CarHardwareManager
+import androidx.car.app.hardware.info.EnergyLevel
+import androidx.car.app.hardware.info.Model
+import androidx.car.app.hardware.info.Speed
+import androidx.car.app.model.*
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.drawable.IconCompat
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleObserver
+import androidx.lifecycle.OnLifecycleEvent
+import net.vonforst.evmap.R
+import net.vonforst.evmap.ui.Gauge
+import kotlin.math.min
+import kotlin.math.roundToInt
+
+class VehicleDataScreen(ctx: CarContext) : Screen(ctx), LifecycleObserver {
+ private val hardwareMan = ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
+ private var model: Model? = null
+ private var energyLevel: EnergyLevel? = null
+ private var speed: Speed? = null
+ private var gauge = Gauge((ctx.resources.displayMetrics.density * 128).roundToInt(), ctx)
+ private val maxSpeed = 160f / 3.6f // m/s, speed gauge will show max if speed is higher
+
+ private val permissions = listOf(
+ "com.google.android.gms.permission.CAR_FUEL",
+ "com.google.android.gms.permission.CAR_SPEED"
+ )
+
+ init {
+ lifecycle.addObserver(this)
+ }
+
+ override fun onGetTemplate(): Template {
+ if (!permissionsGranted()) {
+ Handler(Looper.getMainLooper()).post {
+ screenManager.pushForResult(
+ PermissionScreen(
+ carContext,
+ R.string.auto_vehicle_data_permission_needed,
+ permissions
+ )
+ ) {
+ setupListeners()
+ }
+ }
+ }
+
+ val energyLevel = energyLevel
+ val model = model
+ val speed = speed
+
+ return GridTemplate.Builder().apply {
+ setTitle(
+ if (model != null && model.manufacturer.value != null && model.name.value != null) {
+ "${model.manufacturer.value} ${model.name.value}"
+ } else {
+ carContext.getString(R.string.auto_vehicle_data)
+ }
+ )
+ setHeaderAction(Action.BACK)
+ if (!permissionsGranted()) {
+ setLoading(true)
+ } else {
+ setSingleList(
+ ItemList.Builder().apply {
+ addItem(GridItem.Builder().apply {
+ setTitle(carContext.getString(R.string.auto_charging_level))
+ if (energyLevel == null) {
+ setLoading(true)
+ } else if (energyLevel.batteryPercent.value != null && energyLevel.fuelPercent.value != null) {
+ // both battery and fuel (Plug-in hybrid)
+ setText(
+ "\uD83D\uDD0C %.0f %% ⛽ %.0f %%".format(
+ energyLevel.batteryPercent.value,
+ energyLevel.fuelPercent.value
+ )
+ )
+ setImage(
+ gauge.draw(
+ energyLevel.batteryPercent.value,
+ energyLevel.fuelPercent.value
+ ).asCarIcon()
+ )
+ } else if (energyLevel.batteryPercent.value != null) {
+ // BEV
+ setText("%.0f %%".format(energyLevel.batteryPercent.value))
+ setImage(gauge.draw(energyLevel.batteryPercent.value).asCarIcon())
+ } else if (energyLevel.fuelPercent.value != null) {
+ // ICE
+ setText("⛽ %.0f %%".format(energyLevel.fuelPercent.value))
+ setImage(gauge.draw(energyLevel.fuelPercent.value).asCarIcon())
+ } else {
+ setText(carContext.getString(R.string.auto_no_data))
+ setImage(gauge.draw(0f).asCarIcon())
+ }
+ }.build())
+ addItem(GridItem.Builder().apply {
+ setTitle(carContext.getString(R.string.auto_range))
+ if (energyLevel == null) {
+ setLoading(true)
+ } else if (energyLevel.rangeRemainingMeters.value != null) {
+ setText(
+ formatCarUnitDistance(
+ energyLevel.rangeRemainingMeters.value,
+ energyLevel.distanceDisplayUnit.value
+ )
+ )
+ setImage(
+ CarIcon.Builder(
+ IconCompat.createWithResource(
+ carContext,
+ R.drawable.ic_car
+ )
+ ).build()
+ )
+ } else {
+ setText(carContext.getString(R.string.auto_no_data))
+ setImage(
+ CarIcon.Builder(
+ IconCompat.createWithResource(
+ carContext,
+ R.drawable.ic_car
+ )
+ ).build()
+ )
+ }
+ }.build())
+ addItem(GridItem.Builder().apply {
+ setTitle(carContext.getString(R.string.auto_speed))
+ if (speed == null) {
+ setLoading(true)
+ } else {
+ val rawSpeed = speed.rawSpeedMetersPerSecond.value
+ val displaySpeed = speed.displaySpeedMetersPerSecond.value
+ if (rawSpeed != null) {
+ setText(
+ formatCarUnitSpeed(
+ rawSpeed,
+ speed.speedDisplayUnit.value
+ )
+ )
+ setImage(
+ gauge.draw(min(rawSpeed / maxSpeed * 100, 100f)).asCarIcon()
+ )
+ } else if (displaySpeed != null) {
+ setText(
+ formatCarUnitSpeed(
+ speed.displaySpeedMetersPerSecond.value,
+ speed.speedDisplayUnit.value
+ )
+ )
+ setImage(
+ gauge.draw(min(displaySpeed / maxSpeed * 100, 100f))
+ .asCarIcon()
+ )
+ } else {
+ setText(carContext.getString(R.string.auto_no_data))
+ setImage(gauge.draw(0f).asCarIcon())
+ }
+ }
+ }.build())
+ }.build()
+ )
+ }
+ }.build()
+ }
+
+ private fun onEnergyLevelUpdated(energyLevel: EnergyLevel) {
+ this.energyLevel = energyLevel
+ invalidate()
+ }
+
+ private fun onSpeedUpdated(speed: Speed) {
+ this.speed = speed
+ invalidate()
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
+ private fun setupListeners() {
+ if (!permissionsGranted()) return
+
+ println("Setting up energy level listener")
+
+ val exec = ContextCompat.getMainExecutor(carContext)
+ hardwareMan.carInfo.addEnergyLevelListener(exec, ::onEnergyLevelUpdated)
+ hardwareMan.carInfo.addSpeedListener(exec, ::onSpeedUpdated)
+
+ hardwareMan.carInfo.fetchModel(exec) {
+ this.model = it
+ invalidate()
+ }
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
+ private fun removeListeners() {
+ println("Removing energy level listener")
+ hardwareMan.carInfo.removeEnergyLevelListener(::onEnergyLevelUpdated)
+ hardwareMan.carInfo.removeSpeedListener(::onSpeedUpdated)
+ }
+
+ private fun permissionsGranted(): Boolean =
+ permissions.all {
+ ContextCompat.checkSelfPermission(
+ carContext,
+ it
+ ) == PackageManager.PERMISSION_GRANTED
+ }
+}
\ No newline at end of file
diff --git a/app/src/google/java/net/vonforst/evmap/auto/WelcomeScreen.kt b/app/src/google/java/net/vonforst/evmap/auto/WelcomeScreen.kt
index bc2bc06f..65dd54d8 100644
--- a/app/src/google/java/net/vonforst/evmap/auto/WelcomeScreen.kt
+++ b/app/src/google/java/net/vonforst/evmap/auto/WelcomeScreen.kt
@@ -1,63 +1,107 @@
package net.vonforst.evmap.auto
+import android.Manifest
import android.location.Location
+import android.os.Handler
+import android.os.Looper
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.model.*
+import androidx.car.app.versioning.CarAppApiLevels
import androidx.core.graphics.drawable.IconCompat
import net.vonforst.evmap.R
/**
* Welcome screen with selection between favorites and nearby chargers
*/
-@androidx.car.app.annotations.ExperimentalCarApi
class WelcomeScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx), LocationAwareScreen {
private var location: Location? = null
override fun onGetTemplate(): Template {
+ if (!session.locationPermissionGranted()) {
+ Handler(Looper.getMainLooper()).post {
+ screenManager.pushForResult(
+ PermissionScreen(
+ carContext,
+ R.string.auto_location_permission_needed,
+ listOf(Manifest.permission.ACCESS_FINE_LOCATION)
+ )
+ ) {
+ session.bindLocationService()
+ }
+ }
+ }
+
session.mapScreen = this
return PlaceListMapTemplate.Builder().apply {
setTitle(carContext.getString(R.string.app_name))
- location?.let {
- setAnchor(Place.Builder(CarLocation.create(it)).build())
- }
- setItemList(ItemList.Builder().apply {
- addItem(
- Row.Builder()
- .setTitle(carContext.getString(R.string.auto_chargers_closeby))
- .setImage(
- CarIcon.Builder(
- IconCompat.createWithResource(
- carContext,
- R.drawable.ic_address
- )
- )
- .setTint(CarColor.DEFAULT).build()
- )
- .setBrowsable(true)
- .setOnClickListener {
- screenManager.push(MapScreen(carContext, session, favorites = false))
- }
- .build())
- addItem(
- Row.Builder()
- .setTitle(carContext.getString(R.string.auto_favorites))
- .setImage(
- CarIcon.Builder(
- IconCompat.createWithResource(
- carContext,
- R.drawable.ic_fav
+ if (!session.locationPermissionGranted()) {
+ setLoading(true)
+ } else {
+ location?.let {
+ setAnchor(Place.Builder(CarLocation.create(it)).build())
+ }
+ setItemList(ItemList.Builder().apply {
+ addItem(
+ Row.Builder()
+ .setTitle(carContext.getString(R.string.auto_chargers_closeby))
+ .setImage(
+ CarIcon.Builder(
+ IconCompat.createWithResource(
+ carContext,
+ R.drawable.ic_address
+ )
)
+ .setTint(CarColor.DEFAULT).build()
)
- .setTint(CarColor.DEFAULT).build()
+ .setBrowsable(true)
+ .setOnClickListener {
+ screenManager.push(
+ MapScreen(
+ carContext,
+ session,
+ favorites = false
+ )
+ )
+ }
+ .build())
+ addItem(
+ Row.Builder()
+ .setTitle(carContext.getString(R.string.auto_favorites))
+ .setImage(
+ CarIcon.Builder(
+ IconCompat.createWithResource(
+ carContext,
+ R.drawable.ic_fav
+ )
+ )
+ .setTint(CarColor.DEFAULT).build()
+ )
+ .setBrowsable(true)
+ .setOnClickListener {
+ screenManager.push(MapScreen(carContext, session, favorites = true))
+ }
+ .build())
+ if (carContext.carAppApiLevel >= CarAppApiLevels.LEVEL_3) {
+ addItem(
+ Row.Builder()
+ .setTitle(carContext.getString(R.string.auto_vehicle_data))
+ .setImage(
+ CarIcon.Builder(
+ IconCompat.createWithResource(carContext, R.drawable.ic_car)
+ ).setTint(CarColor.DEFAULT).build()
+ )
+ .setBrowsable(true)
+ .setOnClickListener {
+ session.mapScreen = null
+ screenManager.push(VehicleDataScreen(carContext))
+ }
+ .build()
)
- .setBrowsable(true)
- .setOnClickListener {
- screenManager.push(MapScreen(carContext, session, favorites = true))
- }
- .build())
- }.build())
- setCurrentLocationEnabled(true)
+ }
+ }.build())
+ setCurrentLocationEnabled(true)
+ }
setHeaderAction(Action.APP_ICON)
build()
}.build()
diff --git a/app/src/google/java/net/vonforst/evmap/ui/Gauge.kt b/app/src/google/java/net/vonforst/evmap/ui/Gauge.kt
new file mode 100644
index 00000000..f0bb93f6
--- /dev/null
+++ b/app/src/google/java/net/vonforst/evmap/ui/Gauge.kt
@@ -0,0 +1,74 @@
+package net.vonforst.evmap.ui
+
+import android.content.Context
+import android.graphics.*
+import androidx.annotation.ColorInt
+import androidx.core.content.ContextCompat
+import net.vonforst.evmap.R
+import kotlin.math.max
+import kotlin.math.min
+
+class Gauge(val size: Int, ctx: Context) {
+ val arcPaint = Paint().apply {
+ style = Paint.Style.STROKE
+ strokeWidth = size * 0.15f
+ }
+ val gaugePaint = Paint()
+ val activeColor = ContextCompat.getColor(ctx, R.color.gauge_active)
+ val middleColor = ContextCompat.getColor(ctx, R.color.gauge_middle)
+ val inactiveColor = ContextCompat.getColor(ctx, R.color.gauge_inactive)
+
+ fun draw(valuePercent: Float?, secondValuePercent: Float? = null): Bitmap {
+ val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
+ val canvas = Canvas(bitmap)
+
+ canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
+
+ val angle = valuePercent?.let { 180f * it / 100 } ?: 0f
+ val secondAngle = secondValuePercent?.let { 180f * it / 100 }
+
+ drawArc(angle, secondAngle, canvas)
+ if (secondAngle != null) drawGauge(secondAngle, inactiveColor, canvas)
+ drawGauge(angle, Color.WHITE, canvas)
+ return bitmap
+ }
+
+ private fun drawGauge(angle: Float, @ColorInt color: Int, canvas: Canvas) {
+ gaugePaint.color = color
+ canvas.save()
+ canvas.rotate(angle - 90, size / 2f, 3 * size / 4f)
+ canvas.drawCircle(size / 2f, 3 * size / 4f, size * 0.1F, gaugePaint)
+ canvas.drawRect(size * 0.48f, 3 * size / 4f, size * 0.53f, size * 0.325f, gaugePaint)
+ canvas.restore()
+ }
+
+ private fun drawArc(angle: Float, secondAngle: Float?, canvas: Canvas) {
+ val (angle1, angle2) = if (secondAngle != null) {
+ min(angle, secondAngle) to max(angle, secondAngle)
+ } else {
+ angle to null
+ }
+
+ arcPaint.color = activeColor
+ val arcBounds = RectF(
+ arcPaint.strokeWidth / 2,
+ size / 4f + arcPaint.strokeWidth / 2,
+ size - arcPaint.strokeWidth / 2,
+ 5 * size / 4f - arcPaint.strokeWidth / 2
+ )
+
+ canvas.drawArc(arcBounds, 180f, angle1, false, arcPaint)
+ if (angle2 != null) {
+ arcPaint.color = middleColor
+ canvas.drawArc(arcBounds, 180f + angle1, angle2 - angle1, false, arcPaint)
+ }
+ arcPaint.color = inactiveColor
+ canvas.drawArc(
+ arcBounds,
+ 180f + (angle2 ?: angle1),
+ 180f - (angle2 ?: angle1),
+ false,
+ arcPaint
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/google/res/drawable/ic_car.xml b/app/src/google/res/drawable/ic_car.xml
new file mode 100644
index 00000000..dc2afc1f
--- /dev/null
+++ b/app/src/google/res/drawable/ic_car.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/google/res/values-de/values.xml b/app/src/google/res/values-de/values.xml
index bcc63a28..1db7c465 100644
--- a/app/src/google/res/values-de/values.xml
+++ b/app/src/google/res/values-de/values.xml
@@ -15,12 +15,22 @@
In App öffnen
Auf dem Telefon geöffnet
Um EVMap auf Android Auto zu nutzen, braucht die App Zugriff auf deinen Standort.
+ Für diese Funktion benötigt EVMap Zugriff auf Daten deines Fahrzeugs.
Auf Telefon zulassen
In der Nähe
Favoriten
⚠️ Störungsmeldung (%s)
Weitere Aktualisierung nicht möglich. Bitte zurück gehen und neu starten.
+ Preise
+ Fahrzeugdaten
+ Ladezustand
+ Nicht verfügbar
+ Reichweite
+ Geschwindigkeit
Android Auto-Unterstützung
Auf unterstützen Autos kannst du EVMap auch mit Android Auto nutzen. Öffne dazu einfach die EVMap-App aus dem Menü von Android Auto.
klingt cool
+ EVMap konnte das Fahrzeugmodell nicht erkennen.
+ Keins der in der App ausgewählten Fahrzeuge passt zu diesem Fahrzeug (%s %s). Bitte wähle in der App ein passendes Fahrzeug aus.
+ Mehrere der in der App ausgewählten Fahrzeuge passen zu diesem Fahrzeug (%s %s). Bitte wähle nur ein passendes Fahrzeug in der App aus.
\ No newline at end of file
diff --git a/app/src/google/res/values/colors.xml b/app/src/google/res/values/colors.xml
new file mode 100644
index 00000000..34fdac36
--- /dev/null
+++ b/app/src/google/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #00e676
+ #087f23
+ #9e9e9e
+
\ No newline at end of file
diff --git a/app/src/google/res/values/values.xml b/app/src/google/res/values/values.xml
index 13380609..94b84215 100644
--- a/app/src/google/res/values/values.xml
+++ b/app/src/google/res/values/values.xml
@@ -25,12 +25,22 @@
Open in app
Opened on phone
To run EVMap on Android Auto, you need to grant access to your location.
+ For this feature, EVMap needs access to your vehicle data.
Grant on phone
Nearby chargers
Favorites
⚠️ Fault report (%s)
Further updates not possible. Please go back and restart.
+ Pricing
+ Vehicle data
+ Charging level
+ Unavailable
+ Range
+ Speed
Android Auto support
You can also use EVMap from within Android Auto on supported cars. Simply select the EVMap app in the Android Auto menu.
sounds cool
+ EVMap could not determine your vehicle model.
+ None of the vehicles selected in the app matches this vehicle (%s %s). Please select a matching vehicle from the app.
+ Mehrere der in der App ausgewählten Fahrzeuge passen zu diesem Fahrzeug (%s %s). Bitte wähle nur ein passendes Fahrzeug in der App aus.
\ No newline at end of file
diff --git a/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceApi.kt b/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceApi.kt
index 07110b6e..4745e8d6 100644
--- a/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceApi.kt
+++ b/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceApi.kt
@@ -15,6 +15,7 @@ import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
+import java.util.*
interface ChargepriceApi {
@POST("charge_prices")
@@ -33,6 +34,9 @@ interface ChargepriceApi {
private val cacheSize = 1L * 1024 * 1024 // 1MB
val supportedLanguages = setOf("de", "en", "fr", "nl")
+ val DATA_SOURCE_GOINGELECTRIC = "going_electric"
+ val DATA_SOURCE_OPENCHARGEMAP = "open_charge_map"
+
private val jsonApiAdapterFactory = ResourceAdapterFactory.builder()
.add(ChargepriceRequest::class.java)
.add(ChargepriceTariff::class.java)
@@ -75,6 +79,16 @@ interface ChargepriceApi {
return retrofit.create(ChargepriceApi::class.java)
}
+
+ fun getChargepriceLanguage(): String {
+ val locale = Locale.getDefault().language
+ return if (supportedLanguages.contains(locale)) {
+ locale
+ } else {
+ "en"
+ }
+ }
+
@JvmStatic
fun isCountrySupported(country: String, dataSource: String): Boolean = when (dataSource) {
// list of countries updated 2021/08/24
diff --git a/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceModel.kt b/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceModel.kt
index b4a7da0f..cc30a5df 100644
--- a/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceModel.kt
+++ b/app/src/main/java/net/vonforst/evmap/api/chargeprice/ChargepriceModel.kt
@@ -11,6 +11,7 @@ import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.Equatable
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.model.ChargeLocation
+import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.ui.currency
import kotlin.math.ceil
import kotlin.math.floor
@@ -148,6 +149,26 @@ class ChargepriceCar : Resource(), Equatable {
result = 31 * result + manufacturer.hashCode()
return result
}
+
+ private val acConnectors = listOf(
+ Chargepoint.CEE_BLAU,
+ Chargepoint.CEE_ROT,
+ Chargepoint.SCHUKO,
+ Chargepoint.TYPE_1,
+ Chargepoint.TYPE_2_UNKNOWN,
+ Chargepoint.TYPE_2_SOCKET,
+ Chargepoint.TYPE_2_PLUG
+ )
+ private val plugMapping = mapOf(
+ "ccs" to Chargepoint.CCS_UNKNOWN,
+ "tesla_suc" to Chargepoint.SUPERCHARGER,
+ "tesla_ccs" to Chargepoint.CCS_UNKNOWN,
+ "chademo" to Chargepoint.CHADEMO
+ )
+ val compatibleEvmapConnectors: List
+ get() = dcChargePorts.map {
+ plugMapping[it]
+ }.filterNotNull().plus(acConnectors)
}
@JsonApi(type = "brand")
diff --git a/app/src/main/java/net/vonforst/evmap/storage/Database.kt b/app/src/main/java/net/vonforst/evmap/storage/Database.kt
index 0655c0a7..8b5db0b8 100644
--- a/app/src/main/java/net/vonforst/evmap/storage/Database.kt
+++ b/app/src/main/java/net/vonforst/evmap/storage/Database.kt
@@ -1,5 +1,6 @@
package net.vonforst.evmap.storage
+import android.annotation.SuppressLint
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
@@ -256,6 +257,7 @@ abstract class AppDatabase : RoomDatabase() {
}
private val MIGRATION_13 = object : Migration(12, 13) {
+ @SuppressLint("Range")
override fun migrate(db: SupportSQLiteDatabase) {
db.beginTransaction()
try {
diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/ChargepriceViewModel.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/ChargepriceViewModel.kt
index 8fd72131..ab932024 100644
--- a/app/src/main/java/net/vonforst/evmap/viewmodel/ChargepriceViewModel.kt
+++ b/app/src/main/java/net/vonforst/evmap/viewmodel/ChargepriceViewModel.kt
@@ -49,27 +49,10 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
MutableLiveData()
}
- private val acConnectors = listOf(
- Chargepoint.CEE_BLAU,
- Chargepoint.CEE_ROT,
- Chargepoint.SCHUKO,
- Chargepoint.TYPE_1,
- Chargepoint.TYPE_2_UNKNOWN,
- Chargepoint.TYPE_2_SOCKET,
- Chargepoint.TYPE_2_PLUG
- )
- private val plugMapping = mapOf(
- "ccs" to Chargepoint.CCS_UNKNOWN,
- "tesla_suc" to Chargepoint.SUPERCHARGER,
- "tesla_ccs" to Chargepoint.CCS_UNKNOWN,
- "chademo" to Chargepoint.CHADEMO
- )
val vehicleCompatibleConnectors: LiveData> by lazy {
MediatorLiveData>().apply {
addSource(vehicle) {
- value = it?.dcChargePorts?.map {
- plugMapping[it]
- }?.filterNotNull()?.plus(acConnectors)
+ value = it?.compatibleEvmapConnectors
}
}
}
@@ -245,7 +228,7 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
currency = prefs.chargepriceCurrency
)
- }, getChargepriceLanguage())
+ }, ChargepriceApi.getChargepriceLanguage())
val meta =
result.meta.get(ChargepriceApi.moshi.adapter(ChargepriceMeta::class.java)) as ChargepriceMeta
chargePrices.value = Resource.success(result)
@@ -272,13 +255,4 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
}
}
}
-
- private fun getChargepriceLanguage(): String {
- val locale = Locale.getDefault().language
- return if (ChargepriceApi.supportedLanguages.contains(locale)) {
- locale
- } else {
- "en"
- }
- }
}
\ No newline at end of file
diff --git a/fastlane/metadata/android/de-DE/images/featureGraphic.png b/fastlane/metadata/android/de-DE/images/featureGraphic.png
index 6570af02..542b210b 100644
Binary files a/fastlane/metadata/android/de-DE/images/featureGraphic.png and b/fastlane/metadata/android/de-DE/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/de-DE/images/icon.png b/fastlane/metadata/android/de-DE/images/icon.png
index c622d495..e332ff42 100644
Binary files a/fastlane/metadata/android/de-DE/images/icon.png and b/fastlane/metadata/android/de-DE/images/icon.png differ
diff --git a/fastlane/metadata/android/de-DE/images/phoneScreenshots/01_map.png b/fastlane/metadata/android/de-DE/images/phoneScreenshots/01_map.png
new file mode 100644
index 00000000..be1d0c75
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/images/phoneScreenshots/01_map.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4fec0a34114d957c75fe02cb7e972a752c704b41a162fcd61018fb94b2f51499
+size 895589
diff --git a/fastlane/metadata/android/de-DE/images/phoneScreenshots/02_detail.png b/fastlane/metadata/android/de-DE/images/phoneScreenshots/02_detail.png
new file mode 100644
index 00000000..cb504b52
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/images/phoneScreenshots/02_detail.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2d96236006f8a71429080ead33352b51cdc24dac997e13bb21cac9be33c88f01
+size 857431
diff --git a/fastlane/metadata/android/de-DE/images/phoneScreenshots/03_prices.png b/fastlane/metadata/android/de-DE/images/phoneScreenshots/03_prices.png
new file mode 100644
index 00000000..e91f346a
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/images/phoneScreenshots/03_prices.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4f1561eeceaaf11dfc513b3e94fdb87e33363aca6afdff99ed9058bb2549ea59
+size 348799
diff --git a/fastlane/metadata/android/de-DE/images/phoneScreenshots/04_favorites.png b/fastlane/metadata/android/de-DE/images/phoneScreenshots/04_favorites.png
new file mode 100644
index 00000000..24f2ee89
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/images/phoneScreenshots/04_favorites.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:94c75119d726f2926f788266e2bf1d2572079cdeb24a7721eec90b38d94b7d57
+size 96031
diff --git a/fastlane/metadata/android/de-DE/images/phoneScreenshots/05_filters.png b/fastlane/metadata/android/de-DE/images/phoneScreenshots/05_filters.png
new file mode 100644
index 00000000..9731c360
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/images/phoneScreenshots/05_filters.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:498c57fade7895b6c714088c489d829d44da75d8f69b8ecab6c8f998f74411b5
+size 137330
diff --git a/fastlane/metadata/android/de-DE/images/phoneScreenshots/1_de-DE.png b/fastlane/metadata/android/de-DE/images/phoneScreenshots/1_de-DE.png
deleted file mode 100644
index 3eb6b280..00000000
Binary files a/fastlane/metadata/android/de-DE/images/phoneScreenshots/1_de-DE.png and /dev/null differ
diff --git a/fastlane/metadata/android/de-DE/images/phoneScreenshots/2_de-DE.png b/fastlane/metadata/android/de-DE/images/phoneScreenshots/2_de-DE.png
deleted file mode 100644
index a15d01bb..00000000
Binary files a/fastlane/metadata/android/de-DE/images/phoneScreenshots/2_de-DE.png and /dev/null differ
diff --git a/fastlane/metadata/android/en-US/images/featureGraphic.png b/fastlane/metadata/android/en-US/images/featureGraphic.png
new file mode 100644
index 00000000..542b210b
--- /dev/null
+++ b/fastlane/metadata/android/en-US/images/featureGraphic.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f83e571b78b205002cf41d4d625eb22b5bf32c60d24ce39ab8987591183c5c27
+size 72855
diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png
new file mode 100644
index 00000000..e332ff42
--- /dev/null
+++ b/fastlane/metadata/android/en-US/images/icon.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fa929430122f3d3318f430d0148f43aa045373cd53a4e9a193280f97c11328d0
+size 25044
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/01_map.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/01_map.png
new file mode 100644
index 00000000..b018a345
--- /dev/null
+++ b/fastlane/metadata/android/en-US/images/phoneScreenshots/01_map.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:aaa0032bd567bd9f6257bf17ca72915f973d676102c66d532d12309bc901cb5e
+size 884287
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/02_detail.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/02_detail.png
new file mode 100644
index 00000000..5b0f4f0b
--- /dev/null
+++ b/fastlane/metadata/android/en-US/images/phoneScreenshots/02_detail.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:170fc47f88b2515f0b7d1c9b8e87c3fb905324ec32f6e25937f2fc241fbe1bbb
+size 857298
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/03_prices.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/03_prices.png
new file mode 100644
index 00000000..68912eee
--- /dev/null
+++ b/fastlane/metadata/android/en-US/images/phoneScreenshots/03_prices.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:aaf780abbad2388002fc9afc9d6b1534061be9a6bab96bd3c166b3317d5332ff
+size 338280
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/04_favorites.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/04_favorites.png
new file mode 100644
index 00000000..0e35823a
--- /dev/null
+++ b/fastlane/metadata/android/en-US/images/phoneScreenshots/04_favorites.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f5b58726094bb4f0d29229b729a3c369bf9f9b5ad7a9f355dbf20430b656e2ad
+size 97536
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/05_filters.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/05_filters.png
new file mode 100644
index 00000000..c55b913d
--- /dev/null
+++ b/fastlane/metadata/android/en-US/images/phoneScreenshots/05_filters.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3b12d10d0eef40fabc9221bdc45460c7fe607ff9b254e59ecf92123413f3f22c
+size 124451