From 594a48efccc81fa4196387bd83708124f2ebb5c4 Mon Sep 17 00:00:00 2001 From: Kyle Madsen Date: Thu, 22 Sep 2022 15:30:18 -0700 Subject: [PATCH] Remove mapbox navigation from PlaceListOnMapScreen --- libnavui-androidauto/CHANGELOG.md | 1 + libnavui-androidauto/api/current.txt | 11 +- .../mapbox/androidauto/car/MainActionStrip.kt | 10 - .../placeslistonmap/PlacesListOnMapManager.kt | 140 +++++++++++++ .../placeslistonmap/PlacesListOnMapScreen.kt | 184 ++++++------------ .../deeplink/GeoDeeplinkNavigateAction.kt | 9 - 6 files changed, 209 insertions(+), 146 deletions(-) create mode 100644 libnavui-androidauto/src/main/java/com/mapbox/androidauto/car/placeslistonmap/PlacesListOnMapManager.kt diff --git a/libnavui-androidauto/CHANGELOG.md b/libnavui-androidauto/CHANGELOG.md index 9cc64d33e06..880a2ee2ded 100644 --- a/libnavui-androidauto/CHANGELOG.md +++ b/libnavui-androidauto/CHANGELOG.md @@ -5,6 +5,7 @@ Mapbox welcomes participation and contributions from everyone. ## Unreleased #### Features #### Bug fixes and improvements +- Removed `MapboxNavigation` from `PlaceListOnMapScreen`. [#6371](https://github.com/mapbox/mapbox-navigation-android/pull/6371) ## androidauto-v0.12.0 - Sep 26, 2022 ### Changelog diff --git a/libnavui-androidauto/api/current.txt b/libnavui-androidauto/api/current.txt index c10255b0b02..b4c118c7a9f 100644 --- a/libnavui-androidauto/api/current.txt +++ b/libnavui-androidauto/api/current.txt @@ -604,13 +604,22 @@ package com.mapbox.androidauto.car.placeslistonmap { public static final class PlacesListOnMapLayerUtil.Companion { } + public final class PlacesListOnMapManager implements com.mapbox.maps.extension.androidauto.MapboxCarMapObserver { + ctor public PlacesListOnMapManager(com.mapbox.androidauto.car.placeslistonmap.PlacesListOnMapProvider placesListOnMapProvider); + method public androidx.car.app.model.ItemList? currentItemList(); + method public kotlinx.coroutines.flow.StateFlow> getPlaceRecords(); + method public kotlinx.coroutines.flow.StateFlow getPlaceSelected(); + property public final kotlinx.coroutines.flow.StateFlow> placeRecords; + property public final kotlinx.coroutines.flow.StateFlow placeSelected; + } + public interface PlacesListOnMapProvider { method public void cancel(); method public suspend Object? getPlaces(kotlin.coroutines.Continuation>>); } @com.mapbox.maps.MapboxExperimental public final class PlacesListOnMapScreen extends androidx.car.app.Screen { - ctor @UiThread public PlacesListOnMapScreen(com.mapbox.androidauto.car.search.SearchCarContext searchCarContext, com.mapbox.androidauto.car.placeslistonmap.PlacesListOnMapProvider placesProvider, com.mapbox.androidauto.car.placeslistonmap.PlacesListItemMapper placesListItemMapper, java.util.List actionProviders, com.mapbox.androidauto.car.placeslistonmap.PlacesListOnMapLayerUtil placesLayerUtil = com.mapbox.androidauto.car.placeslistonmap.PlacesListOnMapLayerUtil()); + ctor @UiThread public PlacesListOnMapScreen(com.mapbox.androidauto.car.search.SearchCarContext searchCarContext, com.mapbox.androidauto.car.placeslistonmap.PlacesListOnMapProvider placesProvider, java.util.List actionProviders); method public androidx.car.app.model.ItemList getItemList(); method public androidx.car.app.model.Template onGetTemplate(); method public void setItemList(androidx.car.app.model.ItemList); diff --git a/libnavui-androidauto/src/main/java/com/mapbox/androidauto/car/MainActionStrip.kt b/libnavui-androidauto/src/main/java/com/mapbox/androidauto/car/MainActionStrip.kt index 6147fcc45d3..075506b2ff6 100644 --- a/libnavui-androidauto/src/main/java/com/mapbox/androidauto/car/MainActionStrip.kt +++ b/libnavui-androidauto/src/main/java/com/mapbox/androidauto/car/MainActionStrip.kt @@ -9,8 +9,6 @@ import androidx.core.graphics.drawable.IconCompat import com.mapbox.androidauto.R import com.mapbox.androidauto.car.feedback.core.CarFeedbackSender import com.mapbox.androidauto.car.feedback.ui.CarFeedbackAction -import com.mapbox.androidauto.car.placeslistonmap.PlaceMarkerRenderer -import com.mapbox.androidauto.car.placeslistonmap.PlacesListItemMapper import com.mapbox.androidauto.car.placeslistonmap.PlacesListOnMapScreen import com.mapbox.androidauto.car.search.FavoritesApi import com.mapbox.androidauto.car.search.PlaceSearchScreen @@ -94,14 +92,6 @@ class MainActionStrip( return PlacesListOnMapScreen( SearchCarContext(mainCarContext), placesProvider, - PlacesListItemMapper( - PlaceMarkerRenderer(mainCarContext.carContext), - mainCarContext - .mapboxNavigation - .navigationOptions - .distanceFormatterOptions - .unitType - ), listOf( CarFeedbackAction( mainCarContext.mapboxCarMap, diff --git a/libnavui-androidauto/src/main/java/com/mapbox/androidauto/car/placeslistonmap/PlacesListOnMapManager.kt b/libnavui-androidauto/src/main/java/com/mapbox/androidauto/car/placeslistonmap/PlacesListOnMapManager.kt new file mode 100644 index 00000000000..00260c68461 --- /dev/null +++ b/libnavui-androidauto/src/main/java/com/mapbox/androidauto/car/placeslistonmap/PlacesListOnMapManager.kt @@ -0,0 +1,140 @@ +package com.mapbox.androidauto.car.placeslistonmap + +import androidx.car.app.model.ItemList +import com.mapbox.androidauto.car.search.PlaceRecord +import com.mapbox.androidauto.internal.car.extensions.handleStyleOnAttached +import com.mapbox.androidauto.internal.car.extensions.handleStyleOnDetached +import com.mapbox.androidauto.internal.car.extensions.mapboxNavigationForward +import com.mapbox.androidauto.internal.logAndroidAuto +import com.mapbox.androidauto.navigation.location.CarAppLocation +import com.mapbox.geojson.Feature +import com.mapbox.geojson.FeatureCollection +import com.mapbox.geojson.Point +import com.mapbox.maps.MapboxExperimental +import com.mapbox.maps.extension.androidauto.MapboxCarMapObserver +import com.mapbox.maps.extension.androidauto.MapboxCarMapSurface +import com.mapbox.maps.plugin.delegates.listeners.OnStyleLoadedListener +import com.mapbox.navigation.core.MapboxNavigation +import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@OptIn(MapboxExperimental::class) +class PlacesListOnMapManager( + private val placesListOnMapProvider: PlacesListOnMapProvider, +) : MapboxCarMapObserver { + + private var carMapSurface: MapboxCarMapSurface? = null + private var coroutineScope: CoroutineScope? = null + private var styleLoadedListener: OnStyleLoadedListener? = null + private var placesListItemMapper: PlacesListItemMapper? = null + private val placesLayerUtil: PlacesListOnMapLayerUtil = PlacesListOnMapLayerUtil() + private val navigationObserver = mapboxNavigationForward(this::onAttached) { onDetached() } + + private val _placeRecords = MutableStateFlow(listOf()) + val placeRecords: StateFlow> = _placeRecords.asStateFlow() + + private val _placeSelected = MutableStateFlow(null) + val placeSelected: StateFlow = _placeSelected.asStateFlow() + + private val placeClickListener = object : PlacesListItemClickListener { + override fun onItemClick(placeRecord: PlaceRecord) { + logAndroidAuto("PlacesListOnMapScreen request $placeRecord") + _placeSelected.value = placeRecord + } + } + + fun currentItemList(): ItemList? { + val carAppLocation = MapboxNavigationApp.getObserver(CarAppLocation::class) + val currentLocation = carAppLocation.navigationLocationProvider.lastLocation + ?: return null + return placesListItemMapper?.mapToItemList( + currentLocation, + placeRecords.value, + placeClickListener + ) + } + + override fun onAttached(mapboxCarMapSurface: MapboxCarMapSurface) { + super.onAttached(mapboxCarMapSurface) + carMapSurface = mapboxCarMapSurface + coroutineScope = MainScope() + MapboxNavigationApp.registerObserver(navigationObserver) + + styleLoadedListener = mapboxCarMapSurface.handleStyleOnAttached { + placesLayerUtil.initializePlacesListOnMapLayer( + it, + mapboxCarMapSurface.carContext.resources + ) + loadPlaceRecords() + } + } + + override fun onDetached(mapboxCarMapSurface: MapboxCarMapSurface) { + super.onDetached(mapboxCarMapSurface) + mapboxCarMapSurface.handleStyleOnDetached(styleLoadedListener)?.let { + placesLayerUtil.removePlacesListOnMapLayer(it) + } + styleLoadedListener = null + MapboxNavigationApp.unregisterObserver(navigationObserver) + carMapSurface = null + } + + private fun onAttached(mapboxNavigation: MapboxNavigation) { + placesListItemMapper = PlacesListItemMapper( + PlaceMarkerRenderer(carMapSurface?.carContext!!), + mapboxNavigation + .navigationOptions + .distanceFormatterOptions + .unitType + ) + } + + private fun onDetached() { + placesListItemMapper = null + coroutineScope?.cancel() + coroutineScope = null + } + + private fun loadPlaceRecords() { + coroutineScope?.launch { + val expectedPlaceRecords = withContext(Dispatchers.IO) { + placesListOnMapProvider.getPlaces() + } + _placeRecords.value = emptyList() + expectedPlaceRecords.fold( + { + logAndroidAuto( + "PlacesListOnMapScreen ${it.errorMessage}, ${it.throwable?.stackTrace}" + ) + }, + { + _placeRecords.value = it + addPlaceIconsToMap(it) + } + ) + } + } + + private fun addPlaceIconsToMap(places: List) { + logAndroidAuto("PlacesListOnMapScreen addPlaceIconsToMap with ${places.size} places.") + carMapSurface?.mapSurface?.getMapboxMap()?.let { mapboxMap -> + val features = places.filter { it.coordinate != null }.map { + Feature.fromGeometry( + Point.fromLngLat(it.coordinate!!.longitude(), it.coordinate.latitude()) + ) + } + val featureCollection = FeatureCollection.fromFeatures(features) + mapboxMap.getStyle()?.let { + placesLayerUtil.updatePlacesListOnMapLayer(it, featureCollection) + } + } + } +} diff --git a/libnavui-androidauto/src/main/java/com/mapbox/androidauto/car/placeslistonmap/PlacesListOnMapScreen.kt b/libnavui-androidauto/src/main/java/com/mapbox/androidauto/car/placeslistonmap/PlacesListOnMapScreen.kt index 5b49b5f6aad..cab24708b15 100644 --- a/libnavui-androidauto/src/main/java/com/mapbox/androidauto/car/placeslistonmap/PlacesListOnMapScreen.kt +++ b/libnavui-androidauto/src/main/java/com/mapbox/androidauto/car/placeslistonmap/PlacesListOnMapScreen.kt @@ -11,7 +11,7 @@ import androidx.car.app.model.Template import androidx.car.app.navigation.model.PlaceListNavigationTemplate import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner -import com.mapbox.androidauto.MapboxCarApp +import androidx.lifecycle.lifecycleScope import com.mapbox.androidauto.R import com.mapbox.androidauto.car.action.MapboxActionProvider import com.mapbox.androidauto.car.location.CarLocationRenderer @@ -21,136 +21,69 @@ import com.mapbox.androidauto.car.preview.CarRouteRequestCallback import com.mapbox.androidauto.car.preview.RoutePreviewCarContext import com.mapbox.androidauto.car.search.PlaceRecord import com.mapbox.androidauto.car.search.SearchCarContext -import com.mapbox.androidauto.internal.car.extensions.getStyle -import com.mapbox.androidauto.internal.car.extensions.handleStyleOnAttached -import com.mapbox.androidauto.internal.car.extensions.handleStyleOnDetached import com.mapbox.androidauto.internal.logAndroidAuto -import com.mapbox.geojson.Feature -import com.mapbox.geojson.FeatureCollection -import com.mapbox.geojson.Point import com.mapbox.maps.MapboxExperimental -import com.mapbox.maps.extension.androidauto.MapboxCarMapObserver -import com.mapbox.maps.extension.androidauto.MapboxCarMapSurface -import com.mapbox.maps.plugin.delegates.listeners.OnStyleLoadedListener import com.mapbox.navigation.base.route.NavigationRoute -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancelChildren +import com.mapbox.navigation.core.lifecycle.MapboxNavigationApp +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.util.concurrent.CopyOnWriteArrayList @MapboxExperimental class PlacesListOnMapScreen @UiThread constructor( private val searchCarContext: SearchCarContext, - private val placesProvider: PlacesListOnMapProvider, - private val placesListItemMapper: PlacesListItemMapper, + placesProvider: PlacesListOnMapProvider, private val actionProviders: List, - private val placesLayerUtil: PlacesListOnMapLayerUtil = PlacesListOnMapLayerUtil() ) : Screen(searchCarContext.carContext) { @VisibleForTesting var itemList = buildErrorItemList(R.string.car_search_no_results) - private val placeRecords by lazy { CopyOnWriteArrayList() } - private val jobControl by lazy { searchCarContext.mainCarContext.getJobControl() } private val carNavigationCamera = CarLocationsOverviewCamera() - private val locationRenderer = CarLocationRenderer(searchCarContext.mainCarContext) - private var styleLoadedListener: OnStyleLoadedListener? = null - - private val surfaceListener = object : MapboxCarMapObserver { - - override fun onAttached(mapboxCarMapSurface: MapboxCarMapSurface) { - super.onAttached(mapboxCarMapSurface) - logAndroidAuto("PlacesListOnMapScreen loaded") - styleLoadedListener = mapboxCarMapSurface.handleStyleOnAttached { - placesLayerUtil.initializePlacesListOnMapLayer( - it, - carContext.resources - ) - loadPlaceRecords() - } - } + private var carLocationRenderer = CarLocationRenderer(searchCarContext.mainCarContext) + private val placesListOnMapManager = PlacesListOnMapManager(placesProvider) - override fun onDetached(mapboxCarMapSurface: MapboxCarMapSurface) { - super.onDetached(mapboxCarMapSurface) - logAndroidAuto("PlacesListOnMapScreen detached") - mapboxCarMapSurface.handleStyleOnDetached(styleLoadedListener)?.let { - placesLayerUtil.removePlacesListOnMapLayer(it) + init { + lifecycleScope.launch { + placesListOnMapManager.placeRecords.collect { placeRecords -> + onPlaceRecordsChanged(placeRecords) } } - } - - private val placeClickListener = object : PlacesListItemClickListener { - - override fun onItemClick(placeRecord: PlaceRecord) { - logAndroidAuto("PlacesListOnMapScreen request $placeRecord") - searchCarContext.carRouteRequest.request(placeRecord, carRouteRequestCallback) - } - } - - private val carRouteRequestCallback = object : CarRouteRequestCallback { - - override fun onRoutesReady(placeRecord: PlaceRecord, routes: List) { - val routePreviewCarContext = RoutePreviewCarContext(searchCarContext.mainCarContext) - logAndroidAuto("PlacesListOnMapScreen go to CarRoutePreviewScreen ${routes.size}") - screenManager.push(CarRoutePreviewScreen(routePreviewCarContext, placeRecord, routes)) - } - - override fun onUnknownCurrentLocation() { - onErrorItemList(R.string.car_search_unknown_current_location) - } - - override fun onDestinationLocationUnknown() { - onErrorItemList(R.string.car_search_unknown_search_location) - } - - override fun onNoRoutesFound() { - onErrorItemList(R.string.car_search_no_results) + lifecycleScope.launch { + placesListOnMapManager.placeSelected.filterNotNull().collect { placeRecord -> + onPlaceRecordSelected(placeRecord) + } } - } - - init { lifecycle.addObserver(object : DefaultLifecycleObserver { override fun onCreate(owner: LifecycleOwner) { - logAndroidAuto("PlacesListOnMapScreen onCreate") + super.onCreate(owner) + MapboxNavigationApp.registerObserver(searchCarContext.carRouteRequest) } override fun onDestroy(owner: LifecycleOwner) { - logAndroidAuto("PlacesListOnMapScreen onDestroy") - } - - override fun onStart(owner: LifecycleOwner) { - logAndroidAuto("PlacesListOnMapScreen onStart") - } - - override fun onStop(owner: LifecycleOwner) { - logAndroidAuto("PlacesListOnMapScreen onStop") + super.onDestroy(owner) + MapboxNavigationApp.unregisterObserver(searchCarContext.carRouteRequest) } override fun onResume(owner: LifecycleOwner) { - logAndroidAuto("PlacesListOnMapScreen onResume") - searchCarContext.mapboxCarMap.registerObserver(surfaceListener) - searchCarContext.mapboxCarMap.registerObserver(carNavigationCamera) - searchCarContext.mapboxCarMap.registerObserver(locationRenderer) + searchCarContext.mapboxCarMap + .registerObserver(carNavigationCamera) + .registerObserver(carLocationRenderer) + .registerObserver(placesListOnMapManager) } override fun onPause(owner: LifecycleOwner) { - logAndroidAuto("PlacesListOnMapScreen onPause") - placesProvider.cancel() - jobControl.job.cancelChildren() - searchCarContext.mapboxCarMap.unregisterObserver(locationRenderer) - searchCarContext.mapboxCarMap.unregisterObserver(carNavigationCamera) - searchCarContext.mapboxCarMap.unregisterObserver(surfaceListener) + super.onPause(owner) + searchCarContext.mapboxCarMap + .unregisterObserver(carNavigationCamera) + .unregisterObserver(carLocationRenderer) + .unregisterObserver(placesListOnMapManager) } }) } override fun onGetTemplate(): Template { - addPlaceIconsToMap(placeRecords) - val locationProvider = MapboxCarApp.carAppLocationService().navigationLocationProvider - val placesItemList = locationProvider.lastLocation?.run { - placesListItemMapper.mapToItemList(this, placeRecords, placeClickListener) - } ?: ItemList.Builder().build() + val placesItemList = placesListOnMapManager.currentItemList() ?: ItemList.Builder().build() val actionStrip = ActionStrip.Builder().apply { actionProviders.forEach { when (it) { @@ -171,41 +104,40 @@ class PlacesListOnMapScreen @UiThread constructor( .build() } - private fun addPlaceIconsToMap(places: List) { - logAndroidAuto("PlacesListOnMapScreen addPlaceIconsToMap with ${places.size} places.") - searchCarContext.mapboxCarMap.carMapSurface?.let { carMapSurface -> - val features = places.filter { it.coordinate != null }.map { - Feature.fromGeometry( - Point.fromLngLat(it.coordinate!!.longitude(), it.coordinate.latitude()) - ) + private fun onPlaceRecordsChanged(placeRecords: List) { + invalidate() + val coordinates = placeRecords.mapNotNull { it.coordinate } + carNavigationCamera.updateWithLocations(coordinates) + } + + private fun onPlaceRecordSelected(placeRecord: PlaceRecord) { + val carRouteRequestCallback = object : CarRouteRequestCallback { + override fun onRoutesReady(placeRecord: PlaceRecord, routes: List) { + onPlaceRecordSelectedRoutesReady(placeRecord, routes) } - val featureCollection = FeatureCollection.fromFeatures(features) - carMapSurface.getStyle()?.let { - placesLayerUtil.updatePlacesListOnMapLayer(it, featureCollection) + + override fun onUnknownCurrentLocation() { + onErrorItemList(R.string.car_search_unknown_current_location) } - } - val placesWithCoordinates = places.mapNotNull { it.coordinate } - carNavigationCamera.updateWithLocations(placesWithCoordinates) - } - private fun loadPlaceRecords() { - jobControl.scope.launch { - val expectedPlaceRecords = withContext(Dispatchers.IO) { - placesProvider.getPlaces() + override fun onDestinationLocationUnknown() { + onErrorItemList(R.string.car_search_unknown_search_location) + } + + override fun onNoRoutesFound() { + onErrorItemList(R.string.car_search_no_results) } - placeRecords.clear() - expectedPlaceRecords.fold( - { - logAndroidAuto( - "PlacesListOnMapScreen ${it.errorMessage}, ${it.throwable?.stackTrace}" - ) - }, - { - placeRecords.addAll(it) - invalidate() - } - ) } + searchCarContext.carRouteRequest.request(placeRecord, carRouteRequestCallback) + } + + private fun onPlaceRecordSelectedRoutesReady( + placeRecord: PlaceRecord, + routes: List + ) { + logAndroidAuto("PlacesListOnMapScreen go to CarRoutePreviewScreen ${routes.size}") + val routePreviewCarContext = RoutePreviewCarContext(searchCarContext.mainCarContext) + screenManager.push(CarRoutePreviewScreen(routePreviewCarContext, placeRecord, routes)) } private fun onErrorItemList(@StringRes stringRes: Int) { diff --git a/libnavui-androidauto/src/main/java/com/mapbox/androidauto/deeplink/GeoDeeplinkNavigateAction.kt b/libnavui-androidauto/src/main/java/com/mapbox/androidauto/deeplink/GeoDeeplinkNavigateAction.kt index 1e295ac6c24..ee3e44c46db 100644 --- a/libnavui-androidauto/src/main/java/com/mapbox/androidauto/deeplink/GeoDeeplinkNavigateAction.kt +++ b/libnavui-androidauto/src/main/java/com/mapbox/androidauto/deeplink/GeoDeeplinkNavigateAction.kt @@ -6,8 +6,6 @@ import androidx.lifecycle.Lifecycle import com.mapbox.androidauto.car.MainCarContext import com.mapbox.androidauto.car.feedback.core.CarFeedbackSender import com.mapbox.androidauto.car.feedback.ui.CarFeedbackAction -import com.mapbox.androidauto.car.placeslistonmap.PlaceMarkerRenderer -import com.mapbox.androidauto.car.placeslistonmap.PlacesListItemMapper import com.mapbox.androidauto.car.placeslistonmap.PlacesListOnMapScreen import com.mapbox.androidauto.car.search.SearchCarContext import com.mapbox.androidauto.internal.logAndroidAuto @@ -46,13 +44,6 @@ class GeoDeeplinkNavigateAction( return PlacesListOnMapScreen( SearchCarContext(mainCarContext), placesProvider, - PlacesListItemMapper( - PlaceMarkerRenderer(mainCarContext.carContext), - mapboxNavigation - .navigationOptions - .distanceFormatterOptions - .unitType - ), listOf( CarFeedbackAction( mainCarContext.mapboxCarMap,