diff --git a/app/build.gradle b/app/build.gradle index 174ff387..3d10a3b9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -120,7 +120,7 @@ dependencies { implementation 'com.github.pengrad:mapscaleview:1.6.0' // Android Auto - googleImplementation 'com.google.android.libraries.car:car-app:1.0.0-beta.1' + googleImplementation 'androidx.car.app:app:1.0.0-beta01' // AnyMaps def anyMapsVersion = '1f050d860f' diff --git a/app/src/google/AndroidManifest.xml b/app/src/google/AndroidManifest.xml index b8dc6d13..10a8b42b 100644 --- a/app/src/google/AndroidManifest.xml +++ b/app/src/google/AndroidManifest.xml @@ -4,8 +4,9 @@ package="net.vonforst.evmap"> + - + + android:name="androidx.car.app.CarAppService" + android:category="androidx.car.app.category.CHARGING" /> 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 74d27800..5d22c6ec 100644 --- a/app/src/google/java/net/vonforst/evmap/auto/CarAppService.kt +++ b/app/src/google/java/net/vonforst/evmap/auto/CarAppService.kt @@ -2,6 +2,7 @@ package net.vonforst.evmap.auto import android.Manifest import android.content.* +import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.drawable.BitmapDrawable @@ -12,6 +13,13 @@ import android.os.IBinder import android.os.ResultReceiver import android.text.SpannableStringBuilder import android.text.Spanned +import androidx.car.app.CarContext +import androidx.car.app.CarToast +import androidx.car.app.Screen +import androidx.car.app.Session +import androidx.car.app.model.* +import androidx.car.app.model.Distance.UNIT_KILOMETERS +import androidx.car.app.validation.HostValidator import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.IconCompat import androidx.lifecycle.Lifecycle @@ -21,11 +29,6 @@ import androidx.lifecycle.lifecycleScope import androidx.localbroadcastmanager.content.LocalBroadcastManager import coil.imageLoader import coil.request.ImageRequest -import com.google.android.libraries.car.app.CarContext -import com.google.android.libraries.car.app.CarToast -import com.google.android.libraries.car.app.Screen -import com.google.android.libraries.car.app.model.* -import com.google.android.libraries.car.app.model.Distance.UNIT_KILOMETERS import kotlinx.coroutines.* import net.vonforst.evmap.* import net.vonforst.evmap.api.availability.ChargeLocationStatus @@ -46,11 +49,28 @@ import java.time.format.DateTimeFormatter import java.time.format.FormatStyle import kotlin.math.roundToInt + interface LocationAwareScreen { fun updateLocation(location: Location) } -class CarAppService : com.google.android.libraries.car.app.CarAppService(), LifecycleObserver { +class CarAppService : androidx.car.app.CarAppService() { + override fun createHostValidator(): HostValidator { + return if (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0) { + HostValidator.ALLOW_ALL_HOSTS_VALIDATOR + } else { + HostValidator.Builder(applicationContext) + .addAllowedHosts(androidx.car.app.R.array.hosts_allowlist_sample) + .build() + } + } + + override fun onCreateSession(): Session { + return EVMapSession(this) + } +} + +class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver { var mapScreen: LocationAwareScreen? = null set(value) { field = value @@ -72,18 +92,7 @@ class CarAppService : com.google.android.libraries.car.app.CarAppService(), Life } } - private val locationReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - val location = intent.getParcelableExtra(CarLocationService.EXTRA_LOCATION) as Location? - val mapScreen = this@CarAppService.mapScreen - if (location != null && mapScreen != null) { - mapScreen.updateLocation(location) - } - this@CarAppService.location = location - } - } - - override fun onCreate() { + init { lifecycle.addObserver(this) } @@ -101,11 +110,22 @@ class CarAppService : com.google.android.libraries.car.app.CarAppService(), Life Manifest.permission.ACCESS_FINE_LOCATION ) == PackageManager.PERMISSION_GRANTED + 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 + } + } + @OnLifecycleEvent(Lifecycle.Event.ON_START) fun bindLocationService() { if (!locationPermissionGranted()) return - bindService( - Intent(this, CarLocationService::class.java), + cas.bindService( + Intent(cas, CarLocationService::class.java), serviceConnection, Context.BIND_AUTO_CREATE ) @@ -114,12 +134,12 @@ class CarAppService : com.google.android.libraries.car.app.CarAppService(), Life @OnLifecycleEvent(Lifecycle.Event.ON_STOP) private fun unbindLocationService() { locationService?.removeLocationUpdates() - unbindService(serviceConnection) + cas.unbindService(serviceConnection) } @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) private fun registerBroadcastReceiver() { - LocalBroadcastManager.getInstance(this).registerReceiver( + LocalBroadcastManager.getInstance(cas).registerReceiver( locationReceiver, IntentFilter(CarLocationService.ACTION_BROADCAST) ); @@ -127,28 +147,28 @@ class CarAppService : com.google.android.libraries.car.app.CarAppService(), Life @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) private fun unregisterBroadcastReceiver() { - LocalBroadcastManager.getInstance(this).unregisterReceiver(locationReceiver) + LocalBroadcastManager.getInstance(cas).unregisterReceiver(locationReceiver) } } /** * Welcome screen with selection between favorites and nearby chargers */ -class WelcomeScreen(ctx: CarContext, val cas: CarAppService) : Screen(ctx), LocationAwareScreen { +class WelcomeScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx), LocationAwareScreen { private var location: Location? = null - override fun getTemplate(): Template { - cas.mapScreen = this - return PlaceListMapTemplate.builder().apply { + override fun onGetTemplate(): Template { + session.mapScreen = this + return PlaceListMapTemplate.Builder().apply { setTitle(carContext.getString(R.string.app_name)) location?.let { - setAnchor(Place.builder(LatLng.create(it)).build()) + setAnchor(Place.Builder(CarLocation.create(it)).build()) } - setItemList(ItemList.builder().apply { - addItem(Row.builder() + setItemList(ItemList.Builder().apply { + addItem(Row.Builder() .setTitle(carContext.getString(R.string.auto_chargers_closeby)) .setImage( - CarIcon.builder( + CarIcon.Builder( IconCompat.createWithResource( carContext, R.drawable.ic_address @@ -156,27 +176,28 @@ class WelcomeScreen(ctx: CarContext, val cas: CarAppService) : Screen(ctx), Loca ) .setTint(CarColor.DEFAULT).build() ) - .setIsBrowsable(true) + .setBrowsable(true) .setOnClickListener { - screenManager.push(MapScreen(carContext, cas, favorites = false)) + 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 + addItem( + Row.Builder() + .setTitle(carContext.getString(R.string.auto_favorites)) + .setImage( + CarIcon.Builder( + IconCompat.createWithResource( + carContext, + R.drawable.ic_fav + ) ) + .setTint(CarColor.DEFAULT).build() ) - .setTint(CarColor.DEFAULT).build() - ) - .setIsBrowsable(true) - .setOnClickListener { - screenManager.push(MapScreen(carContext, cas, favorites = true)) - } - .build()) + .setBrowsable(true) + .setOnClickListener { + screenManager.push(MapScreen(carContext, session, favorites = true)) + } + .build()) }.build()) setCurrentLocationEnabled(true) setHeaderAction(Action.APP_ICON) @@ -193,13 +214,13 @@ class WelcomeScreen(ctx: CarContext, val cas: CarAppService) : Screen(ctx), Loca /** * Screen to grant location permission */ -class PermissionScreen(ctx: CarContext, val cas: CarAppService) : Screen(ctx) { - override fun getTemplate(): Template { - return MessageTemplate.builder(carContext.getString(R.string.auto_location_permission_needed)) +class PermissionScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) { + override fun onGetTemplate(): Template { + return MessageTemplate.Builder(carContext.getString(R.string.auto_location_permission_needed)) .setTitle(carContext.getString(R.string.app_name)) .setHeaderAction(Action.APP_ICON) - .setActions(listOf( - Action.builder() + .addAction( + Action.Builder() .setTitle(carContext.getString(R.string.grant_on_phone)) .setBackgroundColor(CarColor.PRIMARY) .setOnClickListener(ParkedOnlyOnClickListener.create { @@ -213,8 +234,13 @@ class PermissionScreen(ctx: CarContext, val cas: CarAppService) : Screen(ctx) { resultData: Bundle? ) { if (resultData!!.getBoolean(PermissionActivity.RESULT_GRANTED)) { - cas.bindLocationService() - screenManager.push(WelcomeScreen(carContext, cas)) + session.bindLocationService() + screenManager.push( + WelcomeScreen( + carContext, + session + ) + ) } } }) @@ -225,15 +251,16 @@ class PermissionScreen(ctx: CarContext, val cas: CarAppService) : Screen(ctx) { CarToast.LENGTH_LONG ).show() }) - .build(), - Action.builder() + .build() + ) + .addAction( + Action.Builder() .setTitle(carContext.getString(R.string.cancel)) .setOnClickListener { - cas.stopSelf() + carContext.finishCarApp() } .build(), - - )) + ) .build() } } @@ -241,7 +268,7 @@ class PermissionScreen(ctx: CarContext, val cas: CarAppService) : Screen(ctx) { /** * Main map screen showing either nearby chargers or favorites */ -class MapScreen(ctx: CarContext, val cas: CarAppService, val favorites: Boolean = false) : +class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boolean = false) : Screen(ctx), LocationAwareScreen { private var updateCoroutine: Job? = null private var numUpdates = 0 @@ -260,9 +287,9 @@ class MapScreen(ctx: CarContext, val cas: CarAppService, val favorites: Boolean HashMap() private val maxRows = 6 - override fun getTemplate(): Template { - cas.mapScreen = this - return PlaceListMapTemplate.builder().apply { + override fun onGetTemplate(): Template { + session.mapScreen = this + return PlaceListMapTemplate.Builder().apply { setTitle( carContext.getString( if (favorites) { @@ -273,8 +300,8 @@ class MapScreen(ctx: CarContext, val cas: CarAppService, val favorites: Boolean ) ) location?.let { - setAnchor(Place.builder(LatLng.create(it)).build()) - } ?: setIsLoading(true) + setAnchor(Place.Builder(CarLocation.create(it)).build()) + } ?: setLoading(true) chargers?.take(maxRows)?.let { chargerList -> val builder = ItemList.Builder() chargerList.forEach { charger -> @@ -290,7 +317,7 @@ class MapScreen(ctx: CarContext, val cas: CarAppService, val favorites: Boolean ) ) setItemList(builder.build()) - } ?: setIsLoading(true) + } ?: setLoading(true) setCurrentLocationEnabled(true) setHeaderAction(Action.BACK) build() @@ -299,15 +326,16 @@ class MapScreen(ctx: CarContext, val cas: CarAppService, val favorites: Boolean private fun formatCharger(charger: ChargeLocation): Row { val color = ContextCompat.getColor(carContext, getMarkerTint(charger)) - val place = Place.builder(LatLng.create(charger.coordinates.lat, charger.coordinates.lng)) - .setMarker( - PlaceMarker.builder() - .setColor(CarColor.createCustom(color, color)) - .build() - ) - .build() + val place = + Place.Builder(CarLocation.create(charger.coordinates.lat, charger.coordinates.lng)) + .setMarker( + PlaceMarker.Builder() + .setColor(CarColor.createCustom(color, color)) + .build() + ) + .build() - return Row.builder().apply { + return Row.Builder().apply { setTitle(charger.name) val text = SpannableStringBuilder() @@ -343,7 +371,11 @@ class MapScreen(ctx: CarContext, val cas: CarAppService, val favorites: Boolean } addText(text) - setMetadata(Metadata.ofPlace(place)) + setMetadata( + Metadata.Builder() + .setPlace(place) + .build() + ) setOnClickListener { screenManager.push(ChargerDetailScreen(carContext, charger)) @@ -439,13 +471,13 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : private val iconGen = ChargerIconGenerator(carContext, null, oversize = 1.4f, height = 64) - override fun getTemplate(): Template { + override fun onGetTemplate(): Template { if (charger == null) loadCharger() - return PaneTemplate.builder( + return PaneTemplate.Builder( Pane.Builder().apply { charger?.let { charger -> - addRow(Row.builder().apply { + addRow(Row.Builder().apply { setTitle(charger.address.toString()) val icon = iconGen.getBitmap( @@ -454,7 +486,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : multi = charger.isMulti() ) setImage( - CarIcon.of(IconCompat.createWithBitmap(icon)), + CarIcon.Builder(IconCompat.createWithBitmap(icon)).build(), Row.IMAGE_TYPE_LARGE ) @@ -479,10 +511,10 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : } addText(chargepointsText) }.build()) - addRow(Row.builder().apply { + addRow(Row.Builder().apply { photo?.let { setImage( - CarIcon.of(IconCompat.createWithBitmap(photo)), + CarIcon.Builder(IconCompat.createWithBitmap(photo)).build(), Row.IMAGE_TYPE_LARGE ) } @@ -513,23 +545,23 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : Row.IMAGE_TYPE_ICON) }*/ }.build()) - setActions(listOf( - Action.builder() - .setIcon( - CarIcon.of( - IconCompat.createWithResource( - carContext, - R.drawable.ic_navigation - ) + addAction(Action.Builder() + .setIcon( + CarIcon.Builder( + IconCompat.createWithResource( + carContext, + R.drawable.ic_navigation ) - ) - .setTitle(carContext.getString(R.string.navigate)) - .setBackgroundColor(CarColor.PRIMARY) - .setOnClickListener { - navigateToCharger(charger) - } - .build(), - Action.builder() + ).build() + ) + .setTitle(carContext.getString(R.string.navigate)) + .setBackgroundColor(CarColor.PRIMARY) + .setOnClickListener { + navigateToCharger(charger) + } + .build()) + addAction( + Action.Builder() .setTitle(carContext.getString(R.string.open_in_app)) .setOnClickListener(ParkedOnlyOnClickListener.create { val intent = Intent(carContext, MapsActivity::class.java) @@ -546,8 +578,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : }) .build() ) - ) - } ?: setIsLoading(true) + } ?: setLoading(true) }.build() ).apply { setTitle(chargerSparse.name)