diff --git a/android-auto-app/build.gradle.kts b/android-auto-app/build.gradle.kts
index ff2a7d217b..f19fa58d25 100644
--- a/android-auto-app/build.gradle.kts
+++ b/android-auto-app/build.gradle.kts
@@ -8,11 +8,11 @@ plugins {
val buildFromSource: String by project
android {
- compileSdkVersion(AndroidVersions.compileSdkVersion)
+ compileSdkVersion(AndroidVersions.compileSdkVersion_AndroidAuto)
defaultConfig {
applicationId = "com.mapbox.maps.testapp.auto"
- minSdkVersion(AndroidVersions.minAndroidAutoSdkVersion)
- targetSdkVersion(AndroidVersions.targetSdkVersion)
+ minSdkVersion(AndroidVersions.minSdkVersion_AndroidAuto)
+ targetSdkVersion(AndroidVersions.targetSdkVersion_AndroidAuto)
versionCode = 1
versionName = "0.1.0"
multiDexEnabled = true
@@ -50,10 +50,10 @@ dependencies {
implementation(Dependencies.googleCarAppLibrary)
implementation(Dependencies.kotlin)
implementation(Dependencies.androidxAppCompat)
+ implementation(Dependencies.androidxCoreKtx)
// By default, the Maps SDK uses the Android Location Provider to obtain raw location updates.
// And with Android 11, the raw location updates might suffer from precision issue.
-
// The Maps SDK also comes pre-compiled with support for the [Google's Fused Location Provider](https://developers.google.com/location-context/fused-location-provider)
// if that dependency is available. This means, that if your target devices support Google Play
// Services, [we recommend adding the Google Play Location Services dependency to your project](https://developers.google.com/android/guides/setup).
diff --git a/android-auto-app/src/main/AndroidManifest.xml b/android-auto-app/src/main/AndroidManifest.xml
index e631477ba7..ece2e7aaec 100644
--- a/android-auto-app/src/main/AndroidManifest.xml
+++ b/android-auto-app/src/main/AndroidManifest.xml
@@ -8,7 +8,6 @@
-
-
-
+ lastGpsLocation = point
+ if (isTrackingPuck) {
+ surface?.mapSurface?.getMapboxMap()?.setCamera(
+ cameraOptions {
+ center(point)
+ padding(insets)
+ }
+ )
+ }
+ }
+
/**
* Initialise the car camera controller with a map surface.
*/
- fun init(mapSurface: MapSurface, edgeInsets: EdgeInsets = EdgeInsets(0.0, 0.0, 0.0, 0.0)) {
- insets = edgeInsets
- surface = mapSurface
- surface.getMapboxMap().setCamera(
+ override fun onAttached(mapboxCarMapSurface: MapboxCarMapSurface) {
+ super.onAttached(mapboxCarMapSurface)
+ this.surface = mapboxCarMapSurface
+ mapboxCarMapSurface.mapSurface.getMapboxMap().setCamera(
cameraOptions {
- pitch(INITIAL_PITCH)
- zoom(INITIAL_ZOOM)
+ pitch(previousCameraState?.pitch ?: INITIAL_PITCH)
+ zoom(previousCameraState?.zoom ?: INITIAL_ZOOM)
center(lastGpsLocation)
}
)
+ with(mapboxCarMapSurface.mapSurface.location) {
+ // Show a 3D location puck
+ locationPuck = CarLocationPuck.duckLocationPuckLowZoom
+ enabled = true
+ addOnIndicatorPositionChangedListener(changePositionListener)
+ }
}
- override fun onIndicatorPositionChanged(point: Point) {
- lastGpsLocation = point
- if (isTrackingPuck) {
- surface.getMapboxMap().setCamera(
- cameraOptions {
- center(point)
- padding(insets)
- }
- )
+ override fun onDetached(mapboxCarMapSurface: MapboxCarMapSurface) {
+ previousCameraState = mapboxCarMapSurface.mapSurface.getMapboxMap().cameraState
+ with(mapboxCarMapSurface.mapSurface.location) {
+ removeOnIndicatorPositionChangedListener(changePositionListener)
}
+ super.onDetached(mapboxCarMapSurface)
+ }
+
+ override fun onVisibleAreaChanged(visibleArea: Rect, edgeInsets: EdgeInsets) {
+ insets = edgeInsets
}
- override fun onMapScroll() {
+ override fun onScroll(
+ mapboxCarMapSurface: MapboxCarMapSurface,
+ distanceX: Float,
+ distanceY: Float
+ ): Boolean {
dismissTracking()
+ return false
}
/**
* Make camera center to location puck and track the location puck's position.
*/
fun focusOnLocationPuck() {
- surface.camera.flyTo(
+ surface?.mapSurface?.camera?.flyTo(
cameraOptions {
center(lastGpsLocation)
}
@@ -67,23 +96,51 @@ class CarCameraController : OnIndicatorPositionChangedListener, OnMapScrollListe
}
/**
- * Adjust the camera's zoom level by the given scale factor.
- *
- * @param scaleFactor the scale factor to be applied to the camera's current zoom level.
+ * Function dedicated to zoom in map action buttons.
*/
- fun zoomBy(scaleFactor: Double) {
- val cameraState = surface.getMapboxMap().cameraState
- surface.camera.easeTo(
- cameraOptions {
- zoom(scaleFactor * cameraState.zoom)
- }
- )
+ fun zoomInAction() = scaleEaseBy(ZOOM_ACTION_DELTA)
+
+ /**
+ * Function dedicated to zoom in map action buttons.
+ */
+ fun zoomOutAction() = scaleEaseBy(-ZOOM_ACTION_DELTA)
+
+ private fun scaleEaseBy(delta: Double) {
+ val mapSurface = surface?.mapSurface
+ val fromZoom = mapSurface?.getMapboxMap()?.cameraState?.zoom ?: return
+ val toZoom = (fromZoom + delta).coerceIn(MIN_ZOOM_OUT, MAX_ZOOM_IN)
+ mapSurface.camera.easeTo(cameraOptions { zoom(toZoom) })
}
companion object {
+ /**
+ * Default location for the demo.
+ */
private val HELSINKI = Point.fromLngLat(24.9384, 60.1699)
+
+ /**
+ * Default zoom for the demo.
+ */
private const val INITIAL_ZOOM = 16.0
+
+ /**
+ * Constant camera pitch for the demo.
+ */
private const val INITIAL_PITCH = 75.0
- private const val TAG = "CarCameraController"
+
+ /**
+ * When zooming the camera by a delta, this will prevent the camera from zooming further.
+ */
+ private const val MIN_ZOOM_OUT = 6.0
+
+ /**
+ * When zooming the camera by a delta, this will prevent the camera from zooming further.
+ */
+ private const val MAX_ZOOM_IN = 20.0
+
+ /**
+ * Simple zoom delta to associate with the zoom action buttons.
+ */
+ private const val ZOOM_ACTION_DELTA = 0.5
}
}
\ No newline at end of file
diff --git a/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/CarJavaInterfaceChecker.java b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/CarJavaInterfaceChecker.java
deleted file mode 100644
index 282f6e594b..0000000000
--- a/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/CarJavaInterfaceChecker.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.mapbox.maps.testapp.auto.car;
-
-import static com.mapbox.maps.extension.androidauto.MapboxCarUtilsKt.initMapSurface;
-
-import androidx.car.app.Session;
-
-import com.mapbox.maps.MapInitOptions;
-import com.mapbox.maps.extension.androidauto.MapSurfaceReadyCallback;
-import com.mapbox.maps.extension.androidauto.OnMapScaleListener;
-import com.mapbox.maps.extension.androidauto.OnMapScrollListener;
-
-public class CarJavaInterfaceChecker {
-
- private void carSession(Session session, MapInitOptions mapInitOptions,
- OnMapScrollListener scrollListener,
- OnMapScaleListener scaleListener,
- MapSurfaceReadyCallback mapSurfaceReadyCallback) {
- initMapSurface(session, mapSurfaceReadyCallback);
- initMapSurface(session, mapInitOptions, mapSurfaceReadyCallback);
- initMapSurface(session, mapInitOptions, scrollListener, mapSurfaceReadyCallback);
- initMapSurface(session, mapInitOptions, scrollListener, scaleListener, mapSurfaceReadyCallback);
- }
-}
\ No newline at end of file
diff --git a/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/CarMapShowcase.kt b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/CarMapShowcase.kt
new file mode 100644
index 0000000000..2a04f81b1b
--- /dev/null
+++ b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/CarMapShowcase.kt
@@ -0,0 +1,63 @@
+package com.mapbox.maps.testapp.auto.car
+
+import androidx.car.app.CarContext
+import com.mapbox.maps.MapboxExperimental
+import com.mapbox.maps.Style
+import com.mapbox.maps.extension.androidauto.MapboxCarMapObserver
+import com.mapbox.maps.extension.androidauto.MapboxCarMapSurface
+import com.mapbox.maps.extension.style.layers.generated.skyLayer
+import com.mapbox.maps.extension.style.layers.properties.generated.SkyType
+import com.mapbox.maps.extension.style.sources.generated.rasterDemSource
+import com.mapbox.maps.extension.style.style
+import com.mapbox.maps.extension.style.terrain.generated.terrain
+
+/**
+ * Example showing how you can add a sky layer that has a sun direction,
+ * and adding a terrain layer to show mountains.
+ */
+@OptIn(MapboxExperimental::class)
+class CarMapShowcase : MapboxCarMapObserver {
+
+ private var mapboxCarMapSurface: MapboxCarMapSurface? = null
+
+ override fun onAttached(mapboxCarMapSurface: MapboxCarMapSurface) {
+ this.mapboxCarMapSurface = mapboxCarMapSurface
+ loadMapStyle(mapboxCarMapSurface.carContext)
+ }
+
+ override fun onDetached(mapboxCarMapSurface: MapboxCarMapSurface) {
+ this.mapboxCarMapSurface = null
+ }
+
+ fun mapStyleUri(carContext: CarContext): String {
+ return if (carContext.isDarkMode) Style.TRAFFIC_NIGHT else Style.TRAFFIC_DAY
+ }
+
+ fun loadMapStyle(carContext: CarContext) {
+ // https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/#paint-sky-sky-atmosphere-sun
+ // Position of the sun center [a azimuthal angle, p polar angle].
+ // Azimuthal is degrees from north, where 0.0 is north.
+ // Polar is degrees from overhead, where 0.0 is overhead.
+ val sunDirection = if (carContext.isDarkMode) listOf(-50.0, 90.2) else listOf(0.0, 0.0)
+
+ mapboxCarMapSurface?.mapSurface?.getMapboxMap()?.loadStyle(
+ styleExtension = style(mapStyleUri(carContext)) {
+ +rasterDemSource(DEM_SOURCE) {
+ url(TERRAIN_URL_TILE_RESOURCE)
+ tileSize(514)
+ }
+ +terrain(DEM_SOURCE)
+ +skyLayer(SKY_LAYER) {
+ skyType(SkyType.ATMOSPHERE)
+ skyAtmosphereSun(sunDirection)
+ }
+ }
+ )
+ }
+
+ companion object {
+ private const val SKY_LAYER = "sky"
+ private const val DEM_SOURCE = "mapbox-dem"
+ private const val TERRAIN_URL_TILE_RESOURCE = "mapbox://mapbox.mapbox-terrain-dem-v1"
+ }
+}
\ No newline at end of file
diff --git a/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapScreen.kt b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapScreen.kt
index c31f9d9c7f..7a07587cc0 100644
--- a/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapScreen.kt
+++ b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapScreen.kt
@@ -1,32 +1,33 @@
package com.mapbox.maps.testapp.auto.car
-import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen
-import androidx.car.app.model.*
+import androidx.car.app.model.Action
+import androidx.car.app.model.ActionStrip
+import androidx.car.app.model.CarColor
+import androidx.car.app.model.CarIcon
+import androidx.car.app.model.Template
import androidx.car.app.navigation.model.NavigationTemplate
import androidx.core.graphics.drawable.IconCompat
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import com.mapbox.maps.MapboxExperimental
+import com.mapbox.maps.extension.androidauto.MapboxCarMap
import com.mapbox.maps.testapp.auto.R
-import java.lang.ref.WeakReference
/**
* Simple demo of how to show a Mapbox Map on the Android Auto screen.
*/
-class MapScreen(carContext: CarContext) : Screen(carContext) {
+@OptIn(MapboxExperimental::class)
+class MapScreen(
+ val mapboxCarMap: MapboxCarMap
+) : Screen(mapboxCarMap.carContext) {
private var isInPanMode: Boolean = false
- private lateinit var carCameraController: WeakReference
-
- /**
- * Set the map camera controller, so that the UI elements(such as action button) in the template
- * can interact with the camera.
- */
- fun setMapCameraController(controller: CarCameraController) {
- carCameraController = WeakReference(controller)
- }
+ private val carCameraController = CarCameraController()
override fun onGetTemplate(): Template {
val builder = NavigationTemplate.Builder()
- builder.setBackgroundColor(CarColor.SECONDARY)
+ .setBackgroundColor(CarColor.SECONDARY)
builder.setActionStrip(
ActionStrip.Builder()
@@ -41,7 +42,7 @@ class MapScreen(carContext: CarContext) : Screen(carContext) {
).build()
)
.setOnClickListener {
- carCameraController.get()?.focusOnLocationPuck()
+ carCameraController.focusOnLocationPuck()
}
.build()
)
@@ -76,7 +77,7 @@ class MapScreen(carContext: CarContext) : Screen(carContext) {
)
.setOnClickListener {
// handle zoom out
- carCameraController.get()?.zoomBy(0.95)
+ carCameraController.zoomOutAction()
}
.build()
)
@@ -91,7 +92,7 @@ class MapScreen(carContext: CarContext) : Screen(carContext) {
).build()
)
.setOnClickListener {
- carCameraController.get()?.zoomBy(1.05)
+ carCameraController.zoomInAction()
}
.build()
)
@@ -114,4 +115,16 @@ class MapScreen(carContext: CarContext) : Screen(carContext) {
return builder.build()
}
+
+ init {
+ lifecycle.addObserver(object : DefaultLifecycleObserver {
+ override fun onCreate(owner: LifecycleOwner) {
+ mapboxCarMap.registerObserver(carCameraController)
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ mapboxCarMap.unregisterObserver(carCameraController)
+ }
+ })
+ }
}
\ No newline at end of file
diff --git a/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapSession.kt b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapSession.kt
index f93341809a..c6fa7c05bc 100644
--- a/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapSession.kt
+++ b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapSession.kt
@@ -7,83 +7,51 @@ import android.content.res.Configuration
import androidx.car.app.Screen
import androidx.car.app.ScreenManager
import androidx.car.app.Session
-import com.mapbox.maps.EdgeInsets
-import com.mapbox.maps.MapSurface
-import com.mapbox.maps.Style
-import com.mapbox.maps.extension.androidauto.initMapSurface
-import com.mapbox.maps.extension.style.layers.generated.skyLayer
-import com.mapbox.maps.extension.style.layers.properties.generated.SkyType
-import com.mapbox.maps.extension.style.sources.generated.rasterDemSource
-import com.mapbox.maps.extension.style.style
-import com.mapbox.maps.extension.style.terrain.generated.terrain
-import com.mapbox.maps.plugin.locationcomponent.location
-import com.mapbox.maps.testapp.auto.R
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import com.mapbox.maps.MapInitOptions
+import com.mapbox.maps.MapboxExperimental
+import com.mapbox.maps.extension.androidauto.MapboxCarMap
/**
* Session class for the Mapbox Map sample app for Android Auto.
*/
+@OptIn(MapboxExperimental::class)
class MapSession : Session() {
- private lateinit var mapSurface: MapSurface
- private val carCameraController = CarCameraController()
+ private val carMapShowcase = CarMapShowcase()
+ private var mapboxCarMap: MapboxCarMap? = null
override fun onCreateScreen(intent: Intent): Screen {
- val mapScreen = MapScreen(carContext)
- initMapSurface(scrollListener = carCameraController) { surface ->
- mapSurface = surface
- carCameraController.init(
- mapSurface,
- EdgeInsets(
- carContext.resources.getDimensionPixelSize(R.dimen.map_padding).toDouble(),
- 0.0,
- 0.0,
- 0.0
- )
- )
- mapScreen.setMapCameraController(carCameraController)
- loadStyle(surface)
- initLocationComponent(surface)
- }
+ // The onCreate is guaranteed to be called before onCreateScreen. You can pass the
+ // mapboxCarMap to other screens. Each screen can register and unregister observers.
+ // This allows you to scope behaviors to sessions, screens, or events.
+ val mapScreen = MapScreen(mapboxCarMap!!)
+
return if (carContext.checkSelfPermission(ACCESS_FINE_LOCATION) != PERMISSION_GRANTED) {
- carContext.getCarService(ScreenManager::class.java).push(mapScreen)
+ carContext.getCarService(ScreenManager::class.java)
+ .push(mapScreen)
RequestPermissionScreen(carContext)
} else mapScreen
}
- private fun loadStyle(surface: MapSurface) {
- surface.getMapboxMap().loadStyle(
- style(if (carContext.isDarkMode) Style.TRAFFIC_NIGHT else Style.TRAFFIC_DAY) {
- +rasterDemSource(SOURCE) {
- url(TERRAIN_URL_TILE_RESOURCE)
- // 514 specifies padded DEM tile and provides better performance than 512 tiles.
- tileSize(514)
- }
- +terrain(SOURCE)
- +skyLayer(SKY_LAYER) {
- skyType(SkyType.ATMOSPHERE)
- skyAtmosphereSun(listOf(-50.0, 90.2))
- }
- }
- )
- }
-
- private fun initLocationComponent(surface: MapSurface) {
- // Enable location component plugin with 3D location puck.
- surface.location.apply {
- // Show a 3D location puck
- locationPuck = CarLocationPuck.duckLocationPuckLowZoom
- enabled = true
- addOnIndicatorPositionChangedListener(carCameraController)
- }
- }
-
override fun onCarConfigurationChanged(newConfiguration: Configuration) {
- super.onCarConfigurationChanged(newConfiguration)
- loadStyle(mapSurface)
+ carMapShowcase.loadMapStyle(carContext)
}
- companion object {
- private const val SOURCE = "TERRAIN_SOURCE"
- private const val SKY_LAYER = "sky"
- private const val TERRAIN_URL_TILE_RESOURCE = "mapbox://mapbox.mapbox-terrain-dem-v1"
+ init {
+ lifecycle.addObserver(object : DefaultLifecycleObserver {
+ override fun onCreate(owner: LifecycleOwner) {
+ // The carContext is not initialized until onCreate. Initialize your object here
+ // and then register any observers that should have a lifecycle for the entire
+ // car session.
+ mapboxCarMap = MapboxCarMap(MapInitOptions(carContext))
+ .registerObserver(carMapShowcase)
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ mapboxCarMap?.clearObservers()
+ mapboxCarMap = null
+ }
+ })
}
}
\ No newline at end of file
diff --git a/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/testing/CarJavaInterfaceChecker.java b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/testing/CarJavaInterfaceChecker.java
new file mode 100644
index 0000000000..6ba0aa214b
--- /dev/null
+++ b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/testing/CarJavaInterfaceChecker.java
@@ -0,0 +1,74 @@
+package com.mapbox.maps.testapp.auto.testing;
+
+import android.graphics.Rect;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.CarContext;
+import androidx.car.app.SurfaceContainer;
+
+import com.mapbox.maps.EdgeInsets;
+import com.mapbox.maps.MapInitOptions;
+import com.mapbox.maps.MapSurface;
+import com.mapbox.maps.ScreenCoordinate;
+import com.mapbox.maps.extension.androidauto.MapboxCarMap;
+import com.mapbox.maps.extension.androidauto.MapboxCarMapObserver;
+import com.mapbox.maps.extension.androidauto.MapboxCarMapSurface;
+
+class CarJavaInterfaceChecker {
+
+ void constructors(MapInitOptions mapInitOptions) {
+ MapboxCarMap mapboxCarMap = new MapboxCarMap(mapInitOptions);
+ }
+
+ void getters(MapboxCarMap mapboxCarMap) {
+ CarContext carContext = mapboxCarMap.getCarContext();
+ Rect visibleArea = mapboxCarMap.getVisibleArea();
+ EdgeInsets edgeInsets = mapboxCarMap.getEdgeInsets();
+ MapboxCarMapSurface mapboxCarMapSurface = mapboxCarMap.getCarMapSurface();
+ }
+
+ void getters(MapboxCarMapSurface mapboxCarMapSurface) {
+ CarContext carContext = mapboxCarMapSurface.getCarContext();
+ MapSurface mapSurface = mapboxCarMapSurface.getMapSurface();
+ SurfaceContainer surfaceContainer = mapboxCarMapSurface.getSurfaceContainer();
+ }
+
+ private void observers(MapboxCarMap mapboxCarMap) {
+ MapboxCarMapObserver observer = new MapboxCarMapObserver() {
+
+ @Override
+ public boolean onScale(@NonNull MapboxCarMapSurface mapboxCarMapSurface, @NonNull ScreenCoordinate anchor, double fromZoom, double toZoom) {
+ return false;
+ }
+
+ @Override
+ public boolean onFling(@NonNull MapboxCarMapSurface mapboxCarMapSurface, float velocityX, float velocityY) {
+ return false;
+ }
+
+ @Override
+ public boolean onScroll(@NonNull MapboxCarMapSurface mapboxCarMapSurface, float distanceX, float distanceY) {
+ return false;
+ }
+
+ @Override
+ public void onVisibleAreaChanged(@NonNull Rect visibleArea, @NonNull EdgeInsets edgeInsets) {
+
+ }
+
+ @Override
+ public void onDetached(@NonNull MapboxCarMapSurface mapboxCarMapSurface) {
+
+ }
+
+ @Override
+ public void onAttached(@NonNull MapboxCarMapSurface mapboxCarMapSurface) {
+
+ }
+ };
+ mapboxCarMap.registerObserver(observer);
+ mapboxCarMap.unregisterObserver(observer);
+ mapboxCarMap.clearObservers();
+ }
+
+}
diff --git a/android-auto-app/src/main/res/values/dimens.xml b/android-auto-app/src/main/res/values/dimens.xml
deleted file mode 100644
index ff7e4b5a71..0000000000
--- a/android-auto-app/src/main/res/values/dimens.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- 100dp
-
\ No newline at end of file
diff --git a/android-auto-app/src/main/res/xml/automotive_app_desc.xml b/android-auto-app/src/main/res/xml/automotive_app_desc.xml
index 88a21cc8cf..1037a90565 100644
--- a/android-auto-app/src/main/res/xml/automotive_app_desc.xml
+++ b/android-auto-app/src/main/res/xml/automotive_app_desc.xml
@@ -1,6 +1,4 @@
-
-
+
+
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/Project.kt b/buildSrc/src/main/kotlin/Project.kt
index 2476879a47..e3c82a0c3e 100644
--- a/buildSrc/src/main/kotlin/Project.kt
+++ b/buildSrc/src/main/kotlin/Project.kt
@@ -1,8 +1,10 @@
object AndroidVersions {
const val minSdkVersion = 21
- const val minAndroidAutoSdkVersion = 23
const val targetSdkVersion = 30
const val compileSdkVersion = 30
+ const val minSdkVersion_AndroidAuto = 23
+ const val targetSdkVersion_AndroidAuto = 30
+ const val compileSdkVersion_AndroidAuto = 31
}
object Plugins {
@@ -99,7 +101,7 @@ object Versions {
const val squareLeakCanary = "2.4"
const val materialDesign = "1.2.0"
const val googlePlayServicesLocation = "18.0.0"
- const val googleCarAppLibrary= "1.1.0-alpha01"
+ const val googleCarAppLibrary= "1.1.0"
const val kotlinCoroutines = "1.3.9"
const val junit = "4.13"
const val mockk = "1.9.3"
diff --git a/extension-androidauto/README.md b/extension-androidauto/README.md
index 303e25f376..05e565a100 100644
--- a/extension-androidauto/README.md
+++ b/extension-androidauto/README.md
@@ -1,14 +1,16 @@
-## Mapbox Maps Android Auto Extension for Android
+# Mapbox Maps Android Auto Extension
-### Overview
+## Overview
-The Mapbox Maps Android Auto Extension for Android is an public library for initialising the MapSurface from a [car app session](https://developer.android.com/reference/androidx/car/app/Session), and handling scroll and scale gestures.
+The Mapbox Maps Android Auto Extension is a public library for initialising the Mapbox `MapSurface` from a [Session](https://developer.android.com/reference/androidx/car/app/Session). The extension provides a simple framework for adding Mapbox to Android Auto head units.
A full overview of classes and interfaces can be found in our [API documentation](https://docs.mapbox.com/android/beta/maps/guides/).
-### Getting Started
+Working examples of the Android Auto extension can be found in our [test application](https://github.com/mapbox/mapbox-maps-android/tree/master/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto). To see examples of an integration with the Mapbox Navigation and Search SDKs, you can also try the [navigation examples](https://github.com/mapbox/mapbox-navigation-android-examples).
-This README is intended for developers who are interested in [contributing](https://github.com/mapbox/mapbox-maps-android/blob/master/CONTRIBUTING.md) to the Mapbox Maps Android Auto Extension for Android. Please visit [DEVELOPING.md](https://github.com/mapbox/mapbox-maps-android/blob/master/DEVELOPING.md) for general information and instructions on how to use the Mapbox Maps Extension System. To add the android auto extension to your project, you configure its dependency in your `build.gradle` files.
+## Getting Started
+
+This README is intended for developers who are interested in [contributing](https://github.com/mapbox/mapbox-maps-android/blob/master/CONTRIBUTING.md) or building an app that uses the Mapbox Maps Android Auto Extension. Please visit [DEVELOPING.md](https://github.com/mapbox/mapbox-maps-android/blob/master/DEVELOPING.md) for general information and instructions on how to use the Mapbox Maps Extension System. To add the android auto extension to your project, you configure its dependency in your `build.gradle` files.
```groovy
// In the root build.gradle file
@@ -28,39 +30,106 @@ allprojects {
}
}
-// In the app build.gradle file
+// In your build.gradle, add the extension with your other dependencies.
dependencies {
- implementation 'com.mapbox.extension:maps-androidauto:10.0.0-rc.9'
+ implementation 'com.mapbox.extension:maps-androidauto:10.4.+'
+
+ // Pick your versions of mapbox map and android auto.
+ implementation 'androidx.car.app:app:1.+'
+ implementation 'com.mapbox.maps:android:10.4.+'
}
```
-### Example
+## AndroidManifest and permissions
-Using the Mapbox Maps Android Auto Extension for Android could be done using:
+You should become familiar with [Google's documentation for building a navigation app](https://developer.android.com/training/cars/apps/navigation). You will need to declare your own [CarAppService](https://developer.android.com/reference/androidx/car/app/CarAppService), your own `minCarApiLevel`, create your own `xml/automotive_app_desc`, and make your own car app theme.
-```kotlin
-class MapSession : Session() {
- private lateinit var mapSurface: MapSurface
+The Mapbox Android Auto extension includes the `androidx.car.app.ACCESS_SURFACE` permission, you do not need to add this permission to your manifest. You will need to add the `androidx.car.app.NAVIGATION_TEMPLATES` permission, along with any other permission your app requires.
+
+``` xml
+
+
+
+
+
+
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+## Example Session and MapScreen
+In this example, the Session manages an instance of `MapboxCarMap` and then each `Screen` can register and unregister observers. Each screen can control map features. Your implementations of `MapboxCarMapObserver` give you control of the map.
+
+```kotlin
+class MySession : Session() {
override fun onCreateScreen(intent: Intent): Screen {
- val mapScreen = MapScreen(carContext)
- initMapSurface(scrollListener = carCameraController) { surface ->
- // Interact with the created MapSurface
- mapSurface = surface
- surface.getMapboxMap().loadStyleUri(Style.MAPBOX_STREETS) { style ->
- // Interact with the style
- ...
+ // You must create the MapInitOptions with the CarContext.
+ val mapInitOptions = MapInitOptions(carContext)
+ val mapboxCarMap = MapboxCarMap(mapInitOptions)
+ return MapScreen(mapboxCarMap)
+ }
+}
+
+class MyMapScreen(
+ val mapboxCarMap: MapboxCarMap
+) : Screen(mapboxCarMap.carContext) {
+
+ override fun onGetTemplate(): Template {
+ val builder = NavigationTemplate.Builder()
+ --snip--
+
+ init {
+ val myCustomMapExperience = MyCustomMapExperience()
+ lifecycle.addObserver(object : DefaultLifecycleObserver {
+ override fun onCreate(owner: LifecycleOwner) {
+ mapboxCarMap.registerObserver(myCustomMapExperience)
}
- }
- return if (carContext.checkSelfPermission(ACCESS_FINE_LOCATION) != PERMISSION_GRANTED) {
- carContext.getCarService(ScreenManager::class.java).push(mapScreen)
- RequestPermissionScreen(carContext)
- } else mapScreen
+ override fun onDestroy(owner: LifecycleOwner) {
+ mapboxCarMap.unregisterObserver(myCustomMapExperience)
+ }
+ })
}
-```
-More concrete examples of the Android Auto extension can be found in our [test application](https://github.com/mapbox/mapbox-maps-android/tree/master/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto).
+class MyCustomMapExperience : MapboxCarMapObserver {
+ override fun onAttached(mapboxCarMapSurface: MapboxCarMapSurface) {
+ val mapboxMap = mapboxCarMapSurface?.mapSurface?.getMapboxMap()!!
+ mapboxMap.loadStyle(
+ --snip--
+}
+```
#### Dependencies
diff --git a/extension-androidauto/build.gradle.kts b/extension-androidauto/build.gradle.kts
index 9a21f38f60..8e069daa3f 100644
--- a/extension-androidauto/build.gradle.kts
+++ b/extension-androidauto/build.gradle.kts
@@ -7,10 +7,10 @@ plugins {
}
android {
- compileSdkVersion(AndroidVersions.compileSdkVersion)
+ compileSdkVersion(AndroidVersions.compileSdkVersion_AndroidAuto)
defaultConfig {
- minSdkVersion(AndroidVersions.minAndroidAutoSdkVersion)
- targetSdkVersion(AndroidVersions.targetSdkVersion)
+ minSdkVersion(AndroidVersions.minSdkVersion_AndroidAuto)
+ targetSdkVersion(AndroidVersions.targetSdkVersion_AndroidAuto)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -23,10 +23,13 @@ android {
dependencies {
compileOnly(project(":sdk"))
+ testImplementation(project(":sdk"))
+
implementation(Dependencies.googleCarAppLibrary)
implementation(Dependencies.kotlin)
implementation(Dependencies.androidxCoreKtx)
implementation(Dependencies.androidxAnnotations)
+
testImplementation(Dependencies.junit)
testImplementation(Dependencies.mockk)
testImplementation(Dependencies.androidxTestCore)
@@ -50,6 +53,6 @@ project.apply {
from("$rootDir/gradle/ktlint.gradle")
from("$rootDir/gradle/lint.gradle")
from("$rootDir/gradle/jacoco.gradle")
-// from("$rootDir/gradle/sdk-registry.gradle")
+ from("$rootDir/gradle/sdk-registry.gradle")
from("$rootDir/gradle/track-public-apis.gradle")
}
\ No newline at end of file
diff --git a/extension-androidauto/src/main/AndroidManifest.xml b/extension-androidauto/src/main/AndroidManifest.xml
index 582dc0a176..9dbed1a4f5 100644
--- a/extension-androidauto/src/main/AndroidManifest.xml
+++ b/extension-androidauto/src/main/AndroidManifest.xml
@@ -1 +1,6 @@
-
+
+
+
+
+
diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CarMapSurfaceCallback.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CarMapSurfaceCallback.kt
new file mode 100644
index 0000000000..da845f30ee
--- /dev/null
+++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CarMapSurfaceCallback.kt
@@ -0,0 +1,78 @@
+package com.mapbox.maps.extension.androidauto
+
+import android.graphics.Rect
+import androidx.car.app.AppManager
+import androidx.car.app.CarContext
+import androidx.car.app.SurfaceCallback
+import androidx.car.app.SurfaceContainer
+import com.mapbox.common.Logger
+import com.mapbox.maps.MapInitOptions
+import com.mapbox.maps.MapboxExperimental
+
+/**
+ * @see MapboxCarMap to create new map experiences.
+ *
+ * This is a Mapbox implementation for the [SurfaceCallback]. It is used to simplify the lower
+ * level calls that manage the map surface. This class handles the surface callbacks and forwards
+ * them to the [CarMapSurfaceOwner] where [MapboxCarMapObserver] instances are notified.
+ */
+@OptIn(MapboxExperimental::class)
+internal class CarMapSurfaceCallback internal constructor(
+ private val carContext: CarContext,
+ private val carMapSurfaceOwner: CarMapSurfaceOwner,
+ private val mapInitOptions: MapInitOptions
+) : SurfaceCallback {
+
+ fun onBind() {
+ carContext.getCarService(AppManager::class.java).setSurfaceCallback(this)
+ }
+
+ override fun onSurfaceAvailable(surfaceContainer: SurfaceContainer) {
+ Logger.i(TAG, "onSurfaceAvailable $surfaceContainer")
+ surfaceContainer.surface?.let { surface ->
+ val mapSurface = MapSurfaceProvider.create(
+ carContext,
+ surface,
+ mapInitOptions
+ )
+ mapSurface.onStart()
+ mapSurface.surfaceCreated()
+ mapSurface.surfaceChanged(surfaceContainer.width, surfaceContainer.height)
+ val carMapSurface = MapboxCarMapSurface(carContext, mapSurface, surfaceContainer)
+ carMapSurfaceOwner.surfaceAvailable(carMapSurface)
+ }
+ }
+
+ override fun onVisibleAreaChanged(visibleArea: Rect) {
+ Logger.i(TAG, "onVisibleAreaChanged visibleArea:$visibleArea")
+ carMapSurfaceOwner.surfaceVisibleAreaChanged(visibleArea)
+ }
+
+ override fun onStableAreaChanged(stableArea: Rect) {
+ // Have not found a need for this.
+ }
+
+ override fun onScroll(distanceX: Float, distanceY: Float) {
+ Logger.i(TAG, "onScroll $distanceX, $distanceY")
+ carMapSurfaceOwner.scroll(distanceX, distanceY)
+ }
+
+ override fun onFling(velocityX: Float, velocityY: Float) {
+ Logger.i(TAG, "onFling $velocityX, $velocityY")
+ carMapSurfaceOwner.fling(velocityX, velocityY)
+ }
+
+ override fun onScale(focusX: Float, focusY: Float, scaleFactor: Float) {
+ Logger.i(TAG, "onScroll $focusX, $focusY, $scaleFactor")
+ carMapSurfaceOwner.scale(focusX, focusY, scaleFactor)
+ }
+
+ override fun onSurfaceDestroyed(surfaceContainer: SurfaceContainer) {
+ Logger.i(TAG, "onSurfaceDestroyed")
+ carMapSurfaceOwner.surfaceDestroyed()
+ }
+
+ private companion object {
+ private const val TAG = "CarMapSurfaceCallback"
+ }
+}
\ No newline at end of file
diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CarMapSurfaceOwner.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CarMapSurfaceOwner.kt
new file mode 100644
index 0000000000..6ce417beed
--- /dev/null
+++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CarMapSurfaceOwner.kt
@@ -0,0 +1,192 @@
+package com.mapbox.maps.extension.androidauto
+
+import android.graphics.Rect
+import com.mapbox.common.Logger
+import com.mapbox.maps.CameraOptions
+import com.mapbox.maps.EdgeInsets
+import com.mapbox.maps.MapboxExperimental
+import com.mapbox.maps.ScreenCoordinate
+import com.mapbox.maps.plugin.animation.camera
+import java.util.concurrent.CopyOnWriteArraySet
+
+/**
+ * @see MapboxCarMap to create new map experiences.
+ *
+ * Maintains the surface state for [MapboxCarMap].
+ */
+@OptIn(MapboxExperimental::class)
+internal class CarMapSurfaceOwner {
+
+ internal var mapboxCarMapSurface: MapboxCarMapSurface? = null
+ private set
+ internal var visibleArea: Rect? = null
+ private set
+ internal var edgeInsets: EdgeInsets? = null
+ private set
+ internal var visibleCenter: ScreenCoordinate = visibleCenter()
+ private set
+
+ private val carMapObservers = CopyOnWriteArraySet()
+
+ fun registerObserver(mapboxCarMapObserver: MapboxCarMapObserver) {
+ carMapObservers.add(mapboxCarMapObserver)
+ Logger.i(TAG, "registerObserver + 1 = ${carMapObservers.size}")
+
+ mapboxCarMapSurface?.let { carMapSurface ->
+ mapboxCarMapObserver.onAttached(carMapSurface)
+ }
+ ifNonNull(mapboxCarMapSurface, visibleArea, edgeInsets) { _, area, edge ->
+ Logger.i(TAG, "registerObserver visibleAreaChanged")
+ mapboxCarMapObserver.onVisibleAreaChanged(area, edge)
+ }
+ }
+
+ fun unregisterObserver(mapboxCarMapObserver: MapboxCarMapObserver) {
+ carMapObservers.remove(mapboxCarMapObserver)
+ mapboxCarMapSurface?.let { mapboxCarMapObserver.onDetached(it) }
+ Logger.i(TAG, "unregisterObserver - 1 = ${carMapObservers.size}")
+ }
+
+ fun clearObservers() {
+ this.mapboxCarMapSurface?.let { surface -> carMapObservers.forEach { it.onDetached(surface) } }
+ carMapObservers.clear()
+ }
+
+ fun surfaceAvailable(mapboxCarMapSurface: MapboxCarMapSurface) {
+ Logger.i(TAG, "surfaceAvailable")
+ val oldCarMapSurface = this.mapboxCarMapSurface
+ this.mapboxCarMapSurface = mapboxCarMapSurface
+ oldCarMapSurface?.let { carMapObservers.forEach { it.onDetached(oldCarMapSurface) } }
+ carMapObservers.forEach { it.onAttached(mapboxCarMapSurface) }
+
+ notifyVisibleAreaChanged()
+ }
+
+ fun surfaceDestroyed() {
+ Logger.i(TAG, "surfaceDestroyed")
+ val detachSurface = this.mapboxCarMapSurface
+ detachSurface?.mapSurface?.onStop()
+ detachSurface?.mapSurface?.surfaceDestroyed()
+ detachSurface?.mapSurface?.onDestroy()
+ this.mapboxCarMapSurface = null
+ detachSurface?.let { carMapObservers.forEach { it.onDetached(detachSurface) } }
+ }
+
+ fun surfaceVisibleAreaChanged(visibleArea: Rect) {
+ Logger.i(TAG, "surfaceVisibleAreaChanged")
+ this.visibleArea = visibleArea
+ notifyVisibleAreaChanged()
+ }
+
+ private fun notifyVisibleAreaChanged() {
+ this.edgeInsets = visibleArea?.edgeInsets()
+ this.visibleCenter = visibleCenter()
+ ifNonNull(mapboxCarMapSurface, visibleArea, edgeInsets) { _, area, edge ->
+ Logger.i(TAG, "notifyVisibleAreaChanged $area $edge")
+ carMapObservers.forEach {
+ it.onVisibleAreaChanged(area, edge)
+ }
+ }
+ }
+
+ private inline fun ifNonNull(
+ r1: R1?,
+ r2: R2?,
+ r3: R3?,
+ func: (R1, R2, R3) -> T
+ ): T? = if (r1 != null && r2 != null && r3 != null) {
+ func(r1, r2, r3)
+ } else {
+ null
+ }
+
+ private fun Rect.edgeInsets(): EdgeInsets? {
+ val surfaceContainer = mapboxCarMapSurface?.surfaceContainer ?: return null
+ return EdgeInsets(
+ top.toDouble(),
+ left.toDouble(),
+ (surfaceContainer.height - bottom).toDouble(),
+ (surfaceContainer.width - right).toDouble()
+ )
+ }
+
+ private fun visibleCenter(): ScreenCoordinate {
+ return visibleArea?.run(rectCenterMapper)
+ ?: mapboxCarMapSurface?.run(surfaceContainerCenterMapper)
+ ?: ScreenCoordinate(0.0, 0.0)
+ }
+
+ fun scroll(distanceX: Float, distanceY: Float) {
+ val carMapSurface = mapboxCarMapSurface ?: return
+ val handled = carMapObservers.any { it.onScroll(carMapSurface, distanceX, distanceY) }
+ if (handled) return
+
+ with(carMapSurface.mapSurface.getMapboxMap()) {
+ val fromCoordinate = visibleCenter
+ dragStart(fromCoordinate)
+ val toCoordinate = ScreenCoordinate(
+ fromCoordinate.x - distanceX,
+ fromCoordinate.y - distanceY
+ )
+ Logger.i(TAG, "scroll from $fromCoordinate to $toCoordinate")
+ setCamera(getDragCameraOptions(fromCoordinate, toCoordinate))
+ dragEnd()
+ }
+ }
+
+ fun fling(velocityX: Float, velocityY: Float) {
+ val carMapSurface = mapboxCarMapSurface ?: return
+ val handled = carMapObservers.any { it.onFling(carMapSurface, velocityX, velocityY) }
+ if (handled) return
+
+ Logger.i(TAG, "fling $velocityX, $velocityY")
+ // TODO implement fling
+ // https://github.com/mapbox/mapbox-navigation-android-examples/issues/67
+ }
+
+ fun scale(focusX: Float, focusY: Float, scaleFactor: Float) {
+ val carMapSurface = mapboxCarMapSurface ?: return
+ with(carMapSurface.mapSurface.getMapboxMap()) {
+ val fromZoom = cameraState.zoom
+ val toZoom = fromZoom - (1.0 - scaleFactor.toDouble())
+ val anchor = ScreenCoordinate(
+ focusX.toDouble(),
+ focusY.toDouble()
+ )
+ val handled = carMapObservers.any { it.onScale(carMapSurface, anchor, fromZoom, toZoom) }
+ if (handled) return
+
+ val cameraOptions = CameraOptions.Builder()
+ .zoom(toZoom)
+ .anchor(anchor)
+ .build()
+
+ Logger.i(TAG, "scale with $focusX, $focusY $scaleFactor -> $fromZoom $toZoom")
+ if (scaleFactor == DOUBLE_TAP_SCALE_FACTOR) {
+ carMapSurface.mapSurface.camera.easeTo(cameraOptions)
+ } else {
+ setCamera(cameraOptions)
+ }
+ }
+ }
+
+ private companion object {
+ private const val TAG = "CarMapSurfaceOwner"
+
+ private val rectCenterMapper = { rect: Rect ->
+ ScreenCoordinate(rect.exactCenterX().toDouble(), rect.exactCenterY().toDouble())
+ }
+
+ private val surfaceContainerCenterMapper = { carMapSurface: MapboxCarMapSurface ->
+ val container = carMapSurface.surfaceContainer
+ ScreenCoordinate(container.width / 2.0, container.height / 2.0)
+ }
+
+ /**
+ * This appears to be undocumented from android auto. But when running from the emulator,
+ * you can double tap the screen and zoom in to reproduce this value.
+ * It is a jarring experience if you do not easeTo the zoom.
+ */
+ private const val DOUBLE_TAP_SCALE_FACTOR = 2.0f
+ }
+}
\ No newline at end of file
diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapSurfaceProvider.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapSurfaceProvider.kt
new file mode 100644
index 0000000000..fc0788c39a
--- /dev/null
+++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapSurfaceProvider.kt
@@ -0,0 +1,17 @@
+package com.mapbox.maps.extension.androidauto
+
+import android.content.Context
+import android.view.Surface
+import com.mapbox.maps.MapInitOptions
+import com.mapbox.maps.MapSurface
+
+/**
+ * This provider is needed for creating unit tests.
+ */
+internal object MapSurfaceProvider {
+ fun create(
+ context: Context,
+ surface: Surface,
+ mapInitOptions: MapInitOptions
+ ) = MapSurface(context, surface, mapInitOptions)
+}
\ No newline at end of file
diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarMap.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarMap.kt
new file mode 100644
index 0000000000..5abf70f529
--- /dev/null
+++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarMap.kt
@@ -0,0 +1,102 @@
+package com.mapbox.maps.extension.androidauto
+
+import android.graphics.Rect
+import androidx.car.app.AppManager
+import androidx.car.app.CarContext
+import androidx.lifecycle.Lifecycle
+import com.mapbox.maps.EdgeInsets
+import com.mapbox.maps.MapInitOptions
+import com.mapbox.maps.MapboxExperimental
+
+/**
+ * This is the main entry point for controlling the Mapbox car map surface.
+ *
+ * You can add the [MapboxCarMap] to your entire [androidx.car.app.Session] using the [Lifecycle].
+ * And then any [androidx.car.app.Screen] that is using a supported template will automatically
+ * show the Mapbox map. You can also, specify a [MapboxCarMap] for each individual
+ * [androidx.car.app.Screen] by constructing the [MapboxCarMap] with the screen Lifecycle.
+ *
+ * Supported templates include:
+ * - [androidx.car.app.navigation.model.NavigationTemplate]
+ * - [androidx.car.app.navigation.model.RoutePreviewNavigationTemplate]
+ * - [androidx.car.app.navigation.model.PlaceListNavigationTemplate]
+ *
+ * Customize your [MapboxCarMap] with your own implementations of [MapboxCarMapObserver].
+ * Use the [registerObserver] and [unregisterObserver] functions to load and detach the observers.
+ *
+ * The internals of this class use [AppManager.setSurfaceCallback], which assumes there is a single
+ * surface callback. Do not use setSurfaceCallback, and do not create multiple instances of
+ * [MapboxCarMap].
+ *
+ * @param mapInitOptions to initialize the [MapboxCarMapSurface]
+ */
+@MapboxExperimental
+class MapboxCarMap(
+ private val mapInitOptions: MapInitOptions
+) {
+ /**
+ * Accessor for the carContext provided to the MapInitOptions. This makes it easier to create
+ * screens with the MapboxCarMap in the constructor.
+ *
+ * For example:
+ * class YourMapScreen(val mapboxCarMap: MapboxCarMap) : Screen(mapboxCarMap.carContext) {
+ *
+ * The carContext can also be found in the [MapboxCarMapObserver] callbacks. Make sure to
+ * call [MapboxCarMap.clearObservers] when your car session is destroyed.
+ */
+ val carContext: CarContext = mapInitOptions.context as? CarContext
+ ?: throw IllegalArgumentException("You must construct a MapboxCarMap with the CarContext")
+
+ private val carMapSurfaceOwner = CarMapSurfaceOwner()
+ private val carMapSurfaceCallback = CarMapSurfaceCallback(
+ carContext,
+ carMapSurfaceOwner,
+ mapInitOptions
+ )
+
+ init {
+ carMapSurfaceCallback.onBind()
+ }
+
+ /**
+ * Returns the current [MapboxCarMapSurface]. It is recommended to use [registerObserver] and
+ * [MapboxCarMapObserver] to attach and detach your customizations.
+ */
+ val carMapSurface: MapboxCarMapSurface?
+ get() { return carMapSurfaceOwner.mapboxCarMapSurface }
+
+ /**
+ * Accessor to the visible area calculated by the car library. It is recommended to
+ * use the values returned by [MapboxCarMapObserver.onVisibleAreaChanged].
+ */
+ val visibleArea: Rect?
+ get() { return carMapSurfaceOwner.visibleArea }
+
+ /**
+ * Accessor to the edgeInsets calculated by the car library. It is recommended to
+ * use the values returned by [MapboxCarMapObserver.onVisibleAreaChanged].
+ */
+ val edgeInsets: EdgeInsets?
+ get() { return carMapSurfaceOwner.edgeInsets }
+
+ /**
+ * @param mapboxCarMapObserver implements the desired mapbox car experiences
+ */
+ fun registerObserver(mapboxCarMapObserver: MapboxCarMapObserver) = apply {
+ carMapSurfaceOwner.registerObserver(mapboxCarMapObserver)
+ }
+
+ /**
+ * @param mapboxCarMapObserver the instance used in [registerObserver]
+ */
+ fun unregisterObserver(mapboxCarMapObserver: MapboxCarMapObserver) {
+ carMapSurfaceOwner.unregisterObserver(mapboxCarMapObserver)
+ }
+
+ /**
+ * Optional function to clear all observers registered through [registerObserver]
+ */
+ fun clearObservers() {
+ carMapSurfaceOwner.clearObservers()
+ }
+}
\ No newline at end of file
diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarMapObserver.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarMapObserver.kt
new file mode 100644
index 0000000000..94ae421ac0
--- /dev/null
+++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarMapObserver.kt
@@ -0,0 +1,120 @@
+package com.mapbox.maps.extension.androidauto
+
+import android.graphics.Rect
+import androidx.car.app.SurfaceCallback
+import com.mapbox.maps.EdgeInsets
+import com.mapbox.maps.MapboxExperimental
+import com.mapbox.maps.ScreenCoordinate
+
+/**
+ * Many downstream services will not work until the surface has been created and the map has
+ * loaded. This interface allows you to create custom Mapbox experiences for the car.
+ */
+@MapboxExperimental
+interface MapboxCarMapObserver {
+
+ /**
+ * Called when a [MapboxCarMapSurface] has been loaded.
+ * You can assume there will only be a single surface at a time.
+ *
+ * @see [MapboxCarMap.registerObserver]
+ *
+ * @param mapboxCarMapSurface loaded and ready to use car map surface
+ */
+ fun onAttached(mapboxCarMapSurface: MapboxCarMapSurface) {
+ // No op by default
+ }
+
+ /**
+ * Called when a [MapboxCarMapSurface] has been detached from this observer. Some examples that
+ * can cause this to detach are:
+ * - [MapboxCarMap] lifecycle is destroyed
+ * - This observer has been unregistered with [MapboxCarMap.unregisterObserver]
+ *
+ * You can assume that there was a corresponding call to [onAttached] with the same
+ * [MapboxCarMapObserver] instance.
+ *
+ * @param mapboxCarMapSurface loaded and ready car map surface
+ */
+ fun onDetached(mapboxCarMapSurface: MapboxCarMapSurface) {
+ // No op by default
+ }
+
+ /**
+ * Called when the car library updates the visible regions for the surface. For example, this
+ * is triggered when the action buttons come in and out of visibility.
+ * You can assume this will be called after [onAttached].
+ *
+ * @param visibleArea the visible area provided by the host
+ * @param edgeInsets distance from each side of the screen that creates the [visibleArea]
+ */
+ fun onVisibleAreaChanged(visibleArea: Rect, edgeInsets: EdgeInsets) {
+ // No op by default
+ }
+
+ /**
+ * Allows you to implement or observe the map scroll gesture handler. The surface is [onAttached]
+ * before this can be triggered.
+ *
+ * @see [SurfaceCallback.onScroll] for instructions to enable.
+ *
+ * @param mapboxCarMapSurface loaded and ready car map surface
+ * @param distanceX the distance in pixels along the X axis
+ * @param distanceY the distance in pixels along the Y axis
+ *
+ * @return true when the fling scroll was handled, false will trigger the default handler
+ */
+ fun onScroll(
+ mapboxCarMapSurface: MapboxCarMapSurface,
+ distanceX: Float,
+ distanceY: Float
+ ): Boolean {
+ // By default, scroll is handled internally
+ return false
+ }
+
+ /**
+ * Allows you to implement or observe the map fling gesture handler. The surface is [onAttached]
+ * before this can be triggered.
+ *
+ * @see [SurfaceCallback.onFling] for instructions to enable.
+ *
+ * @param mapboxCarMapSurface loaded and ready car map surface
+ * @param velocityX the velocity of this fling measured in pixels per second along the x axis
+ * @param velocityY the velocity of this fling measured in pixels per second along the y axis
+ *
+ * @return true when the fling call was handled, false will trigger the default handler
+ */
+ fun onFling(
+ mapboxCarMapSurface: MapboxCarMapSurface,
+ velocityX: Float,
+ velocityY: Float
+ ): Boolean {
+ // By default, fling is handled internally
+ return false
+ }
+
+ /**
+ * Allows you to implement or observe the map scale gesture handler. The surface is [onAttached]
+ * before this can be triggered.
+ *
+ * @see [SurfaceCallback.onScroll] for instructions to enable.
+ *
+ * @param mapboxCarMapSurface loaded and ready car map surface
+ * @param anchor the focus point in pixels for the zooming gesture
+ * @param fromZoom the current zoom of the Mapbox camera
+ * @param toZoom the new zoom that will be set if the function returns false
+ *
+ * @return true when the scale call was handled, false will trigger the default handler
+ */
+ @Suppress("LongParameterList")
+ fun onScale(
+ mapboxCarMapSurface: MapboxCarMapSurface,
+ anchor: ScreenCoordinate,
+ fromZoom: Double,
+ toZoom: Double
+ ): Boolean {
+ // By default, scale is handled internally
+ return false
+ }
+}
\ No newline at end of file
diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarMapSurface.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarMapSurface.kt
new file mode 100644
index 0000000000..5606f25c73
--- /dev/null
+++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarMapSurface.kt
@@ -0,0 +1,33 @@
+package com.mapbox.maps.extension.androidauto
+
+import androidx.car.app.CarContext
+import androidx.car.app.SurfaceContainer
+import com.mapbox.maps.MapSurface
+import com.mapbox.maps.MapboxExperimental
+
+/**
+ * This contains the Android Auto head unit map information.
+ * @see MapboxCarMap.registerObserver
+ *
+ * @property carContext reference to the context provided to the [MapboxCarMap]
+ * @property mapSurface Mapbox controllable interface
+ * @property surfaceContainer A container for the Surface created by the car.
+ */
+@MapboxExperimental
+class MapboxCarMapSurface internal constructor(
+ val carContext: CarContext,
+ val mapSurface: MapSurface,
+ val surfaceContainer: SurfaceContainer,
+) {
+ /**
+ * Get a string representation of the map surface.
+ *
+ * @return the string representation
+ */
+ override fun toString(): String {
+ return "MapboxCarMapSurface(carContext=$carContext," +
+ " mapSurface=$mapSurface," +
+ " surfaceContainer=$surfaceContainer" +
+ ")"
+ }
+}
\ No newline at end of file
diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarUtils.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarUtils.kt
deleted file mode 100644
index b1a1a02ad6..0000000000
--- a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarUtils.kt
+++ /dev/null
@@ -1,191 +0,0 @@
-package com.mapbox.maps.extension.androidauto
-
-import android.graphics.Rect
-import androidx.car.app.AppManager
-import androidx.car.app.Session
-import androidx.car.app.SurfaceCallback
-import androidx.car.app.SurfaceContainer
-import androidx.lifecycle.DefaultLifecycleObserver
-import androidx.lifecycle.LifecycleOwner
-import com.mapbox.common.Logger
-import com.mapbox.maps.*
-
-/**
- * MapSurface ready callback, will be called when the MapSurface is created successfully.
- */
-@MapboxExperimental
-fun interface MapSurfaceReadyCallback {
- /**
- * Map surface is ready.
- */
- fun onMapSurfaceReady(mapSurface: MapSurface)
-}
-
-/**
- * Fired when there's a map scroll event.
- */
-@MapboxExperimental
-fun interface OnMapScrollListener {
- /**
- * Map scroll event fired.
- */
- fun onMapScroll()
-}
-
-/**
- * Fired when there's a map scale event.
- */
-@MapboxExperimental
-fun interface OnMapScaleListener {
- /**
- * Map scroll event fired.
- */
- fun onMapScale()
-}
-
-/**
- * Init Mapbox Map surface for the car app.
- *
- * @param mapInitOptions options to initialise a MapboxMap
- * @param scrollListener
- * @param scaleListener
- * @param mapSurfaceReadyCallback
- */
-@MapboxExperimental
-@JvmOverloads
-fun Session.initMapSurface(
- mapInitOptions: MapInitOptions = MapInitOptions(carContext),
- scrollListener: OnMapScrollListener? = null,
- scaleListener: OnMapScaleListener? = null,
- mapSurfaceReadyCallback: MapSurfaceReadyCallback
-) {
- var mapSurface: MapSurface? = null
- val surfaceCallback: SurfaceCallback = object : SurfaceCallback {
- override fun onSurfaceAvailable(surfaceContainer: SurfaceContainer) {
- synchronized(this) {
- Logger.i(TAG, "Surface available $surfaceContainer")
- surfaceContainer.surface?.let { surface ->
- mapSurface = MapSurface(carContext, surface, mapInitOptions).also { surface ->
- surface.surfaceCreated()
- surface.surfaceChanged(surfaceContainer.width, surfaceContainer.height)
- mapSurfaceReadyCallback.onMapSurfaceReady(surface)
- }
- }
- }
- }
-
- override fun onVisibleAreaChanged(visibleArea: Rect) {
- synchronized(this) {
- Logger.i(
- TAG,
- "Visible area changed $visibleArea"
- )
- }
- }
-
- override fun onStableAreaChanged(stableArea: Rect) {
- synchronized(this) {
- Logger.i(
- TAG,
- "Stable area changed $stableArea"
- )
- }
- }
-
- override fun onSurfaceDestroyed(surfaceContainer: SurfaceContainer) {
- synchronized(this) {
- Logger.i(TAG, "Surface destroyed")
- mapSurface?.surfaceDestroyed()
- }
- }
-
- override fun onScroll(distanceX: Float, distanceY: Float) {
- Logger.d(TAG, "onScroll $distanceX, $distanceY")
- synchronized(this) {
- mapSurface?.onScroll(distanceX, distanceY)
- scrollListener?.onMapScroll()
- }
- }
-
- override fun onScale(focusX: Float, focusY: Float, scaleFactor: Float) {
- Logger.d(TAG, "onScroll $focusX, $focusY, $scaleFactor")
- synchronized(this) {
- mapSurface?.onScale(focusX, focusY, scaleFactor)
- scaleListener?.onMapScale()
- }
- }
- }
-
- lifecycle.addObserver(
- object : DefaultLifecycleObserver {
- override fun onCreate(owner: LifecycleOwner) {
- Logger.i(TAG, "SurfaceRenderer created")
- synchronized(this) {
- carContext.getCarService(AppManager::class.java)
- .setSurfaceCallback(surfaceCallback)
- }
- }
-
- override fun onStart(owner: LifecycleOwner) {
- Logger.i(TAG, "onStart")
- synchronized(this) {
- mapSurface?.onStart()
- }
- }
-
- override fun onStop(owner: LifecycleOwner) {
- Logger.i(TAG, "onStop")
- synchronized(this) {
- mapSurface?.onStop()
- }
- }
-
- override fun onDestroy(owner: LifecycleOwner) {
- Logger.i(TAG, "onDestroy")
- synchronized(this) {
- mapSurface?.onDestroy()
- }
- }
- }
- )
-}
-
-private fun MapSurface.onScroll(distanceX: Float, distanceY: Float) {
- Logger.i(TAG, "handleScroll $distanceX, $distanceY")
- synchronized(this) {
- val centerScreen = ScreenCoordinate(0.0, 0.0)
- getMapboxMap().apply {
- dragStart(centerScreen)
- val newCamera = getMapboxMap().getDragCameraOptions(
- centerScreen,
- ScreenCoordinate(
- centerScreen.x - distanceX,
- centerScreen.y - distanceY
- )
- )
- setCamera(newCamera)
- dragEnd()
- }
- }
-}
-
-private fun MapSurface.onScale(focusX: Float, focusY: Float, scaleFactor: Float) {
- Logger.i(TAG, "handleScale $focusX, $focusY. $scaleFactor")
- synchronized(this) {
- val cameraState = getMapboxMap().cameraState
- Logger.i(TAG, "setting zoom ${cameraState.zoom * scaleFactor}")
- val newZoom = cameraState.zoom * scaleFactor
- val cameraOptionsBuilder = CameraOptions.Builder().zoom(newZoom)
- if (focusX >= 0 && focusY >= 0) {
- cameraOptionsBuilder.anchor(
- ScreenCoordinate(
- focusX.toDouble(),
- focusY.toDouble()
- )
- )
- }
- getMapboxMap().setCamera(cameraOptionsBuilder.build())
- }
-}
-
-private const val TAG = "MapboxCarMap"
\ No newline at end of file
diff --git a/extension-androidauto/src/test/java/com/mapbox/maps/extension/androidauto/CarMapSurfaceCallbackTest.kt b/extension-androidauto/src/test/java/com/mapbox/maps/extension/androidauto/CarMapSurfaceCallbackTest.kt
new file mode 100644
index 0000000000..fef99043f1
--- /dev/null
+++ b/extension-androidauto/src/test/java/com/mapbox/maps/extension/androidauto/CarMapSurfaceCallbackTest.kt
@@ -0,0 +1,118 @@
+package com.mapbox.maps.extension.androidauto
+
+import android.graphics.Rect
+import androidx.car.app.AppManager
+import androidx.car.app.CarContext
+import androidx.car.app.SurfaceCallback
+import com.mapbox.common.Logger
+import com.mapbox.maps.MapInitOptions
+import com.mapbox.maps.MapSurface
+import com.mapbox.maps.MapboxExperimental
+import com.mapbox.maps.Style
+import io.mockk.Runs
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.mockkObject
+import io.mockk.mockkStatic
+import io.mockk.slot
+import io.mockk.unmockkAll
+import io.mockk.verify
+import io.mockk.verifyOrder
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+
+@OptIn(MapboxExperimental::class)
+class CarMapSurfaceCallbackTest {
+
+ private val carContext: CarContext = mockk(relaxed = true)
+ private val carMapSurfaceOwner: CarMapSurfaceOwner = mockk()
+ private val mapboxCarOptions: MapInitOptions = mockk(relaxed = true)
+ private val testMapSurface: MapSurface = mockk(relaxed = true)
+
+ private val carMapSurfaceCallback = CarMapSurfaceCallback(
+ carContext,
+ carMapSurfaceOwner,
+ mapboxCarOptions
+ )
+
+ @Before
+ fun setup() {
+ mockkStatic(Logger::class)
+ every { Logger.i(any(), any()) } just Runs
+ mockkObject(MapSurfaceProvider)
+ every { MapSurfaceProvider.create(any(), any(), any()) } returns testMapSurface
+ }
+
+ @After
+ fun teardown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun `onCreate should request the map surface with the SurfaceCallback`() {
+ val surfaceCallback = slot()
+ every { carContext.getCarService(AppManager::class.java) } returns mockk {
+ every { setSurfaceCallback(capture(surfaceCallback)) } just Runs
+ }
+
+ carMapSurfaceCallback.onBind()
+
+ assertTrue(surfaceCallback.isCaptured)
+ assertEquals(surfaceCallback.captured, carMapSurfaceCallback)
+ }
+
+ @Test
+ fun `onSurfaceAvailable should notify surfaceAvailable when style is loaded`() {
+ every { testMapSurface.getMapboxMap() } returns mockk(relaxed = true) {
+ every { loadStyleUri(any(), any(), any()) } answers {
+ secondArg().onStyleLoaded(mockk())
+ }
+ }
+ val carMapSurfaceSlot = slot()
+ every { carMapSurfaceOwner.surfaceAvailable(capture(carMapSurfaceSlot)) } just Runs
+
+ carMapSurfaceCallback.onSurfaceAvailable(
+ mockk {
+ every { surface } returns mockk()
+ every { width } returns 800
+ every { height } returns 400
+ }
+ )
+
+ verifyOrder {
+ testMapSurface.surfaceChanged(800, 400)
+ carMapSurfaceOwner.surfaceAvailable(any())
+ }
+ }
+
+ @Test
+ fun `onVisibleAreaChanged should notify carMapSurfaceOwner surfaceVisibleAreaChanged`() {
+ val visibleRect = mockk()
+ every { carMapSurfaceOwner.surfaceVisibleAreaChanged(any()) } just Runs
+
+ carMapSurfaceCallback.onVisibleAreaChanged(visibleRect)
+
+ verify(exactly = 1) { carMapSurfaceOwner.surfaceVisibleAreaChanged(visibleRect) }
+ }
+
+ @Test
+ fun `onStableAreaChanged should not do anything`() {
+ carMapSurfaceCallback.onStableAreaChanged(mockk())
+
+ verify(exactly = 0) { carMapSurfaceOwner.surfaceVisibleAreaChanged(any()) }
+ verify(exactly = 0) { carMapSurfaceOwner.surfaceDestroyed() }
+ }
+
+ @Test
+ fun `onSurfaceDestroyed should notify carMapSurfaceOwner surfaceDestroyed`() {
+ every { carMapSurfaceOwner.surfaceDestroyed() } just Runs
+
+ carMapSurfaceCallback.onSurfaceDestroyed(mockk())
+
+ verify(exactly = 1) { carMapSurfaceOwner.surfaceDestroyed() }
+ }
+}
\ No newline at end of file
diff --git a/extension-androidauto/src/test/java/com/mapbox/maps/extension/androidauto/CarMapSurfaceOwnerTest.kt b/extension-androidauto/src/test/java/com/mapbox/maps/extension/androidauto/CarMapSurfaceOwnerTest.kt
new file mode 100644
index 0000000000..9443fb3081
--- /dev/null
+++ b/extension-androidauto/src/test/java/com/mapbox/maps/extension/androidauto/CarMapSurfaceOwnerTest.kt
@@ -0,0 +1,620 @@
+@file:Suppress("NoMockkVerifyImport")
+
+package com.mapbox.maps.extension.androidauto
+
+import android.graphics.Rect
+import com.mapbox.maps.CameraOptions
+import com.mapbox.maps.EdgeInsets
+import com.mapbox.maps.MapSurface
+import com.mapbox.maps.MapboxExperimental
+import com.mapbox.maps.MapboxMap
+import com.mapbox.maps.ScreenCoordinate
+import com.mapbox.maps.extension.androidauto.testing.ShadowLogger
+import com.mapbox.maps.plugin.animation.CameraAnimationsPlugin
+import com.mapbox.maps.plugin.animation.camera
+import io.mockk.Runs
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.slot
+import io.mockk.verify
+import io.mockk.verifyOrder
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+@OptIn(MapboxExperimental::class)
+@RunWith(RobolectricTestRunner::class)
+@Config(shadows = [ShadowLogger::class])
+class CarMapSurfaceOwnerTest {
+
+ private val carMapSurfaceOwner = CarMapSurfaceOwner()
+
+ @Test
+ fun `should not notify observer loaded when there is no surface`() {
+ val firstObserver: MapboxCarMapObserver = mockk(relaxed = true)
+ val secondObserver: MapboxCarMapObserver = mockk(relaxed = true)
+ carMapSurfaceOwner.registerObserver(firstObserver)
+ carMapSurfaceOwner.registerObserver(secondObserver)
+ carMapSurfaceOwner.unregisterObserver(firstObserver)
+ carMapSurfaceOwner.clearObservers()
+
+ verify(exactly = 0) { firstObserver.onAttached(any()) }
+ verify(exactly = 0) { secondObserver.onAttached(any()) }
+ }
+
+ @Test
+ fun `should not notify observer detached when there is no surface`() {
+ val firstObserver: MapboxCarMapObserver = mockk(relaxed = true)
+ val secondObserver: MapboxCarMapObserver = mockk(relaxed = true)
+ carMapSurfaceOwner.registerObserver(firstObserver)
+ carMapSurfaceOwner.registerObserver(secondObserver)
+ carMapSurfaceOwner.unregisterObserver(firstObserver)
+ carMapSurfaceOwner.clearObservers()
+
+ verify(exactly = 0) { firstObserver.onDetached(any()) }
+ verify(exactly = 0) { secondObserver.onDetached(any()) }
+ }
+
+ @Test
+ fun `surfaceAvailable should notify observers that map is loaded`() {
+ val firstObserver: MapboxCarMapObserver = mockk(relaxed = true)
+ val secondObserver: MapboxCarMapObserver = mockk(relaxed = true)
+ carMapSurfaceOwner.registerObserver(firstObserver)
+ carMapSurfaceOwner.registerObserver(secondObserver)
+
+ val mapboxCarMapSurface: MapboxCarMapSurface = mockk(relaxed = true)
+ carMapSurfaceOwner.surfaceAvailable(mapboxCarMapSurface)
+
+ verify(exactly = 1) { firstObserver.onAttached(mapboxCarMapSurface) }
+ verify(exactly = 1) { secondObserver.onAttached(mapboxCarMapSurface) }
+ }
+
+ @Test
+ fun `surfaceAvailable should not notify visibleAreaChanged when visible area is null`() {
+ val firstObserver: MapboxCarMapObserver = mockk(relaxed = true)
+ val secondObserver: MapboxCarMapObserver = mockk(relaxed = true)
+ carMapSurfaceOwner.registerObserver(firstObserver)
+ carMapSurfaceOwner.registerObserver(secondObserver)
+
+ val mapboxCarMapSurface: MapboxCarMapSurface = mockk(relaxed = true)
+ carMapSurfaceOwner.surfaceAvailable(mapboxCarMapSurface)
+
+ verify(exactly = 0) { firstObserver.onVisibleAreaChanged(any(), any()) }
+ verify(exactly = 0) { secondObserver.onVisibleAreaChanged(any(), any()) }
+ }
+
+ @Test
+ fun `surfaceVisibleAreaChanged should notify visibleAreaChanged when surface is available`() {
+ val firstObserver: MapboxCarMapObserver = mockk(relaxed = true)
+ val secondObserver: MapboxCarMapObserver = mockk(relaxed = true)
+ carMapSurfaceOwner.registerObserver(firstObserver)
+ carMapSurfaceOwner.registerObserver(secondObserver)
+
+ val mapboxCarMapSurface: MapboxCarMapSurface = mockk {
+ every { surfaceContainer } returns mockk {
+ every { width } returns 800
+ every { height } returns 400
+ }
+ }
+ carMapSurfaceOwner.surfaceAvailable(mapboxCarMapSurface)
+ val visibleRect: Rect = mockk(relaxed = true)
+ carMapSurfaceOwner.surfaceVisibleAreaChanged(visibleRect)
+
+ verify(exactly = 1) { firstObserver.onVisibleAreaChanged(any(), any()) }
+ verify(exactly = 1) { secondObserver.onVisibleAreaChanged(any(), any()) }
+ }
+
+ @Test
+ fun `surfaceVisibleAreaChanged should not notify visibleAreaChanged when surface is not available`() {
+ val firstObserver: MapboxCarMapObserver = mockk(relaxed = true)
+ val secondObserver: MapboxCarMapObserver = mockk(relaxed = true)
+ carMapSurfaceOwner.registerObserver(firstObserver)
+ carMapSurfaceOwner.registerObserver(secondObserver)
+
+ val visibleRect: Rect = mockk(relaxed = true)
+ carMapSurfaceOwner.surfaceVisibleAreaChanged(visibleRect)
+
+ verify(exactly = 0) { firstObserver.onVisibleAreaChanged(any(), any()) }
+ verify(exactly = 0) { secondObserver.onVisibleAreaChanged(any(), any()) }
+ }
+
+ @Test
+ fun `surfaceDestroyed should stop and destroy map before notifying observers`() {
+ val observer: MapboxCarMapObserver = mockk(relaxed = true)
+ carMapSurfaceOwner.registerObserver(observer)
+ val testMapSurface = mockk(relaxed = true)
+ val mapboxCarMapSurface: MapboxCarMapSurface = mockk {
+ every { surfaceContainer } returns mockk {
+ every { width } returns 800
+ every { height } returns 400
+ }
+ every { mapSurface } returns testMapSurface
+ }
+
+ carMapSurfaceOwner.surfaceAvailable(mapboxCarMapSurface)
+ carMapSurfaceOwner.surfaceDestroyed()
+
+ verifyOrder {
+ testMapSurface.onStop()
+ testMapSurface.surfaceDestroyed()
+ testMapSurface.onDestroy()
+ observer.onDetached(mapboxCarMapSurface)
+ }
+ }
+
+ @Test
+ fun `onDetached is called after mapboxCarMapSurface becomes null when surfaceDestroyed`() {
+ carMapSurfaceOwner.registerObserver(object : MapboxCarMapObserver {
+ override fun onDetached(mapboxCarMapSurface: MapboxCarMapSurface) {
+ assertNull(carMapSurfaceOwner.mapboxCarMapSurface)
+ }
+ })
+
+ val testMapSurface = mockk(relaxed = true)
+ val mapboxCarMapSurface: MapboxCarMapSurface = mockk {
+ every { surfaceContainer } returns mockk {
+ every { width } returns 800
+ every { height } returns 400
+ }
+ every { mapSurface } returns testMapSurface
+ }
+
+ carMapSurfaceOwner.surfaceAvailable(mapboxCarMapSurface)
+ carMapSurfaceOwner.surfaceDestroyed()
+ }
+
+ @Test
+ fun `onDetached is called after mapboxCarMapSurface becomes null when new surfaceAvailable`() {
+ carMapSurfaceOwner.registerObserver(object : MapboxCarMapObserver {
+ override fun onDetached(mapboxCarMapSurface: MapboxCarMapSurface) {
+ assertNotEquals(carMapSurfaceOwner.mapboxCarMapSurface, mapboxCarMapSurface)
+ }
+ })
+
+ val firstMapSurface = mockk(relaxed = true)
+ val firstSurface = mockk {
+ every { surfaceContainer } returns mockk {
+ every { width } returns 800
+ every { height } returns 400
+ }
+ every { mapSurface } returns firstMapSurface
+ }
+ val secondSurface = mockk {
+ every { surfaceContainer } returns mockk {
+ every { width } returns 800
+ every { height } returns 400
+ }
+ }
+
+ carMapSurfaceOwner.surfaceAvailable(firstSurface)
+ carMapSurfaceOwner.surfaceVisibleAreaChanged(mockk(relaxed = true))
+ carMapSurfaceOwner.surfaceAvailable(secondSurface)
+ }
+
+ @Test
+ fun `should notify destroy and detached old surface when new surface is available`() {
+ val observer: MapboxCarMapObserver = mockk(relaxed = true)
+ carMapSurfaceOwner.registerObserver(observer)
+ val firstMapSurface = mockk(relaxed = true)
+ val firstSurface = mockk {
+ every { surfaceContainer } returns mockk {
+ every { width } returns 800
+ every { height } returns 400
+ }
+ every { mapSurface } returns firstMapSurface
+ }
+ val secondSurface = mockk {
+ every { surfaceContainer } returns mockk {
+ every { width } returns 800
+ every { height } returns 400
+ }
+ }
+
+ carMapSurfaceOwner.surfaceAvailable(firstSurface)
+ carMapSurfaceOwner.surfaceVisibleAreaChanged(mockk(relaxed = true))
+ carMapSurfaceOwner.surfaceAvailable(secondSurface)
+
+ verifyOrder {
+ observer.onAttached(firstSurface)
+ observer.onVisibleAreaChanged(any(), any())
+ observer.onDetached(firstSurface)
+ observer.onAttached(secondSurface)
+ observer.onVisibleAreaChanged(any(), any())
+ }
+ // Map style changes should not destroy the map.
+ verify(exactly = 0) { firstMapSurface.onStop() }
+ verify(exactly = 0) { firstMapSurface.surfaceDestroyed() }
+ verify(exactly = 0) { firstMapSurface.onDestroy() }
+ }
+
+ @Test
+ fun `surfaceVisibleAreaChanged should notify visibleAreaChanged with edgeInsets`() {
+ val observer: MapboxCarMapObserver = mockk(relaxed = true)
+ carMapSurfaceOwner.registerObserver(observer)
+ val visibleAreaSlot = slot()
+ val edgeInsets = slot()
+ every {
+ observer.onVisibleAreaChanged(capture(visibleAreaSlot), capture(edgeInsets))
+ } just Runs
+ val mapboxCarMapSurface: MapboxCarMapSurface = mockk {
+ every { surfaceContainer } returns mockk {
+ every { width } returns 800
+ every { height } returns 400
+ }
+ }
+
+ carMapSurfaceOwner.surfaceAvailable(mapboxCarMapSurface)
+ val visibleRect = Rect(30, 112, 779, 381)
+ carMapSurfaceOwner.surfaceVisibleAreaChanged(visibleRect)
+
+ assertEquals(visibleRect, visibleAreaSlot.captured)
+ // edgeInset.left = visibleRect.left = 30
+ assertEquals(30.0, edgeInsets.captured.left, 0.0001)
+ // edgeInset.top = visibleRect.top = 112
+ assertEquals(112.0, edgeInsets.captured.top, 0.0001)
+ // edgeInset.right = surfaceContainer.width - visibleRect.right = 800 - 779 = 21
+ assertEquals(21.0, edgeInsets.captured.right, 0.0001)
+ // edgeInsets.bottom = surfaceContainer.height - visibleRect.bottom = 400 - 381 = 19
+ assertEquals(19.0, edgeInsets.captured.bottom, 0.0001)
+ }
+
+ @Test
+ fun `visibleCenter is zero by default`() {
+ val visibleCenter = carMapSurfaceOwner.visibleCenter
+ assertEquals(0.0, visibleCenter.x, 0.0001)
+ assertEquals(0.0, visibleCenter.y, 0.0001)
+ }
+
+ @Test
+ fun `surfaceAvailable finds the visibleCenter`() {
+ val observer: MapboxCarMapObserver = mockk(relaxed = true)
+ carMapSurfaceOwner.registerObserver(observer)
+ val visibleAreaSlot = slot()
+ val edgeInsets = slot()
+ every {
+ observer.onVisibleAreaChanged(capture(visibleAreaSlot), capture(edgeInsets))
+ } just Runs
+ val mapboxCarMapSurface: MapboxCarMapSurface = mockk {
+ every { surfaceContainer } returns mockk {
+ every { width } returns 805
+ every { height } returns 405
+ }
+ }
+
+ carMapSurfaceOwner.surfaceAvailable(mapboxCarMapSurface)
+ val visibleCenter = carMapSurfaceOwner.visibleCenter
+
+ assertEquals(402.5, visibleCenter.x, 0.0001)
+ assertEquals(202.5, visibleCenter.y, 0.0001)
+ }
+
+ @Test
+ fun `surfaceVisibleAreaChanged finds the visibleCenter`() {
+ val observer: MapboxCarMapObserver = mockk(relaxed = true)
+ carMapSurfaceOwner.registerObserver(observer)
+ val visibleAreaSlot = slot()
+ val edgeInsets = slot()
+ every {
+ observer.onVisibleAreaChanged(capture(visibleAreaSlot), capture(edgeInsets))
+ } just Runs
+ val mapboxCarMapSurface: MapboxCarMapSurface = mockk {
+ every { surfaceContainer } returns mockk {
+ every { width } returns 805
+ every { height } returns 405
+ }
+ }
+
+ carMapSurfaceOwner.surfaceAvailable(mapboxCarMapSurface)
+ val visibleRect = Rect(133, 112, 779, 381)
+ carMapSurfaceOwner.surfaceVisibleAreaChanged(visibleRect)
+
+ val visibleCenter = carMapSurfaceOwner.visibleCenter
+ assertEquals(456.0, visibleCenter.x, 0.0001)
+ assertEquals(246.5, visibleCenter.y, 0.0001)
+ }
+
+ // This test may be deleted in the future, if we create or need a better way to detect
+ // start and end drag events. But for now, we're verifying that each scroll is completing a
+ // drag movement.
+ @Test
+ fun `scroll will start and stop dragging`() {
+ val observer: MapboxCarMapObserver = mockk(relaxed = true)
+ val mapboxMap: MapboxMap = mockk(relaxed = true)
+ carMapSurfaceOwner.registerObserver(observer)
+ carMapSurfaceOwner.surfaceAvailable(
+ mockk {
+ every { surfaceContainer } returns mockk {
+ every { width } returns 800
+ every { height } returns 400
+ }
+ every { mapSurface } returns mockk {
+ every { getMapboxMap() } returns mapboxMap
+ }
+ }
+ )
+ val visibleRect = Rect(100, 50, 800, 400)
+ carMapSurfaceOwner.surfaceVisibleAreaChanged(visibleRect)
+
+ carMapSurfaceOwner.scroll(3.0f, -3.0f)
+
+ verifyOrder {
+ mapboxMap.dragStart(ScreenCoordinate(450.0, 225.0))
+ mapboxMap.setCamera(any())
+ mapboxMap.dragEnd()
+ }
+ }
+
+ @Test
+ fun `scroll will move camera from visibleCenter to the delta distance`() {
+ val observer: MapboxCarMapObserver = mockk(relaxed = true)
+ val mapboxMap: MapboxMap = mockk(relaxed = true)
+ carMapSurfaceOwner.registerObserver(observer)
+ carMapSurfaceOwner.surfaceAvailable(
+ mockk {
+ every { surfaceContainer } returns mockk {
+ every { width } returns 800
+ every { height } returns 400
+ }
+ every { mapSurface } returns mockk {
+ every { getMapboxMap() } returns mapboxMap
+ }
+ }
+ )
+ val visibleRect = Rect(100, 50, 800, 400)
+ carMapSurfaceOwner.surfaceVisibleAreaChanged(visibleRect)
+ val fromCoordinateSlot = slot()
+ val toCoordinateSlot = slot()
+ every {
+ mapboxMap.getDragCameraOptions(capture(fromCoordinateSlot), capture(toCoordinateSlot))
+ } returns mockk(relaxed = true)
+
+ carMapSurfaceOwner.scroll(3.3f, -3.3f)
+
+ assertEquals(450.0, fromCoordinateSlot.captured.x, 0.0001)
+ assertEquals(225.0, fromCoordinateSlot.captured.y, 0.0001)
+ assertEquals(446.7, toCoordinateSlot.captured.x, 0.0001)
+ assertEquals(228.3, toCoordinateSlot.captured.y, 0.0001)
+ }
+
+ @Test
+ fun `scroll is ignored if the observer returns true`() {
+ val observer: MapboxCarMapObserver = mockk(relaxed = true) {
+ every { onScroll(any(), any(), any()) } returns true
+ }
+ val mapboxMap: MapboxMap = mockk(relaxed = true)
+ carMapSurfaceOwner.registerObserver(observer)
+ carMapSurfaceOwner.surfaceAvailable(
+ mockk {
+ every { surfaceContainer } returns mockk {
+ every { width } returns 800
+ every { height } returns 400
+ }
+ every { mapSurface } returns mockk {
+ every { getMapboxMap() } returns mapboxMap
+ }
+ }
+ )
+ val visibleRect = Rect(100, 50, 800, 400)
+ carMapSurfaceOwner.surfaceVisibleAreaChanged(visibleRect)
+ val fromCoordinateSlot = slot()
+ val toCoordinateSlot = slot()
+ every {
+ mapboxMap.getDragCameraOptions(capture(fromCoordinateSlot), capture(toCoordinateSlot))
+ } returns mockk(relaxed = true)
+
+ carMapSurfaceOwner.scroll(3.3f, -3.3f)
+
+ verify(exactly = 0) { mapboxMap.dragStart(any()) }
+ verify(exactly = 0) { mapboxMap.setCamera(any()) }
+ verify(exactly = 0) { mapboxMap.dragEnd() }
+ }
+
+ @Test
+ fun `scale double-tap-gesture will easeTo new zoom`() {
+ val observer: MapboxCarMapObserver = mockk(relaxed = true)
+ val cameraPlugin: CameraAnimationsPlugin = mockk(relaxed = true)
+ carMapSurfaceOwner.registerObserver(observer)
+ carMapSurfaceOwner.surfaceAvailable(
+ mockk {
+ every { surfaceContainer } returns mockk {
+ every { width } returns 800
+ every { height } returns 400
+ }
+ every { mapSurface } returns mockk {
+ every { getMapboxMap() } returns mockk {
+ every { cameraState } returns mockk {
+ every { zoom } returns 10.0
+ }
+ }
+ every { camera } returns cameraPlugin
+ }
+ }
+ )
+ val visibleRect = Rect(100, 50, 800, 400)
+ carMapSurfaceOwner.surfaceVisibleAreaChanged(visibleRect)
+ val cameraOptionsSlot = slot()
+ every {
+ cameraPlugin.easeTo(capture(cameraOptionsSlot))
+ } returns mockk(relaxed = true)
+
+ carMapSurfaceOwner.scale(526.0f, 260.0f, 2.0f)
+
+ with(cameraOptionsSlot.captured) {
+ assertEquals(526.0, anchor!!.x, 0.0001)
+ assertEquals(260.0, anchor!!.y, 0.0001)
+ assertEquals(11.0, zoom!!, 0.0001)
+ }
+ }
+
+ @Test
+ fun `scale will use the focus as the anchor point`() {
+ val observer: MapboxCarMapObserver = mockk(relaxed = true)
+ val mapboxMap: MapboxMap = mockk(relaxed = true) {
+ every { cameraState } returns mockk {
+ every { zoom } returns 16.50
+ }
+ }
+ carMapSurfaceOwner.registerObserver(observer)
+ carMapSurfaceOwner.surfaceAvailable(
+ mockk {
+ every { surfaceContainer } returns mockk {
+ every { width } returns 800
+ every { height } returns 400
+ }
+ every { mapSurface } returns mockk {
+ every { getMapboxMap() } returns mapboxMap
+ }
+ }
+ )
+ val cameraOptionsSlot = slot()
+ every {
+ mapboxMap.setCamera(capture(cameraOptionsSlot))
+ } returns mockk(relaxed = true)
+
+ carMapSurfaceOwner.scale(224.4f, 117.1f, 0.98f)
+
+ with(cameraOptionsSlot.captured.anchor!!) {
+ assertEquals(224.4, x, 0.0001)
+ assertEquals(117.1, y, 0.0001)
+ }
+ }
+
+ @Test
+ fun `scale with factor less than one will zoom out`() {
+ val expectedFromZoom = 16.50
+ val expectedToZoom = 16.48
+ val observer: MapboxCarMapObserver = mockk(relaxed = true)
+ val mapboxMap: MapboxMap = mockk(relaxed = true) {
+ every { cameraState } returns mockk {
+ every { zoom } returns expectedFromZoom
+ }
+ }
+ carMapSurfaceOwner.registerObserver(observer)
+ carMapSurfaceOwner.surfaceAvailable(
+ mockk {
+ every { surfaceContainer } returns mockk {
+ every { width } returns 800
+ every { height } returns 400
+ }
+ every { mapSurface } returns mockk {
+ every { getMapboxMap() } returns mapboxMap
+ }
+ }
+ )
+ val cameraOptionsSlot = slot()
+ every {
+ mapboxMap.setCamera(capture(cameraOptionsSlot))
+ } returns mockk(relaxed = true)
+
+ carMapSurfaceOwner.scale(400.0f, 200.0f, 0.98f)
+
+ assertEquals(expectedToZoom, cameraOptionsSlot.captured.zoom!!, 0.0001)
+ }
+
+ @Test
+ fun `scale with factor greater than one will zoom in`() {
+ val expectedFromZoom = 16.50
+ val expectedToZoom = 16.52
+ val observer: MapboxCarMapObserver = mockk(relaxed = true)
+ val mapboxMap: MapboxMap = mockk(relaxed = true) {
+ every { cameraState } returns mockk {
+ every { zoom } returns expectedFromZoom
+ }
+ }
+ carMapSurfaceOwner.registerObserver(observer)
+ carMapSurfaceOwner.surfaceAvailable(
+ mockk {
+ every { surfaceContainer } returns mockk {
+ every { width } returns 800
+ every { height } returns 400
+ }
+ every { mapSurface } returns mockk {
+ every { getMapboxMap() } returns mapboxMap
+ }
+ }
+ )
+ val cameraOptionsSlot = slot()
+ every {
+ mapboxMap.setCamera(capture(cameraOptionsSlot))
+ } returns mockk(relaxed = true)
+
+ carMapSurfaceOwner.scale(400.0f, 200.0f, 1.02f)
+
+ assertEquals(expectedToZoom, cameraOptionsSlot.captured.zoom!!, 0.0001)
+ }
+
+ @Test
+ fun `scale observer can set min and max thresholds`() {
+ val expectedFromZoom = 29.99
+ val observer: MapboxCarMapObserver = mockk(relaxed = true) {
+ every { onScale(any(), any(), any(), any()) } answers {
+ val toZoom: Double = arg(3)
+ assertEquals(29.99, arg(2), 0.001)
+ assertEquals(30.14, toZoom, 0.001)
+ toZoom >= 30 // Returning true makes 30 the maximum threshold
+ }
+ }
+ val mapboxMap: MapboxMap = mockk(relaxed = true) {
+ every { cameraState } returns mockk {
+ every { zoom } returns expectedFromZoom
+ }
+ }
+ carMapSurfaceOwner.registerObserver(observer)
+ carMapSurfaceOwner.surfaceAvailable(
+ mockk {
+ every { surfaceContainer } returns mockk {
+ every { width } returns 800
+ every { height } returns 400
+ }
+ every { mapSurface } returns mockk {
+ every { getMapboxMap() } returns mapboxMap
+ }
+ }
+ )
+ val cameraOptionsSlot = slot()
+ every {
+ mapboxMap.setCamera(capture(cameraOptionsSlot))
+ } returns mockk(relaxed = true)
+
+ carMapSurfaceOwner.scale(400.0f, 200.0f, 1.15f)
+
+ verify(exactly = 0) { mapboxMap.setCamera(any()) }
+ }
+
+ @Test
+ fun `scale is ignored if the observer returns true`() {
+ val observer: MapboxCarMapObserver = mockk(relaxed = true) {
+ every { onScale(any(), any(), any(), any()) } returns true
+ }
+ val mapboxMap: MapboxMap = mockk(relaxed = true) {
+ every { cameraState } returns mockk {
+ every { zoom } returns 10.0
+ }
+ }
+ carMapSurfaceOwner.registerObserver(observer)
+ carMapSurfaceOwner.surfaceAvailable(
+ mockk {
+ every { surfaceContainer } returns mockk {
+ every { width } returns 800
+ every { height } returns 400
+ }
+ every { mapSurface } returns mockk {
+ every { getMapboxMap() } returns mapboxMap
+ }
+ }
+ )
+ val cameraOptionsSlot = slot()
+ every {
+ mapboxMap.setCamera(capture(cameraOptionsSlot))
+ } returns mockk(relaxed = true)
+
+ carMapSurfaceOwner.scale(400.0f, 200.0f, 1.15f)
+
+ verify(exactly = 0) { mapboxMap.setCamera(any()) }
+ }
+}
\ No newline at end of file
diff --git a/extension-androidauto/src/test/java/com/mapbox/maps/extension/androidauto/MapboxCarMapTest.kt b/extension-androidauto/src/test/java/com/mapbox/maps/extension/androidauto/MapboxCarMapTest.kt
new file mode 100644
index 0000000000..a8c16fe448
--- /dev/null
+++ b/extension-androidauto/src/test/java/com/mapbox/maps/extension/androidauto/MapboxCarMapTest.kt
@@ -0,0 +1,220 @@
+package com.mapbox.maps.extension.androidauto
+
+import androidx.car.app.AppManager
+import androidx.car.app.CarContext
+import androidx.car.app.SurfaceCallback
+import com.mapbox.common.Logger
+import com.mapbox.maps.MapInitOptions
+import com.mapbox.maps.MapSurface
+import com.mapbox.maps.MapboxExperimental
+import io.mockk.Runs
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.mockkObject
+import io.mockk.mockkStatic
+import io.mockk.slot
+import io.mockk.unmockkAll
+import io.mockk.verify
+import io.mockk.verifyOrder
+import org.junit.After
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+
+@OptIn(MapboxExperimental::class)
+class MapboxCarMapTest {
+
+ private val testMapSurface: MapSurface = mockk(relaxed = true)
+
+ @Before
+ fun setup() {
+ mockkStatic(Logger::class)
+ every { Logger.i(any(), any()) } just Runs
+ mockkObject(MapSurfaceProvider)
+ every { MapSurfaceProvider.create(any(), any(), any()) } returns testMapSurface
+ }
+
+ @After
+ fun teardown() {
+ unmockkAll()
+ }
+
+ @Test(expected = RuntimeException::class)
+ fun `MapboxCarMap constructor crashes when context is not a CarContext`() {
+ val mapInitOptions = mockk {
+ every { context } returns mockk()
+ }
+
+ MapboxCarMap(mapInitOptions)
+ }
+
+ @Test
+ fun `MapboxCarMap constructor requests the surface callback`() {
+ val surfaceCallbackSlot = slot()
+ val mapInitOptions = mockk {
+ every { context } returns mockk {
+ every { getCarService(AppManager::class.java) } returns mockk {
+ every { setSurfaceCallback(capture(surfaceCallbackSlot)) } just Runs
+ }
+ }
+ }
+
+ MapboxCarMap(mapInitOptions)
+
+ assertTrue(surfaceCallbackSlot.isCaptured)
+ }
+
+ @Test
+ fun `carMapSurface is valid after onSurfaceAvailable`() {
+ val surfaceCallbackSlot = slot()
+ val mapInitOptions = mockk {
+ every { context } returns mockk {
+ every { getCarService(AppManager::class.java) } returns mockk {
+ every { setSurfaceCallback(capture(surfaceCallbackSlot)) } just Runs
+ }
+ }
+ }
+
+ val mapboxCarMap = MapboxCarMap(mapInitOptions)
+ assertNull(mapboxCarMap.carMapSurface)
+ surfaceCallbackSlot.captured.onSurfaceAvailable(mockk(relaxed = true))
+
+ assertNotNull(mapboxCarMap.carMapSurface)
+ }
+
+ @Test
+ fun `visibleArea is valid after onVisibleAreaChanged`() {
+ val surfaceCallbackSlot = slot()
+ val mapInitOptions = mockk {
+ every { context } returns mockk {
+ every { getCarService(AppManager::class.java) } returns mockk {
+ every { setSurfaceCallback(capture(surfaceCallbackSlot)) } just Runs
+ }
+ }
+ }
+
+ val mapboxCarMap = MapboxCarMap(mapInitOptions)
+ assertNull(mapboxCarMap.visibleArea)
+ surfaceCallbackSlot.captured.onSurfaceAvailable(mockk(relaxed = true))
+ surfaceCallbackSlot.captured.onVisibleAreaChanged(mockk(relaxed = true))
+
+ assertNotNull(mapboxCarMap.visibleArea)
+ }
+
+ @Test
+ fun `edgeInsets is valid after onVisibleAreaChanged`() {
+ val surfaceCallbackSlot = slot()
+ val mapInitOptions = mockk {
+ every { context } returns mockk {
+ every { getCarService(AppManager::class.java) } returns mockk {
+ every { setSurfaceCallback(capture(surfaceCallbackSlot)) } just Runs
+ }
+ }
+ }
+
+ val mapboxCarMap = MapboxCarMap(mapInitOptions)
+ assertNull(mapboxCarMap.edgeInsets)
+ surfaceCallbackSlot.captured.onSurfaceAvailable(mockk(relaxed = true))
+ surfaceCallbackSlot.captured.onVisibleAreaChanged(mockk(relaxed = true))
+
+ assertNotNull(mapboxCarMap.edgeInsets)
+ }
+
+ @Test
+ fun `registerObserver includes onAttached and onVisibleAreaChanged`() {
+ val surfaceCallbackSlot = slot()
+ val mapInitOptions = mockk {
+ every { context } returns mockk {
+ every { getCarService(AppManager::class.java) } returns mockk {
+ every { setSurfaceCallback(capture(surfaceCallbackSlot)) } just Runs
+ }
+ }
+ }
+
+ val mapboxCarMap = MapboxCarMap(mapInitOptions)
+ val observer = mockk(relaxed = true)
+ mapboxCarMap.registerObserver(observer)
+ surfaceCallbackSlot.captured.onSurfaceAvailable(mockk(relaxed = true))
+ surfaceCallbackSlot.captured.onVisibleAreaChanged(mockk(relaxed = true))
+
+ verifyOrder {
+ observer.onAttached(any())
+ observer.onVisibleAreaChanged(any(), any())
+ }
+ }
+
+ @Test
+ fun `registerObserver receives callbacks if registered after onVisibleAreaChanged`() {
+ val surfaceCallbackSlot = slot()
+ val mapInitOptions = mockk {
+ every { context } returns mockk {
+ every { getCarService(AppManager::class.java) } returns mockk {
+ every { setSurfaceCallback(capture(surfaceCallbackSlot)) } just Runs
+ }
+ }
+ }
+
+ val mapboxCarMap = MapboxCarMap(mapInitOptions)
+ surfaceCallbackSlot.captured.onSurfaceAvailable(mockk(relaxed = true))
+ surfaceCallbackSlot.captured.onVisibleAreaChanged(mockk(relaxed = true))
+
+ val observer = mockk(relaxed = true)
+ mapboxCarMap.registerObserver(observer)
+
+ verifyOrder {
+ observer.onAttached(any())
+ observer.onVisibleAreaChanged(any(), any())
+ }
+ }
+
+ @Test
+ fun `unregisterObserver will prevent callbacks`() {
+ val surfaceCallbackSlot = slot()
+ val mapInitOptions = mockk {
+ every { context } returns mockk {
+ every { getCarService(AppManager::class.java) } returns mockk {
+ every { setSurfaceCallback(capture(surfaceCallbackSlot)) } just Runs
+ }
+ }
+ }
+
+ val mapboxCarMap = MapboxCarMap(mapInitOptions)
+ val observer = mockk(relaxed = true)
+ mapboxCarMap.registerObserver(observer)
+ mapboxCarMap.unregisterObserver(observer)
+
+ surfaceCallbackSlot.captured.onSurfaceAvailable(mockk(relaxed = true))
+ surfaceCallbackSlot.captured.onVisibleAreaChanged(mockk(relaxed = true))
+
+ verify(exactly = 0) { observer.onAttached(any()) }
+ verify(exactly = 0) { observer.onVisibleAreaChanged(any(), any()) }
+ verify(exactly = 0) { observer.onDetached(any()) }
+ }
+
+ @Test
+ fun `clearObservers will prevent callbacks`() {
+ val surfaceCallbackSlot = slot()
+ val mapInitOptions = mockk {
+ every { context } returns mockk {
+ every { getCarService(AppManager::class.java) } returns mockk {
+ every { setSurfaceCallback(capture(surfaceCallbackSlot)) } just Runs
+ }
+ }
+ }
+
+ val mapboxCarMap = MapboxCarMap(mapInitOptions)
+ val observer = mockk(relaxed = true)
+ mapboxCarMap.registerObserver(observer)
+ mapboxCarMap.clearObservers()
+
+ surfaceCallbackSlot.captured.onSurfaceAvailable(mockk(relaxed = true))
+ surfaceCallbackSlot.captured.onVisibleAreaChanged(mockk(relaxed = true))
+
+ verify(exactly = 0) { observer.onAttached(any()) }
+ verify(exactly = 0) { observer.onVisibleAreaChanged(any(), any()) }
+ verify(exactly = 0) { observer.onDetached(any()) }
+ }
+}
\ No newline at end of file
diff --git a/extension-androidauto/src/test/java/com/mapbox/maps/extension/androidauto/testing/ShadowLogger.java b/extension-androidauto/src/test/java/com/mapbox/maps/extension/androidauto/testing/ShadowLogger.java
new file mode 100644
index 0000000000..46be4f9f4b
--- /dev/null
+++ b/extension-androidauto/src/test/java/com/mapbox/maps/extension/androidauto/testing/ShadowLogger.java
@@ -0,0 +1,35 @@
+package com.mapbox.maps.extension.androidauto.testing;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.mapbox.common.Logger;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+@Implements(Logger.class)
+public class ShadowLogger {
+
+ @Implementation
+ public static void e(@Nullable String tag, @NonNull String message) {
+ Log.e(message, tag);
+ }
+
+ @Implementation
+ public static void d(@Nullable String tag, @NonNull String message) {
+ Log.d(message, tag);
+ }
+
+ @Implementation
+ public static void w(@Nullable String tag, @NonNull String message) {
+ Log.w(message, tag);
+ }
+
+ @Implementation
+ public static void i(@Nullable String tag, @NonNull String message) {
+ Log.i(message, tag);
+ }
+}
\ No newline at end of file