diff --git a/android-auto-app/build.gradle.kts b/android-auto-app/build.gradle.kts
index ff2a7d217b..803dc07116 100644
--- a/android-auto-app/build.gradle.kts
+++ b/android-auto-app/build.gradle.kts
@@ -8,7 +8,10 @@ plugins {
val buildFromSource: String by project
android {
+ // ..1.7.0/res/values/values.xml:105:5-114:25: AAPT: error: resource android:attr/lStar not found.
+ // Receiving the above error when using the android auto car library 1.1.0
compileSdkVersion(AndroidVersions.compileSdkVersion)
+// compileSdkVersion(31)
defaultConfig {
applicationId = "com.mapbox.maps.testapp.auto"
minSdkVersion(AndroidVersions.minAndroidAutoSdkVersion)
@@ -50,10 +53,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..3607c550b0 100644
--- a/android-auto-app/src/main/AndroidManifest.xml
+++ b/android-auto-app/src/main/AndroidManifest.xml
@@ -7,9 +7,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 loaded(mapboxCarMapSurface: MapboxCarMapSurface) {
+ super.loaded(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 detached(mapboxCarMapSurface: MapboxCarMapSurface) {
+ previousCameraState = mapboxCarMapSurface.mapSurface.getMapboxMap().cameraState
+ with(mapboxCarMapSurface.mapSurface.location) {
+ removeOnIndicatorPositionChangedListener(changePositionListener)
}
+ super.detached(mapboxCarMapSurface)
+ }
+
+ override fun visibleAreaChanged(visibleArea: Rect, edgeInsets: EdgeInsets) {
+ insets = edgeInsets
}
- override fun onMapScroll() {
+ override fun scroll(
+ 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 +94,40 @@ 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 {
private val HELSINKI = Point.fromLngLat(24.9384, 60.1699)
private const val INITIAL_ZOOM = 16.0
private const val INITIAL_PITCH = 75.0
- private const val TAG = "CarCameraController"
+
+ /**
+ * When zooming the camera by a delta, this is an estimated min-zoom.
+ */
+ private const val MIN_ZOOM_OUT = 6.0
+
+ /**
+ * When zooming the camera by a delta, this is an estimated max-zoom.
+ */
+ 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..bb33911a4a
--- /dev/null
+++ b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/CarMapShowcase.kt
@@ -0,0 +1,58 @@
+package com.mapbox.maps.testapp.auto.car
+
+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.addLayer
+import com.mapbox.maps.extension.style.layers.generated.SkyLayer
+import com.mapbox.maps.extension.style.layers.getLayerAs
+import com.mapbox.maps.extension.style.sources.addSource
+import com.mapbox.maps.extension.style.sources.generated.RasterDemSource
+import com.mapbox.maps.extension.style.sources.getSourceAs
+import com.mapbox.maps.extension.style.terrain.generated.setTerrain
+import com.mapbox.maps.extension.style.terrain.generated.terrain
+
+class CarMapShowcase : MapboxCarMapObserver {
+
+ override fun loaded(mapboxCarMapSurface: MapboxCarMapSurface) {
+ with(mapboxCarMapSurface.style) {
+ updateSkyLayer(mapboxCarMapSurface.carContext.isDarkMode)
+ updateTerrainLayer()
+ }
+ }
+
+ private fun Style.updateSkyLayer(isDarkMode: Boolean) {
+ var skyLayer = getLayerAs(SKY_LAYER)
+ if (skyLayer == null) {
+ skyLayer = SkyLayer(SKY_LAYER)
+ addLayer(skyLayer)
+ }
+ // 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.
+ if (isDarkMode) {
+ skyLayer.skyAtmosphereSun(listOf(-50.0, 90.2))
+ } else {
+ skyLayer.skyAtmosphereSun(listOf(0.0, 0.0))
+ }
+ }
+
+ private fun Style.updateTerrainLayer() {
+ var demSource = getSourceAs(DEM_SOURCE)
+ if (demSource == null) {
+ demSource = RasterDemSource.Builder(DEM_SOURCE)
+ .url(TERRAIN_URL_TILE_RESOURCE)
+ .tileSize(514)
+ .build()
+ addSource(demSource)
+ }
+ setTerrain(terrain(DEM_SOURCE) { exaggeration(1.7) })
+ }
+
+ 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..1a2b86ae67 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
@@ -6,23 +6,20 @@ import androidx.car.app.Screen
import androidx.car.app.model.*
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.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) {
+class MapScreen(
+ carContext: CarContext,
+ val mapboxCarMap: MapboxCarMap
+) : Screen(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()
@@ -41,7 +38,7 @@ class MapScreen(carContext: CarContext) : Screen(carContext) {
).build()
)
.setOnClickListener {
- carCameraController.get()?.focusOnLocationPuck()
+ carCameraController.focusOnLocationPuck()
}
.build()
)
@@ -76,7 +73,7 @@ class MapScreen(carContext: CarContext) : Screen(carContext) {
)
.setOnClickListener {
// handle zoom out
- carCameraController.get()?.zoomBy(0.95)
+ carCameraController.zoomOutAction()
}
.build()
)
@@ -91,7 +88,7 @@ class MapScreen(carContext: CarContext) : Screen(carContext) {
).build()
)
.setOnClickListener {
- carCameraController.get()?.zoomBy(1.05)
+ carCameraController.zoomInAction()
}
.build()
)
@@ -114,4 +111,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..1331f871a7 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 androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import com.mapbox.maps.MapInitOptions
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 com.mapbox.maps.extension.androidauto.MapboxCarMap
/**
* Session class for the Mapbox Map sample app for Android Auto.
*/
class MapSession : Session() {
- private lateinit var mapSurface: MapSurface
- private val carCameraController = CarCameraController()
+ private lateinit var mapboxCarMap: MapboxCarMap
+ private val carMapEnvironment = CarMapShowcase()
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)
- }
+ val mapScreen = MapScreen(carContext, mapboxCarMap)
+
return if (carContext.checkSelfPermission(ACCESS_FINE_LOCATION) != PERMISSION_GRANTED) {
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)
- }
- }
+ private fun mapStyle() = if (carContext.isDarkMode) Style.TRAFFIC_NIGHT else Style.TRAFFIC_DAY
override fun onCarConfigurationChanged(newConfiguration: Configuration) {
- super.onCarConfigurationChanged(newConfiguration)
- loadStyle(mapSurface)
+ mapboxCarMap.updateMapStyle(mapStyle())
}
- 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) {
+ mapboxCarMap = MapboxCarMap(
+ MapInitOptions(
+ context = carContext,
+ styleUri = mapStyle()
+ ),
+ carContext,
+ lifecycle
+ )
+ mapboxCarMap.registerObserver(carMapEnvironment)
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ mapboxCarMap.unregisterObserver(carMapEnvironment)
+ }
+ })
}
}
\ No newline at end of file
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/extension-androidauto/build.gradle.kts b/extension-androidauto/build.gradle.kts
index 9a21f38f60..b6ffb97acd 100644
--- a/extension-androidauto/build.gradle.kts
+++ b/extension-androidauto/build.gradle.kts
@@ -10,7 +10,7 @@ android {
compileSdkVersion(AndroidVersions.compileSdkVersion)
defaultConfig {
minSdkVersion(AndroidVersions.minAndroidAutoSdkVersion)
- targetSdkVersion(AndroidVersions.targetSdkVersion)
+ targetSdkVersion(AndroidVersions.minAndroidAutoSdkVersion)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -22,11 +22,12 @@ android {
}
dependencies {
- compileOnly(project(":sdk"))
+ implementation(project(":sdk"))
implementation(Dependencies.googleCarAppLibrary)
implementation(Dependencies.kotlin)
implementation(Dependencies.androidxCoreKtx)
implementation(Dependencies.androidxAnnotations)
+
testImplementation(Dependencies.junit)
testImplementation(Dependencies.mockk)
testImplementation(Dependencies.androidxTestCore)
diff --git a/extension-androidauto/src/main/AndroidManifest.xml b/extension-androidauto/src/main/AndroidManifest.xml
index 582dc0a176..846931a7bf 100644
--- a/extension-androidauto/src/main/AndroidManifest.xml
+++ b/extension-androidauto/src/main/AndroidManifest.xml
@@ -1 +1,15 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CarMapLifecycleObserver.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CarMapLifecycleObserver.kt
new file mode 100644
index 0000000000..b498b22f88
--- /dev/null
+++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CarMapLifecycleObserver.kt
@@ -0,0 +1,128 @@
+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 androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import com.mapbox.common.Logger
+import com.mapbox.maps.MapInitOptions
+import com.mapbox.maps.Style
+import com.mapbox.maps.extension.observable.eventdata.MapLoadingErrorEventData
+import com.mapbox.maps.plugin.delegates.listeners.OnMapLoadErrorListener
+
+/**
+ * This class combines Android Auto screen lifecycle events
+ * with SurfaceCallback lifecycle events. It then
+ * sets the [CarMapSurfaceOwner] which allows us to register onto
+ * our own [MapboxCarMapObserver]
+ */
+internal class CarMapLifecycleObserver internal constructor(
+ private val carContext: CarContext,
+ private val carMapSurfaceOwner: CarMapSurfaceOwner,
+ private val mapInitOptions: MapInitOptions
+) : DefaultLifecycleObserver, SurfaceCallback {
+
+ private var mapStyleUri: String = mapInitOptions.styleUri ?: Style.MAPBOX_STREETS
+
+ private val logMapError = object : OnMapLoadErrorListener {
+ override fun onMapLoadError(eventData: MapLoadingErrorEventData) {
+ val errorData = "${eventData.type} ${eventData.message}"
+ Logger.e(TAG, "updateMapStyle onMapLoadError $errorData")
+ }
+ }
+
+ /** Screen lifecycle events */
+
+ override fun onCreate(owner: LifecycleOwner) {
+ Logger.i(TAG, "onCreate request surface")
+ carContext.getCarService(AppManager::class.java)
+ .setSurfaceCallback(this)
+ }
+
+ /** Surface lifecycle events */
+
+ 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.getMapboxMap().loadStyleUri(
+ mapStyleUri,
+ onStyleLoaded = { style ->
+ Logger.i(TAG, "onSurfaceAvailable onStyleLoaded")
+ mapSurface.surfaceChanged(surfaceContainer.width, surfaceContainer.height)
+ val carMapSurface = MapboxCarMapSurface(carContext, mapSurface, surfaceContainer, style)
+ carMapSurfaceOwner.surfaceAvailable(carMapSurface)
+ },
+ onMapLoadErrorListener = logMapError
+ )
+ }
+ }
+
+ 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.
+ // logAndroidAuto("CarMapSurfaceLifecycle Stable area changed stable:$stableArea")
+ }
+
+ 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()
+ }
+
+ /** Map modifiers */
+
+ fun updateMapStyle(styleUri: String) {
+ if (this.mapStyleUri == styleUri) return
+ this.mapStyleUri = styleUri
+
+ Logger.i(TAG, "updateMapStyle $styleUri")
+ val previousCarMapSurface = carMapSurfaceOwner.mapboxCarMapSurface
+ val mapSurface = previousCarMapSurface?.mapSurface
+ mapSurface?.getMapboxMap()?.loadStyleUri(
+ styleUri,
+ onStyleLoaded = { style ->
+ Logger.i(TAG, "updateMapStyle styleAvailable ${style.styleURI}")
+ val carMapSurface = MapboxCarMapSurface(
+ carContext,
+ mapSurface,
+ previousCarMapSurface.surfaceContainer,
+ style,
+ )
+ carMapSurfaceOwner.surfaceAvailable(carMapSurface)
+ },
+ onMapLoadErrorListener = logMapError
+ )
+ }
+
+ private companion object {
+ private const val TAG = "CarMapSurfaceLifecycle"
+ }
+}
\ 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..ed0d8e1635
--- /dev/null
+++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CarMapSurfaceOwner.kt
@@ -0,0 +1,190 @@
+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.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].
+ */
+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.loaded(carMapSurface)
+ }
+ ifNonNull(mapboxCarMapSurface, visibleArea, edgeInsets) { _, area, edge ->
+ Logger.i(TAG, "registerObserver visibleAreaChanged")
+ mapboxCarMapObserver.visibleAreaChanged(area, edge)
+ }
+ }
+
+ fun unregisterObserver(mapboxCarMapObserver: MapboxCarMapObserver) {
+ carMapObservers.remove(mapboxCarMapObserver)
+ mapboxCarMapSurface?.let { mapboxCarMapObserver.detached(it) }
+ Logger.i(TAG, "unregisterObserver - 1 = ${carMapObservers.size}")
+ }
+
+ fun clearObservers() {
+ val oldCarMapSurface = this.mapboxCarMapSurface
+ oldCarMapSurface?.let { carMapObservers.forEach { it.detached(oldCarMapSurface) } }
+ carMapObservers.clear()
+ }
+
+ fun surfaceAvailable(mapboxCarMapSurface: MapboxCarMapSurface) {
+ Logger.i(TAG, "surfaceAvailable")
+ val oldCarMapSurface = this.mapboxCarMapSurface
+ this.mapboxCarMapSurface = mapboxCarMapSurface
+ oldCarMapSurface?.let { carMapObservers.forEach { it.detached(oldCarMapSurface) } }
+ carMapObservers.forEach { it.loaded(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.detached(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.visibleAreaChanged(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.scroll(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.fling(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.scale(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..63844d9b1a
--- /dev/null
+++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarMap.kt
@@ -0,0 +1,84 @@
+package com.mapbox.maps.extension.androidauto
+
+import android.graphics.Rect
+import androidx.car.app.CarContext
+import androidx.lifecycle.Lifecycle
+import com.mapbox.maps.EdgeInsets
+import com.mapbox.maps.MapInitOptions
+import com.mapbox.maps.Style
+
+/**
+ * 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.
+ *
+ * @param mapInitOptions to initialize the [MapboxCarMapSurface]
+ * @param carContext for accessing car services
+ * @param lifecycle to prevent memory leaks and support [MapboxCarMapObserver] lifecycles.
+ */
+class MapboxCarMap(
+ mapInitOptions: MapInitOptions,
+ carContext: CarContext,
+ lifecycle: Lifecycle
+) {
+ private val carMapSurfaceSession = CarMapSurfaceOwner()
+ private val carMapLifecycleObserver = CarMapLifecycleObserver(
+ carContext,
+ carMapSurfaceSession,
+ mapInitOptions
+ )
+
+ val mapboxCarMapSurface: MapboxCarMapSurface?
+ get() = carMapSurfaceSession.mapboxCarMapSurface
+ val visibleArea: Rect?
+ get() = carMapSurfaceSession.visibleArea
+ val edgeInsets: EdgeInsets?
+ get() = carMapSurfaceSession.edgeInsets
+
+ init {
+ lifecycle.addObserver(carMapLifecycleObserver)
+ }
+
+ /**
+ * @param mapboxCarMapObserver implements the desired mapbox car experiences
+ */
+ fun registerObserver(mapboxCarMapObserver: MapboxCarMapObserver) = apply {
+ carMapSurfaceSession.registerObserver(mapboxCarMapObserver)
+ }
+
+ /**
+ * @param mapboxCarMapObserver the instance used in [registerObserver]
+ */
+ fun unregisterObserver(mapboxCarMapObserver: MapboxCarMapObserver) {
+ carMapSurfaceSession.unregisterObserver(mapboxCarMapObserver)
+ }
+
+ /**
+ * Optional function to clear all observers registered through [registerObserver]
+ */
+ fun clearObservers() {
+ carMapSurfaceSession.clearObservers()
+ }
+
+ /**
+ * When the style changes, this will cause all registered observers to trigger
+ * [MapboxCarMapObserver.detached]. The new style will load and trigger
+ * [MapboxCarMapObserver.loaded].
+ *
+ * @param styleUri The styleUri will update the [MapboxCarMapSurface].
+ */
+ fun updateMapStyle(styleUri: String) {
+ carMapLifecycleObserver.updateMapStyle(styleUri)
+ }
+}
\ 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..f72f752be0
--- /dev/null
+++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarMapObserver.kt
@@ -0,0 +1,119 @@
+package com.mapbox.maps.extension.androidauto
+
+import android.graphics.Rect
+import androidx.car.app.SurfaceCallback
+import com.mapbox.maps.EdgeInsets
+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.
+ */
+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 loaded(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 [loaded].
+ *
+ * @param visibleArea the visible area provided by the host
+ * @param edgeInsets distance from each side of the screen that creates the [visibleArea]
+ */
+ fun visibleAreaChanged(visibleArea: Rect, edgeInsets: EdgeInsets) {
+ // No op by default
+ }
+
+ /**
+ * Allows you to implement or observe the map scroll gesture handler. The surface is [loaded]
+ * 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 scroll(
+ 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 [loaded]
+ * 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 fling(
+ 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 [loaded]
+ * 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 scale(
+ mapboxCarMapSurface: MapboxCarMapSurface,
+ anchor: ScreenCoordinate,
+ fromZoom: Double,
+ toZoom: Double
+ ): Boolean {
+ // By default, scale is handled internally
+ return false
+ }
+
+ /**
+ * Called when a [MapboxCarMapSurface] has been detached from this observer. Some examples that
+ * can cause this to detach are:
+ * - [MapboxCarMap] lifecycle is destroyed
+ * - [MapboxCarMap.updateMapStyle] has been called
+ * - This observer has been unregistered with [MapboxCarMap.unregisterObserver]
+ *
+ * You can assume that there was a corresponding call to [loaded] with the same
+ * [MapboxCarMapObserver] instance.
+ *
+ * @param mapboxCarMapSurface loaded and ready car map surface
+ */
+ fun detached(mapboxCarMapSurface: MapboxCarMapSurface) {
+ // No op by default
+ }
+}
\ 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..4dc6111e52
--- /dev/null
+++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarMapSurface.kt
@@ -0,0 +1,35 @@
+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.Style
+
+/**
+ * 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.
+ * @property style reference to the Mapbox Style
+ */
+class MapboxCarMapSurface internal constructor(
+ val carContext: CarContext,
+ val mapSurface: MapSurface,
+ val surfaceContainer: SurfaceContainer,
+ val style: Style
+) {
+ /**
+ * Get a string representation of the map surface.
+ *
+ * @return the string representation
+ */
+ override fun toString(): String {
+ return "MapboxCarMapSurface(carContext=$carContext," +
+ " mapSurface=$mapSurface," +
+ " surfaceContainer=$surfaceContainer," +
+ " style=$style" +
+ ")"
+ }
+}
\ 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/CarMapLifecycleObserverTest.kt b/extension-androidauto/src/test/java/com/mapbox/maps/extension/androidauto/CarMapLifecycleObserverTest.kt
new file mode 100644
index 0000000000..5c09e79f3c
--- /dev/null
+++ b/extension-androidauto/src/test/java/com/mapbox/maps/extension/androidauto/CarMapLifecycleObserverTest.kt
@@ -0,0 +1,165 @@
+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.lifecycle.LifecycleOwner
+import com.mapbox.common.Logger
+import com.mapbox.maps.MapInitOptions
+import com.mapbox.maps.MapSurface
+import com.mapbox.maps.MapboxMap
+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
+
+class CarMapLifecycleObserverTest {
+
+ 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 carMapLifecycleObserver = CarMapLifecycleObserver(
+ 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 lifecycleOwner = mockk()
+ val surfaceCallback = slot()
+ every { carContext.getCarService(AppManager::class.java) } returns mockk {
+ every { setSurfaceCallback(capture(surfaceCallback)) } just Runs
+ }
+
+ carMapLifecycleObserver.onCreate(lifecycleOwner)
+
+ assertTrue(surfaceCallback.isCaptured)
+ assertEquals(surfaceCallback.captured, carMapLifecycleObserver)
+ }
+
+ @Test
+ fun `onSurfaceAvailable should load the MapboxMap`() {
+ val mapboxMap = mockk(relaxed = true)
+ every { testMapSurface.getMapboxMap() } returns mapboxMap
+
+ carMapLifecycleObserver.onSurfaceAvailable(
+ mockk {
+ every { surface } returns mockk()
+ }
+ )
+
+ verifyOrder {
+ testMapSurface.onStart()
+ testMapSurface.surfaceCreated()
+ testMapSurface.getMapboxMap()
+ mapboxMap.loadStyleUri(any(), any(), any())
+ }
+ }
+
+ @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
+
+ carMapLifecycleObserver.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
+
+ carMapLifecycleObserver.onVisibleAreaChanged(visibleRect)
+
+ verify(exactly = 1) { carMapSurfaceOwner.surfaceVisibleAreaChanged(visibleRect) }
+ }
+
+ @Test
+ fun `onStableAreaChanged should not do anything`() {
+ carMapLifecycleObserver.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
+
+ carMapLifecycleObserver.onSurfaceDestroyed(mockk())
+
+ verify(exactly = 1) { carMapSurfaceOwner.surfaceDestroyed() }
+ }
+
+ @Test
+ fun `updateMapStyle should notify surfaceAvailable when style is loaded`() {
+ val previousMapSurface = mockk {
+ every { mapSurface } returns mockk {
+ every { getMapboxMap() } returns mockk(relaxed = true) {
+ every { loadStyleUri(any(), any(), any()) } answers {
+ secondArg().onStyleLoaded(
+ mockk { every { styleURI } returns "test-map-style-loaded" }
+ )
+ }
+ }
+ }
+ every { surfaceContainer } returns mockk()
+ }
+ every { carMapSurfaceOwner.mapboxCarMapSurface } returns previousMapSurface
+ val carMapSurfaceSlot = slot()
+ every { carMapSurfaceOwner.surfaceAvailable(capture(carMapSurfaceSlot)) } just Runs
+
+ carMapLifecycleObserver.updateMapStyle("test-map-style")
+
+ val mapSurface = previousMapSurface.mapSurface
+ verify(exactly = 0) { mapSurface.surfaceChanged(any(), any()) }
+ verify(exactly = 1) { carMapSurfaceOwner.surfaceAvailable(any()) }
+ assertEquals("test-map-style-loaded", carMapSurfaceSlot.captured.style.styleURI)
+ assertEquals(previousMapSurface.mapSurface, carMapSurfaceSlot.captured.mapSurface)
+ }
+}
\ 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..bcb8494068
--- /dev/null
+++ b/extension-androidauto/src/test/java/com/mapbox/maps/extension/androidauto/CarMapSurfaceOwnerTest.kt
@@ -0,0 +1,567 @@
+@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.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.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+@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.loaded(any()) }
+ verify(exactly = 0) { secondObserver.loaded(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.detached(any()) }
+ verify(exactly = 0) { secondObserver.detached(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.loaded(mapboxCarMapSurface) }
+ verify(exactly = 1) { secondObserver.loaded(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.visibleAreaChanged(any(), any()) }
+ verify(exactly = 0) { secondObserver.visibleAreaChanged(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.visibleAreaChanged(any(), any()) }
+ verify(exactly = 1) { secondObserver.visibleAreaChanged(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.visibleAreaChanged(any(), any()) }
+ verify(exactly = 0) { secondObserver.visibleAreaChanged(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.detached(mapboxCarMapSurface)
+ }
+ }
+
+ @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.loaded(firstSurface)
+ observer.visibleAreaChanged(any(), any())
+ observer.detached(firstSurface)
+ observer.loaded(secondSurface)
+ observer.visibleAreaChanged(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.visibleAreaChanged(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.visibleAreaChanged(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.visibleAreaChanged(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 { scroll(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 { scale(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 { scale(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/testing/ShadowLogger.java b/extension-androidauto/src/test/java/com/mapbox/maps/extension/androidauto/testing/ShadowLogger.java
new file mode 100644
index 0000000000..2e6103402b
--- /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