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 ----------- -Screenshot 1Screenshot 2 +Screenshot 1Screenshot 2 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