diff --git a/.circleci/config.yml b/.circleci/config.yml index 7931493515f..ec9b53cc2a7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -35,6 +35,7 @@ workflows: branches: only: - main + - /^tarigo.*/ - release: filters: tags: @@ -650,4 +651,4 @@ jobs: - publish-artifacts: artifact-type: "core" - publish-artifacts: - artifact-type: "ui" \ No newline at end of file + artifact-type: "ui" diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dcefd42ec5..25846caf88b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,46 @@ Mapbox welcomes participation and contributions from everyone. +## Mapbox Navigation SDK 2.1.1 - December 13, 2021 + +For details on how v2 differs from v1 and guidance on migrating from v1 of the Mapbox Navigation SDK for Android to the v2 public preview, see [2.0 Navigation SDK Migration Guide](https://github.com/mapbox/mapbox-navigation-android/wiki/2.0-Navigation-SDK-Migration-Guide). + +### Changelog +#### Bug fixes and improvements +- Fixed billing issue when upgrading Mapbox Maps SDK from v9 to v10. [#5263](https://github.com/mapbox/mapbox-navigation-android/pull/5263) +- Fixed notification appearance on Android 12. [#5159](https://github.com/mapbox/mapbox-navigation-android/pull/5159) + +### Mapbox dependencies +This release depends on, and has been tested with, the following Mapbox dependencies: +- Mapbox Maps SDK `v10.0.2` ([release notes](https://github.com/mapbox/mapbox-maps-android/releases/tag/android-v10.0.2)) +- Mapbox Navigation Native `v79.0.3` +- Mapbox Core Common `v20.0.3` +- Mapbox Java `v6.1.0` ([release notes](https://github.com/mapbox/mapbox-java/releases/tag/v6.1.0)) +- Mapbox Android Core `v5.0.0` +- Mapbox Android Telemetry `v8.1.0` + +## Mapbox Navigation SDK 2.1.0 - December 2, 2021 +Mapbox Navigation SDK v2.1.0 was released prematurely with an unresolved billing issue. We deleted this version from the Maven repository and we’re preparing the v2.1.1 release with additional fixes. + +We strongly recommend not to release your application with v2.1.0. More information will be available in [this announcement](https://github.com/mapbox/mapbox-navigation-android/discussions/5213). + +## Mapbox Navigation SDK 2.1.0-rc.2 - November 18, 2021 + +For details on how v2 differs from v1 and guidance on migrating from v1 of the Mapbox Navigation SDK for Android to the v2 public preview, see [2.0 Navigation SDK Migration Guide](https://github.com/mapbox/mapbox-navigation-android/wiki/2.0-Navigation-SDK-Migration-Guide). + +### Changelog +#### Bug fixes and improvements +- Trigger RouteAlternativesObserver on main thread. [#5120](https://github.com/mapbox/mapbox-navigation-android/pull/5120) + +### Mapbox dependencies +This release depends on, and has been tested with, the following Mapbox dependencies: +- Mapbox Maps SDK `v10.0.0` ([release notes](https://github.com/mapbox/mapbox-maps-android/releases/tag/android-v10.0.0)) +- Mapbox Navigation Native `v79.0.3` +- Mapbox Core Common `v20.0.0` +- Mapbox Java `v6.1.0` ([release notes](https://github.com/mapbox/mapbox-java/releases/tag/v6.1.0)) +- Mapbox Android Core `v5.0.0` +- Mapbox Android Telemetry `v8.1.0` + ## Mapbox Navigation SDK 2.1.0-rc.1 - October 28, 2021 For details on how v2 differs from v1 and guidance on migrating from v1 of the Mapbox Navigation SDK for Android to the v2 public preview, see [2.0 Navigation SDK Migration Guide](https://github.com/mapbox/mapbox-navigation-android/wiki/2.0-Navigation-SDK-Migration-Guide). diff --git a/build.gradle b/build.gradle index 5325883395c..13c696314b9 100644 --- a/build.gradle +++ b/build.gradle @@ -60,7 +60,7 @@ allprojects { // we allow access to snapshots repo if ALLOW_SNAPSHOT_REPOSITORY is set, what means we are running on CI // with Navigation Native forced to be some snapshot version // if you need to use snapshots while development, just set `addSnapshotsRepo` to true manually - def addSnapshotsRepo = System.getenv("ALLOW_SNAPSHOT_REPOSITORY")?.toBoolean() ?: false + def addSnapshotsRepo = true //System.getenv("ALLOW_SNAPSHOT_REPOSITORY")?.toBoolean() ?: false if (addSnapshotsRepo) { println("Snapshot repository reference added.") maven { diff --git a/gradle.properties b/gradle.properties index 58ff2311bc0..e93b89688d2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ POM_LICENCE_DIST=repo POM_DEVELOPER_ID=mapbox POM_DEVELOPER_NAME=Mapbox -POM_SNAPSHOT_VERSION_NAME=2.1.0-SNAPSHOT +POM_SNAPSHOT_VERSION_NAME=2.1.1-test-core-telemetry-SNAPSHOT # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 637cfd6f2b0..e6787afc3da 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -12,17 +12,17 @@ ext { // version which we should use in this build def mapboxNavigatorVersion = System.getenv("FORCE_MAPBOX_NAVIGATION_NATIVE_VERSION") if (mapboxNavigatorVersion == null || mapboxNavigatorVersion == '') { - mapboxNavigatorVersion = '79.0.2' + mapboxNavigatorVersion = '79.0.3' } println("Navigation Native version: " + mapboxNavigatorVersion) version = [ - mapboxMapSdk : '10.0.0', + mapboxMapSdk : '10.1.2-test-core-telemetry-SNAPSHOT', mapboxSdkServices : '6.1.0', mapboxEvents : '8.1.0', mapboxCore : '5.0.0', mapboxNavigator : "${mapboxNavigatorVersion}", - mapboxCommonNative : '20.0.0', + mapboxCommonNative : '20.1.1-b1dc6ad813d20bd73288a64e9344c260484fe2ff-SNAPSHOT', mapboxCrashMonitor : '2.0.0', mapboxAnnotationPlugin : '0.8.0', mapboxBaseAndroid : '0.5.0', diff --git a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteAlternativesTest.kt b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteAlternativesTest.kt index 0dfa79966ed..1b867d72080 100644 --- a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteAlternativesTest.kt +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/core/RouteAlternativesTest.kt @@ -104,6 +104,7 @@ class RouteAlternativesTest : BaseTest(EmptyTestActivity::cla Espresso.onIdle() } firstAlternative.unregister() + assertTrue(firstAlternative.calledOnMainThread) runOnMainSync { val countDownLatch = CountDownLatch(1) diff --git a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/idling/RouteAlternativesIdlingResource.kt b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/idling/RouteAlternativesIdlingResource.kt index fd0552e832d..b65f8bc6661 100644 --- a/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/idling/RouteAlternativesIdlingResource.kt +++ b/instrumentation-tests/src/androidTest/java/com/mapbox/navigation/instrumentation_tests/utils/idling/RouteAlternativesIdlingResource.kt @@ -1,5 +1,6 @@ package com.mapbox.navigation.instrumentation_tests.utils.idling +import android.os.Looper import androidx.test.espresso.IdlingResource import com.mapbox.api.directions.v5.models.DirectionsRoute import com.mapbox.navigation.base.route.RouterOrigin @@ -24,6 +25,7 @@ class RouteAlternativesIdlingResource( private set var alternatives: List? = null private set + var calledOnMainThread = true private var callback: IdlingResource.ResourceCallback? = null @@ -47,6 +49,11 @@ class RouteAlternativesIdlingResource( alternatives: List, routerOrigin: RouterOrigin ) { + // Verify this happens on the main thread. + if (Looper.myLooper() != Looper.getMainLooper()) { + calledOnMainThread = false + } + mapboxNavigation.unregisterRouteAlternativesObserver(this) this.routeProgress = routeProgress this.alternatives = alternatives diff --git a/libnavigation-base/src/main/java/com/mapbox/navigation/base/metrics/MetricEvent.kt b/libnavigation-base/src/main/java/com/mapbox/navigation/base/metrics/MetricEvent.kt index 47539965435..b46e87deb90 100644 --- a/libnavigation-base/src/main/java/com/mapbox/navigation/base/metrics/MetricEvent.kt +++ b/libnavigation-base/src/main/java/com/mapbox/navigation/base/metrics/MetricEvent.kt @@ -2,6 +2,7 @@ package com.mapbox.navigation.base.metrics import androidx.annotation.StringDef import com.google.gson.Gson +import com.mapbox.bindgen.Value /** * An interface with types of metrics events that the SDK would send via Telemetry @@ -39,6 +40,13 @@ interface MetricEvent { * @return String */ fun toJson(gson: Gson): String + + /** + * Present [MetricEvent] as [Value] + * + * @return Value + */ + fun toValue(): Value } /** diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt index 52f60647822..563ad0cefcb 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/MapboxNavigation.kt @@ -426,7 +426,8 @@ class MapboxNavigation @VisibleForTesting internal constructor( routeAlternativesController = RouteAlternativesControllerProvider.create( navigationOptions.routeAlternativesOptions, navigator, - tripSession + tripSession, + threadController, ) routeRefreshController = RouteRefreshControllerProvider.createRouteRefreshController( navigationOptions.routeRefreshOptions, diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesController.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesController.kt index 6b96f8a93d2..71c27b37ced 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesController.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesController.kt @@ -1,23 +1,30 @@ package com.mapbox.navigation.core.routealternatives -import com.mapbox.api.directions.v5.models.DirectionsRoute import com.mapbox.base.common.logger.model.Message import com.mapbox.base.common.logger.model.Tag import com.mapbox.navigation.base.internal.utils.parseDirectionsResponse import com.mapbox.navigation.base.route.RouteAlternativesOptions import com.mapbox.navigation.base.route.RouterOrigin +import com.mapbox.navigation.base.trip.model.RouteProgress import com.mapbox.navigation.core.trip.session.TripSession import com.mapbox.navigation.navigator.internal.MapboxNativeNavigator +import com.mapbox.navigation.utils.internal.ThreadController import com.mapbox.navigation.utils.internal.logI +import com.mapbox.navigator.RouteAlternative +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import java.util.concurrent.CopyOnWriteArraySet internal class RouteAlternativesController constructor( private val options: RouteAlternativesOptions, private val navigator: MapboxNativeNavigator, - private val tripSession: TripSession + private val tripSession: TripSession, + private val threadController: ThreadController ) { + private val mainJobControl by lazy { threadController.getMainScopeAndRootJob() } + private val nativeRouteAlternativesController = navigator.createRouteAlternativesController() .apply { setRouteAlternativesOptions( @@ -44,6 +51,7 @@ internal class RouteAlternativesController constructor( observers.remove(routeAlternativesObserver) if (observers.isEmpty()) { nativeRouteAlternativesController.removeObserver(nativeObserver) + mainJobControl.job.cancelChildren() } } @@ -58,16 +66,28 @@ internal class RouteAlternativesController constructor( private val nativeObserver = object : com.mapbox.navigator.RouteAlternativesObserver { override fun onRouteAlternativesChanged( - routeAlternatives: List + routeAlternatives: List ): List { val routeProgress = tripSession.getRouteProgress() ?: return emptyList() - // Map the alternatives from nav-native, add the existing RouteOptions. + onRouteAlternativesChanged(routeProgress, routeAlternatives) + + // This is supposed to be able to filter alternatives. If we want to provide + // a mechanism to let downstream developers edit the routes - we should remove + // the call to directionsSession.setRoutes + return emptyList() + } + } - // TODO make async - val alternatives: List = runBlocking { - routeAlternatives.map { routeAlternative -> + private fun onRouteAlternativesChanged( + routeProgress: RouteProgress, + routeAlternatives: List + ) { + // Map the alternatives from nav-native, add the existing RouteOptions. + val alternatives = runBlocking { + routeAlternatives + .map { routeAlternative -> parseDirectionsResponse( routeAlternative.route, routeProgress.route.routeOptions() @@ -75,19 +95,15 @@ internal class RouteAlternativesController constructor( logI(TAG, Message("Response metadata: $it")) }.first() } - } + } + mainJobControl.scope.launch { // Notify the listeners. // TODO https://github.com/mapbox/mapbox-navigation-native/issues/4409 // There is no way to determine if the route was Onboard or Offboard observers.forEach { it.onRouteAlternatives(routeProgress, alternatives, RouterOrigin.Onboard) } - - // This is supposed to be able to filter alternatives. If we want to provide - // a mechanism to let downstream developers edit the routes - we should remove - // the call to directionsSession.setRoutes - return emptyList() } } diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesControllerProvider.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesControllerProvider.kt index 8ec105da2bc..1b2e9be7642 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesControllerProvider.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesControllerProvider.kt @@ -3,16 +3,19 @@ package com.mapbox.navigation.core.routealternatives import com.mapbox.navigation.base.route.RouteAlternativesOptions import com.mapbox.navigation.core.trip.session.TripSession import com.mapbox.navigation.navigator.internal.MapboxNativeNavigator +import com.mapbox.navigation.utils.internal.ThreadController internal object RouteAlternativesControllerProvider { fun create( options: RouteAlternativesOptions, navigator: MapboxNativeNavigator, - tripSession: TripSession + tripSession: TripSession, + threadController: ThreadController ) = RouteAlternativesController( options, navigator, - tripSession + tripSession, + threadController ) } diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routeoptions/RouteOptionsUpdater.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routeoptions/RouteOptionsUpdater.kt index 4925eabb69c..d4c49ce2d83 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/routeoptions/RouteOptionsUpdater.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/routeoptions/RouteOptionsUpdater.kt @@ -1,5 +1,6 @@ package com.mapbox.navigation.core.routeoptions +import com.mapbox.api.directions.v5.DirectionsCriteria import com.mapbox.api.directions.v5.models.Bearing import com.mapbox.api.directions.v5.models.RouteOptions import com.mapbox.base.common.logger.model.Message @@ -134,16 +135,22 @@ class RouteOptionsUpdater { coordinatesList.size - remainingWaypoints - 1 ) ) - .layersList( - mutableListOf(locationMatcherResult.zLevel).apply { - val legacyLayerList = routeOptions.layersList() - if (legacyLayerList != null) { - addAll(legacyLayerList.takeLast(remainingWaypoints)) - } else { - repeat(remainingWaypoints) { add(null) } - } + } + + if ( + routeOptions.profile() == DirectionsCriteria.PROFILE_DRIVING || + routeOptions.profile() == DirectionsCriteria.PROFILE_DRIVING_TRAFFIC + ) { + optionsBuilder.layersList( + mutableListOf(locationMatcherResult.zLevel).apply { + val legacyLayerList = routeOptions.layersList() + if (legacyLayerList != null) { + addAll(legacyLayerList.takeLast(remainingWaypoints)) + } else { + repeat(remainingWaypoints) { add(null) } } - ) + } + ) } optionsBuilder.arriveBy(null) diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/MapboxNavigationTelemetry.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/MapboxNavigationTelemetry.kt index 840f9ab4aa9..2f227867267 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/MapboxNavigationTelemetry.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/MapboxNavigationTelemetry.kt @@ -15,6 +15,7 @@ import com.mapbox.navigation.base.metrics.MetricsReporter import com.mapbox.navigation.base.options.NavigationOptions import com.mapbox.navigation.base.trip.model.RouteLegProgress import com.mapbox.navigation.base.trip.model.RouteProgress +import com.mapbox.navigation.base.trip.model.RouteProgressState import com.mapbox.navigation.core.BuildConfig import com.mapbox.navigation.core.MapboxNavigation import com.mapbox.navigation.core.arrival.ArrivalObserver @@ -93,18 +94,31 @@ private data class SessionMetadata( * @param timeOfReroute time of reroute. Unit is **time in millis**. * @param timeSinceLastReroute time since last reroute. Unit is **millis**. * @param driverModeArrivalTime arrival time of driver mode + * @param currentDistanceTraveled for the active session */ private data class DynamicSessionValues( var rerouteCount: Int = 0, var timeOfReroute: Long = 0L, var timeSinceLastReroute: Int = 0, var driverModeArrivalTime: Date? = null, + var currentDistanceTraveled: Int = 0, + var accumulatedDistanceTraveled: Int = 0, ) { fun reset() { rerouteCount = 0 timeOfReroute = 0 timeSinceLastReroute = 0 driverModeArrivalTime = null + currentDistanceTraveled = 0 + accumulatedDistanceTraveled = 0 + } + + fun accumulateDistanceTraveled(distance: Int) { + accumulatedDistanceTraveled += distance + } + + fun resetCurrentDistanceTraveled() { + currentDistanceTraveled = 0 } } @@ -289,6 +303,16 @@ internal object MapboxNavigationTelemetry { private val routeProgressObserver = RouteProgressObserver { routeProgress -> this.routeData.routeProgress = routeProgress + val dynamicValues = getSessionMetadataIfTelemetryRunning()?.dynamicValues + if (routeProgress.currentState == RouteProgressState.OFF_ROUTE) { + dynamicValues?.accumulateDistanceTraveled( + routeProgress.distanceTraveled.toInt() + ) + dynamicValues?.resetCurrentDistanceTraveled() + } else { + dynamicValues?.currentDistanceTraveled = + routeProgress.distanceTraveled.toInt() + } } private val onRouteDataChanged: () -> Unit = { @@ -480,6 +504,8 @@ internal object MapboxNavigationTelemetry { this.feedbackSubType = feedbackSubType this.locationsBefore = feedbackMetadata.locationsBeforeEvent this.locationsAfter = feedbackMetadata.locationsAfterEvent + val distanceTraveled = + getSessionMetadataIfTelemetryRunning()?.dynamicValues.retrieveDistanceTraveled() populate( this@MapboxNavigationTelemetry.sdkIdentifier, null, @@ -493,6 +519,7 @@ internal object MapboxNavigationTelemetry { feedbackMetadata.driverMode, feedbackMetadata.driverModeStartTime, feedbackMetadata.rerouteCount, + distanceTraveled, feedbackMetadata.eventVersion, feedbackMetadata.appMetadata, ) @@ -727,6 +754,7 @@ internal object MapboxNavigationTelemetry { } private fun NavigationEvent.populateWithLocalVars(sessionMetadata: SessionMetadata?) { + val distanceTraveled = sessionMetadata?.dynamicValues.retrieveDistanceTraveled() this.populate( this@MapboxNavigationTelemetry.sdkIdentifier, routeData.originalRoute, @@ -740,11 +768,18 @@ internal object MapboxNavigationTelemetry { sessionMetadata?.telemetryNavSessionState?.getModeName(), sessionMetadata?.driverModeStartTime?.let { generateCreateDateFormatted(it) }, sessionMetadata?.dynamicValues?.rerouteCount, + distanceTraveled, EVENT_VERSION, createAppMetadata() ) } + private fun DynamicSessionValues?.retrieveDistanceTraveled(): Int { + val currentDistanceTraveled = this?.currentDistanceTraveled ?: 0 + val accumulatedDistanceTraveled = this?.accumulatedDistanceTraveled ?: 0 + return currentDistanceTraveled + accumulatedDistanceTraveled + } + private fun NavigationFreeDriveEvent.populate( type: FreeDriveEventType, navSessionIdentifier: String, diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/NavEventsPopulateUtil.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/NavEventsPopulateUtil.kt index 0c073319c8a..31f84eb0e1c 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/NavEventsPopulateUtil.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/NavEventsPopulateUtil.kt @@ -28,6 +28,7 @@ import com.mapbox.navigation.utils.internal.logD * @param driverModeStartTime driver mode start time. * Use [TelemetryUtils.generateCreateDateFormatted] * @param rerouteCount reroute count + * @param distanceTraveled accumulated for the session * @param eventVersion events version [MapboxNavigationTelemetry.EVENT_VERSION] * @param appMetadata use [MapboxNavigationTelemetry.createAppMetadata] */ @@ -44,6 +45,7 @@ internal fun NavigationEvent.populate( @FeedbackEvent.DriverMode driverMode: String?, driverModeStartTime: String?, rerouteCount: Int?, + distanceTraveled: Int, eventVersion: Int, appMetadata: AppMetadata?, ) { @@ -56,7 +58,7 @@ internal fun NavigationEvent.populate( distanceRemaining = routeProgressNonNull.distanceRemaining.toInt() durationRemaining = routeProgressNonNull.durationRemaining.toInt() - distanceCompleted = routeProgressNonNull.distanceTraveled.toInt() + distanceCompleted = distanceTraveled routeProgressNonNull.route.let { geometry = it.geometry() diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/NavTelemetryUtils.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/NavTelemetryUtils.kt index 327db431cbe..f66e9c03c96 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/NavTelemetryUtils.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/NavTelemetryUtils.kt @@ -5,7 +5,7 @@ import android.location.Location import android.media.AudioManager import android.provider.Settings import android.text.TextUtils -import com.mapbox.android.telemetry.TelemetryUtils +import com.mapbox.common.TelemetryUtils import com.mapbox.api.directions.v5.models.DirectionsRoute import com.mapbox.core.constants.Constants import com.mapbox.geojson.Point diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/CoreTelemetryEventUtils.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/CoreTelemetryEventUtils.kt new file mode 100644 index 00000000000..dda1ccf6601 --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/CoreTelemetryEventUtils.kt @@ -0,0 +1,68 @@ +package com.mapbox.navigation.core.telemetry.events + +import com.mapbox.bindgen.Value + +internal fun String.toValue(): Value = Value.valueOf(this) +internal fun Boolean.toValue(): Value = Value.valueOf(this) +internal fun Int.toValue(): Value = Value.valueOf(this.toLong()) +internal fun Double.toValue(): Value = Value.valueOf(this) +internal fun Float.toValue(): Value = Value.valueOf(this.toDouble()) + +internal fun TelemetryLocation.toValue(): Value { + val fields = hashMapOf() + fields["lat"] = latitude.toValue() + fields["lng"] = longitude.toValue() + fields["speed"] = speed.toValue() + fields["course"] = bearing.toValue() + fields["altitude"] = altitude.toValue() + fields["timestamp"] = timestamp.toValue() + fields["horizontalAccuracy"] = horizontalAccuracy.toValue() + fields["verticalAccuracy"] = verticalAccuracy.toValue() + return Value.valueOf(fields) +} + +internal fun AppMetadata.toValue(): Value { + val fields = hashMapOf() + fields["name"] = name.toValue() + fields["version"] = version.toValue() + userId?.let { fields["userId"] = it.toValue() } + sessionId?.let { fields["sessionId"] = it.toValue() } + return Value.valueOf(fields) +} + +internal fun NavigationStepData.toValue(): Value { + val fields = hashMapOf() + + upcomingInstruction?.let { fields["upcomingInstruction"] = it.toValue() } + upcomingModifier?.let { fields["upcomingModifier"] = it.toValue() } + upcomingName?.let { fields["upcomingName"] = it.toValue() } + upcomingType?.let { fields["upcomingType"] = it.toValue() } + previousInstruction?.let { fields["previousInstruction"] = it.toValue() } + previousModifier?.let { fields["previousModifier"] = it.toValue() } + fields["previousName"] = previousName.toValue() + previousType?.let { fields["previousType"] = it.toValue() } + fields["distance"] = distance.toValue() + fields["duration"] = duration.toValue() + fields["distanceRemaining"] = distanceRemaining.toValue() + fields["durationRemaining"] = durationRemaining.toValue() + + return Value.valueOf(fields) +} + +/* FIXME: find the way to generalize following functions with compile-time check. */ + +internal fun Array.toValue(): Value { + val values = mutableListOf() + for (item in this) { + values.add(item.toValue()) + } + return Value.valueOf(values) +} + +internal fun Array.toValue(): Value { + val values = mutableListOf() + for (item in this) { + values.add(item.toValue()) + } + return Value.valueOf(values) +} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/NavigationCancelEvent.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/NavigationCancelEvent.kt index e141066734e..e83968f612f 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/NavigationCancelEvent.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/NavigationCancelEvent.kt @@ -1,6 +1,7 @@ package com.mapbox.navigation.core.telemetry.events import android.annotation.SuppressLint +import com.mapbox.bindgen.Value import com.mapbox.navigation.base.metrics.NavigationMetrics @SuppressLint("ParcelCreator") @@ -16,4 +17,19 @@ internal class NavigationCancelEvent( var comment: String = "" override fun getEventName(): String = NavigationMetrics.CANCEL_SESSION + + override fun toValue(): Value { + val value = super.toValue() + + val fields = hashMapOf() + value.contents?.let { it as HashMap + fields.putAll(it) + } + + arrivalTimestamp?.let { fields["arrivalTimestamp"] = it.toValue() } + fields["rating"] = rating.toValue() + fields["comment"] = comment.toValue() + + return value + } } diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/NavigationEvent.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/NavigationEvent.kt index 506b70cf472..bd00d56876b 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/NavigationEvent.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/NavigationEvent.kt @@ -2,9 +2,11 @@ package com.mapbox.navigation.core.telemetry.events import android.os.Build import android.os.Parcel +import androidx.annotation.CallSuper import com.google.gson.Gson import com.mapbox.android.telemetry.Event import com.mapbox.android.telemetry.TelemetryUtils +import com.mapbox.bindgen.Value import com.mapbox.navigation.base.metrics.MetricEvent import com.mapbox.navigation.core.BuildConfig @@ -93,4 +95,63 @@ internal abstract class NavigationEvent( override val metricName: String get() = getEventName() + + @CallSuper + override fun toValue(): Value { + val fields = hashMapOf() + + fields["version"] = version.toValue() + fields["operatingSystem"] = operatingSystem.toValue() + device?.let { fields["device"] = it.toValue() } + fields["sdkVersion"] = sdkVersion.toValue() + fields["created"] = created.toValue() + fields["volumeLevel"] = volumeLevel.toValue() + fields["batteryLevel"] = batteryLevel.toValue() + fields["screenBrightness"] = screenBrightness.toValue() + fields["batteryPluggedIn"] = batteryPluggedIn.toValue() + fields["connectivity"] = connectivity.toValue() + fields["audioType"] = audioType.toValue() + fields["applicationState"] = applicationState.toValue() + fields["event"] = event.toValue() + + sdkIdentifier?.let { fields["sdkIdentifier"] = it.toValue() } + + navigatorSessionIdentifier?.let { fields["navigatorSessionIdentifier"] = it.toValue() } + startTimestamp?.let { fields["startTimestamp"] = it.toValue() } + driverMode?.let { fields["driverMode"] = it.toValue() } + sessionIdentifier?.let { fields["sessionIdentifier"] = it.toValue() } + geometry?.let { fields["geometry"] = it.toValue() } + profile?.let { fields["profile"] = it.toValue() } + originalRequestIdentifier?.let { fields["originalRequestIdentifier"] = it.toValue() } + requestIdentifier?.let { fields["requestIdentifier"] = it.toValue() } + originalGeometry?.let { fields["originalGeometry"] = it.toValue() } + locationEngine?.let { fields["locationEngine"] = it.toValue() } + tripIdentifier?.let { fields["tripIdentifier"] = it.toValue() } + fields["lat"] = lat.toValue() + fields["lng"] = lng.toValue() + fields["simulation"] = simulation.toValue() + fields["absoluteDistanceToDestination"] = absoluteDistanceToDestination.toValue() + fields["percentTimeInPortrait"] = percentTimeInPortrait.toValue() + fields["percentTimeInForeground"] = percentTimeInForeground.toValue() + fields["distanceCompleted"] = distanceCompleted.toValue() + fields["distanceRemaining"] = distanceRemaining.toValue() + fields["durationRemaining"] = durationRemaining.toValue() + fields["eventVersion"] = eventVersion.toValue() + fields["estimatedDistance"] = estimatedDistance.toValue() + fields["estimatedDuration"] = estimatedDuration.toValue() + fields["rerouteCount"] = rerouteCount.toValue() + fields["originalEstimatedDistance"] = originalEstimatedDistance.toValue() + fields["originalEstimatedDuration"] = originalEstimatedDuration.toValue() + fields["stepCount"] = stepCount.toValue() + fields["originalStepCount"] = originalStepCount.toValue() + fields["legIndex"] = legIndex.toValue() + fields["legCount"] = legCount.toValue() + fields["stepIndex"] = stepIndex.toValue() + fields["voiceIndex"] = voiceIndex.toValue() + fields["bannerIndex"] = bannerIndex.toValue() + fields["totalStepCount"] = totalStepCount.toValue() + appMetadata?.let { fields["appMetadata"] = it.toValue() } + + return Value.valueOf(fields) + } } diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/NavigationFeedbackEvent.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/NavigationFeedbackEvent.kt index 1d77bab7617..c48b75c4cac 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/NavigationFeedbackEvent.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/NavigationFeedbackEvent.kt @@ -1,6 +1,7 @@ package com.mapbox.navigation.core.telemetry.events import android.annotation.SuppressLint +import com.mapbox.bindgen.Value import com.mapbox.navigation.base.metrics.NavigationMetrics @SuppressLint("ParcelCreator") @@ -24,4 +25,26 @@ internal class NavigationFeedbackEvent( var feedbackSubType: Array? = emptyArray() override fun getEventName(): String = NavigationMetrics.FEEDBACK + + override fun toValue(): Value { + val value = super.toValue() + + val fields = hashMapOf() + value.contents?.let { it as HashMap + fields.putAll(it) + } + + fields["userId"] = userId.toValue() + fields["feedbackId"] = feedbackId.toValue() + fields["step"] = step.toValue() + feedbackType?.let { fields["feedbackType"] = it.toValue() } + source?.let { fields["source"] = it.toValue() } + description?.let { fields["description"] = it.toValue() } + locationsBefore?.let { fields["locationsBefore"] = it.toValue() } + locationsAfter?.let { fields["locationsAfter"] = it.toValue() } + screenshot?.let { fields["screenshot"] = it.toValue() } + feedbackSubType?.let { fields["feedbackSubType"] = it.toValue() } + + return value + } } diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/NavigationFreeDriveEvent.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/NavigationFreeDriveEvent.kt index bbbf12d331a..3e58717832b 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/NavigationFreeDriveEvent.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/NavigationFreeDriveEvent.kt @@ -5,6 +5,7 @@ import android.os.Parcel import com.google.gson.Gson import com.mapbox.android.telemetry.Event import com.mapbox.android.telemetry.TelemetryUtils +import com.mapbox.bindgen.Value import com.mapbox.navigation.base.metrics.MetricEvent import com.mapbox.navigation.base.metrics.NavigationMetrics @@ -55,6 +56,34 @@ internal class NavigationFreeDriveEvent( override fun toJson(gson: Gson): String = gson.toJson(this) + override fun toValue(): Value { + val fields = hashMapOf() + + fields["version"] = version.toValue() + fields["created"] = created.toValue() + fields["volumeLevel"] = volumeLevel.toValue() + fields["batteryLevel"] = batteryLevel.toValue() + fields["screenBrightness"] = screenBrightness.toValue() + fields["batteryPluggedIn"] = batteryPluggedIn.toValue() + fields["connectivity"] = connectivity.toValue() + fields["audioType"] = audioType.toValue() + fields["applicationState"] = applicationState.toValue() + fields["event"] = event.toValue() + fields["eventVersion"] = eventVersion.toValue() + locationEngine?.let { fields["locationEngine"] = it.toValue() } + fields["percentTimeInPortrait"] = percentTimeInPortrait.toValue() + fields["percentTimeInForeground"] = percentTimeInForeground.toValue() + fields["simulation"] = simulation.toValue() + navigatorSessionIdentifier?.let { fields["navigatorSessionIdentifier"] = it.toValue() } + startTimestamp?.let { fields["startTimestamp"] = it.toValue() } + sessionIdentifier?.let { fields["sessionIdentifier"] = it.toValue() } + location?.let { fields["location"] = it.toValue() } + eventType?.let { fields["eventType"] = it.toValue() } + appMetadata?.let { fields["appMetadata"] = it.toValue() } + + return Value.valueOf(fields) + } + override fun describeContents(): Int { return 0 } diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/NavigationRerouteEvent.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/NavigationRerouteEvent.kt index 2d5666c59e4..e0a10d86886 100644 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/NavigationRerouteEvent.kt +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/NavigationRerouteEvent.kt @@ -1,6 +1,7 @@ package com.mapbox.navigation.core.telemetry.events import android.annotation.SuppressLint +import com.mapbox.bindgen.Value import com.mapbox.navigation.base.metrics.NavigationMetrics @SuppressLint("ParcelCreator") @@ -23,4 +24,25 @@ internal class NavigationRerouteEvent( var screenshot: String? = null override fun getEventName(): String = NavigationMetrics.REROUTE + + override fun toValue(): Value { + val value = super.toValue() + + val fields = hashMapOf() + value.contents?.let { it as HashMap + fields.putAll(it) + } + + fields["newDistanceRemaining"] = newDistanceRemaining.toValue() + fields["newDurationRemaining"] = newDurationRemaining.toValue() + fields["feedbackId"] = feedbackId.toValue() + newGeometry?.let { fields["newGeometry"] = it.toValue() } + fields["step"] = step.toValue() + fields["secondsSinceLastReroute"] = secondsSinceLastReroute.toValue() + locationsBefore?.let { fields["locationsBefore"] = it.toValue() } + locationsAfter?.let { fields["locationsAfter"] = it.toValue() } + screenshot?.let { fields["screenshot"] = it.toValue() } + + return value + } } diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/MapboxNavigationTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/MapboxNavigationTest.kt index 34137cfd095..e7b9b8689f7 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/MapboxNavigationTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/MapboxNavigationTest.kt @@ -180,7 +180,7 @@ class MapboxNavigationTest { } returns routeRefreshController mockkObject(RouteAlternativesControllerProvider) every { - RouteAlternativesControllerProvider.create(any(), any(), any()) + RouteAlternativesControllerProvider.create(any(), any(), any(), any()) } returns routeAlternativesController every { applicationContext.applicationContext } returns applicationContext diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesControllerTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesControllerTest.kt index f55bb2823a4..89cee224cb3 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesControllerTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routealternatives/RouteAlternativesControllerTest.kt @@ -8,6 +8,7 @@ import com.mapbox.navigation.core.trip.session.TripSession import com.mapbox.navigation.navigator.internal.MapboxNativeNavigator import com.mapbox.navigation.testing.FileUtils import com.mapbox.navigation.testing.MainCoroutineRule +import com.mapbox.navigation.utils.internal.ThreadController import com.mapbox.navigator.RouteAlternativesControllerInterface import io.mockk.every import io.mockk.just @@ -38,6 +39,7 @@ class RouteAlternativesControllerTest { options, navigator, tripSession, + ThreadController(), ) @Test @@ -102,7 +104,7 @@ class RouteAlternativesControllerTest { } @Test - fun `should broadcast alternative routes changes from nav-native`() { + fun `should broadcast alternative routes changes from nav-native`() = coroutineRule.runBlockingTest { val routeAlternativesController = routeAlternativesController() val nativeObserver = slot() every { controllerInterface.addObserver(capture(nativeObserver)) } just runs @@ -130,7 +132,7 @@ class RouteAlternativesControllerTest { } @Test - fun `should broadcast current route with alternative`() { + fun `should broadcast current route with alternative`() = coroutineRule.runBlockingTest { val routeAlternativesController = routeAlternativesController() val nativeObserver = slot() every { controllerInterface.addObserver(capture(nativeObserver)) } just runs @@ -169,7 +171,7 @@ class RouteAlternativesControllerTest { } @Test - fun `should set alternative RouteOptions to primary RouteOptions`() { + fun `should set alternative RouteOptions to primary RouteOptions`() = coroutineRule.runBlockingTest { val originalCoordinates = "-122.270375,37.801429;-122.271496, 37.799063" val routeAlternativesController = routeAlternativesController() val nativeObserver = slot() diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/RouteOptionsUpdaterTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/RouteOptionsUpdaterTest.kt index 350785fbd1f..b0d34cac7f1 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/RouteOptionsUpdaterTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/routeoptions/RouteOptionsUpdaterTest.kt @@ -196,6 +196,42 @@ class RouteOptionsUpdaterTest { MapboxRouteOptionsUpdateCommonTest.checkImmutableFields(routeOptions, newRouteOptions) } + @Test + fun `new options return without layer for profiles other than driving`() { + listOf( + Pair(DirectionsCriteria.PROFILE_CYCLING, false), + Pair(DirectionsCriteria.PROFILE_DRIVING, true), + Pair(DirectionsCriteria.PROFILE_DRIVING_TRAFFIC, true), + Pair(DirectionsCriteria.PROFILE_WALKING, false), + ).forEach { (profile, result) -> + val routeOptions = provideRouteOptionsWithCoordinates() + .toBuilder() + .profile(profile) + .build() + val routeProgress: RouteProgress = mockk(relaxed = true) { + every { remainingWaypoints } returns 2 + } + + val newRouteOptions = + routeRefreshAdapter.update(routeOptions, routeProgress, locationMatcherResult) + .let { + assertTrue(it is RouteOptionsUpdater.RouteOptionsResult.Success) + return@let it as RouteOptionsUpdater.RouteOptionsResult.Success + } + .routeOptions + + val expectedLayers = if (result) { + listOf(DEFAULT_Z_LEVEL, null, null) + } else { + null + } + val actualLayers = newRouteOptions.layersList() + + assertEquals("for $profile", expectedLayers, actualLayers) + MapboxRouteOptionsUpdateCommonTest.checkImmutableFields(routeOptions, newRouteOptions) + } + } + @Test fun `new options return with current layer and previous layers`() { val routeOptions = provideRouteOptionsWithCoordinatesAndLayers() diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/telemetry/MapboxNavigationTelemetryTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/telemetry/MapboxNavigationTelemetryTest.kt index 0cd56ad3786..7e05a6a41ed 100644 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/telemetry/MapboxNavigationTelemetryTest.kt +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/telemetry/MapboxNavigationTelemetryTest.kt @@ -21,6 +21,7 @@ import com.mapbox.navigation.base.metrics.MetricEvent import com.mapbox.navigation.base.options.NavigationOptions import com.mapbox.navigation.base.trip.model.RouteLegProgress import com.mapbox.navigation.base.trip.model.RouteProgress +import com.mapbox.navigation.base.trip.model.RouteProgressState.OFF_ROUTE import com.mapbox.navigation.base.trip.model.RouteProgressState.TRACKING import com.mapbox.navigation.base.trip.model.RouteStepProgress import com.mapbox.navigation.core.MapboxNavigation @@ -708,6 +709,33 @@ class MapboxNavigationTelemetryTest { checkEventsInSameSession(events) } + @Test + fun rerouteEvent_accumulates_distance_traveled_on_offRoute() { + baseMock() + mockAnotherRoute() + mockRouteProgress() + + baseInitialization() + updateRoute(anotherRoute, RoutesExtra.ROUTES_UPDATE_REASON_REROUTE) + locationsCollector.flushBuffers() + every { routeProgress.currentState } returns OFF_ROUTE + updateRouteProgress(count = 1) + every { routeProgress.currentState } returns TRACKING + updateRouteProgress(count = 1) + updateRoute(anotherRoute, RoutesExtra.ROUTES_UPDATE_REASON_REROUTE) + locationsCollector.flushBuffers() + + val events = captureAndVerifyMetricsReporter(exactly = 4) + assertTrue(events[0] is NavigationAppUserTurnstileEvent) + assertTrue(events[1] is NavigationDepartEvent) + assertTrue(events[2] is NavigationRerouteEvent) + assertTrue(events[3] is NavigationRerouteEvent) + assertEquals( + (ROUTE_PROGRESS_DISTANCE_TRAVELED * 2).toInt(), + (events[3] as NavigationRerouteEvent).distanceCompleted + ) + } + @Test fun departEvent_populated_correctly() { baseMock() @@ -717,6 +745,7 @@ class MapboxNavigationTelemetryTest { val departEvent = events[1] as NavigationDepartEvent checkOriginalParams(departEvent, originalRoute) + assertEquals(0, departEvent.distanceCompleted) } @Test @@ -733,6 +762,7 @@ class MapboxNavigationTelemetryTest { val rerouteEvent = events[2] as NavigationRerouteEvent checkOriginalParams(rerouteEvent, anotherRoute) + assertEquals(routeProgress.distanceTraveled.toInt(), rerouteEvent.distanceCompleted) } @Test @@ -1449,7 +1479,6 @@ class MapboxNavigationTelemetryTest { ) assertEquals(routeProgress.distanceRemaining.toInt(), event.distanceRemaining) assertEquals(routeProgress.durationRemaining.toInt(), event.durationRemaining) - assertEquals(routeProgress.distanceTraveled.toInt(), event.distanceCompleted) assertEquals(currentRoute.geometry(), event.geometry) assertEquals(currentRoute.routeOptions()?.profile(), event.profile) assertEquals(currentRoute.routeIndex()?.toInt(), event.legIndex) diff --git a/libnavigation-metrics/src/main/java/com/mapbox/navigation/metrics/MapboxMetricsReporter.kt b/libnavigation-metrics/src/main/java/com/mapbox/navigation/metrics/MapboxMetricsReporter.kt index ffb6d59cb36..aabc747e927 100644 --- a/libnavigation-metrics/src/main/java/com/mapbox/navigation/metrics/MapboxMetricsReporter.kt +++ b/libnavigation-metrics/src/main/java/com/mapbox/navigation/metrics/MapboxMetricsReporter.kt @@ -3,6 +3,15 @@ package com.mapbox.navigation.metrics import android.content.Context import com.google.gson.Gson import com.mapbox.android.telemetry.MapboxTelemetry +import com.mapbox.bindgen.Value +import com.mapbox.common.Event +import com.mapbox.common.EventPriority +import com.mapbox.common.EventsService +import com.mapbox.common.EventsServiceInterface +import com.mapbox.common.EventsServiceError +import com.mapbox.common.EventsServiceObserver +import com.mapbox.common.EventsServiceOptions +import com.mapbox.common.Logger import com.mapbox.navigation.base.metrics.MetricEvent import com.mapbox.navigation.base.metrics.MetricsObserver import com.mapbox.navigation.base.metrics.MetricsReporter @@ -15,14 +24,26 @@ import kotlinx.coroutines.launch * Default implementation of [MetricsReporter] interface. */ object MapboxMetricsReporter : MetricsReporter { + private const val TAG = "MBMetricsReporter" private val gson = Gson() private lateinit var mapboxTelemetry: MapboxTelemetry + private lateinit var eventsService: EventsServiceInterface @Volatile private var metricsObserver: MetricsObserver? = null private var ioJobController = InternalJobControlFactory.createIOScopeJobControl() + private val eventsServiceObserver by lazy { + object : EventsServiceObserver { + override fun didEncounterError(error: EventsServiceError, events: Value) { + Logger.e(TAG, "EventsService failure: $error") + } + + override fun didSendEvents(events: Value) {} + } + } + /** * Initialize [mapboxTelemetry] that need to send event to Mapbox Telemetry server. * @@ -38,14 +59,28 @@ object MapboxMetricsReporter : MetricsReporter { ) { mapboxTelemetry = MapboxTelemetry(context, accessToken, userAgent) mapboxTelemetry.enable() + val eventsServiceOptions = EventsServiceOptions(accessToken, userAgent, null) + eventsService = EventsService(eventsServiceOptions.overrideIfNeeded(context)) + + if (!EventsService.getEventsCollectionState()) { + EventsService.setEventsCollectionState(true) { error -> + error?.let { + Logger.e(TAG, "Failed to turn on events collection: $error") + } ?: run { + eventsService.registerObserver(eventsServiceObserver) + } + } + } } // For test purposes only internal fun init( mapboxTelemetry: MapboxTelemetry, - jobControlFactory: InternalJobControlFactory, + eventsService: EventsServiceInterface, + jobControlFactory: InternalJobControlFactory ) { this.mapboxTelemetry = mapboxTelemetry + this.eventsService = eventsService ioJobController = jobControlFactory.createIOScopeJobControl() mapboxTelemetry.enable() } @@ -69,6 +104,12 @@ object MapboxMetricsReporter : MetricsReporter { fun disable() { removeObserver() mapboxTelemetry.disable() + eventsService.unregisterObserver(eventsServiceObserver) + EventsService.setEventsCollectionState(false) { error -> + error?.let { + Logger.e(TAG, "Failed to turn off events collection: $error") + } + } ioJobController.job.cancelChildren() } @@ -80,6 +121,12 @@ object MapboxMetricsReporter : MetricsReporter { mapboxTelemetry.push(it) } + eventsService.sendEvent(Event(EventPriority.IMMEDIATE, metricEvent.toValue())) { error -> + error?.let { + Logger.e(TAG, "Failed to send event: $error") + } + } + ioJobController.scope.launch { metricsObserver?.onMetricUpdated(metricEvent.metricName, metricEvent.toJson(gson)) } @@ -99,3 +146,18 @@ object MapboxMetricsReporter : MetricsReporter { this.metricsObserver = null } } + +/** + * Read url and token from resources if they are available + */ +fun EventsServiceOptions.overrideIfNeeded(context: Context): EventsServiceOptions { + val endpointId = context.resources.getIdentifier("mapbox_events_url", "string", context.packageName) + val tokenId = context.resources.getIdentifier("mapbox_events_access_token", "string", context.packageName) + return if (endpointId != 0 || tokenId !=0) { + EventsServiceOptions( + if (tokenId!=0) context.getString(tokenId) else token, + userAgentFragment, + if (endpointId != 0) context.getString(endpointId) else baseURL + ) + } else this +} diff --git a/libnavigation-metrics/src/main/java/com/mapbox/navigation/metrics/internal/event/NavigationAppUserTurnstileEvent.kt b/libnavigation-metrics/src/main/java/com/mapbox/navigation/metrics/internal/event/NavigationAppUserTurnstileEvent.kt index 821fcd5057a..4e971d069d6 100644 --- a/libnavigation-metrics/src/main/java/com/mapbox/navigation/metrics/internal/event/NavigationAppUserTurnstileEvent.kt +++ b/libnavigation-metrics/src/main/java/com/mapbox/navigation/metrics/internal/event/NavigationAppUserTurnstileEvent.kt @@ -2,8 +2,10 @@ package com.mapbox.navigation.metrics.internal.event import com.google.gson.Gson import com.mapbox.android.telemetry.AppUserTurnstile +import com.mapbox.bindgen.Value import com.mapbox.navigation.base.metrics.MetricEvent import com.mapbox.navigation.base.metrics.NavigationMetrics +import org.json.JSONObject class NavigationAppUserTurnstileEvent( val event: AppUserTurnstile @@ -13,4 +15,10 @@ class NavigationAppUserTurnstileEvent( get() = NavigationMetrics.APP_USER_TURNSTILE override fun toJson(gson: Gson): String = gson.toJson(this) + + override fun toValue(): Value { + val jsonObject = JSONObject(toJson(Gson())) + val conversionResult = Value.fromJson(jsonObject.getJSONObject("event").toString()) + return conversionResult.value ?: Value.nullValue() + } } diff --git a/libnavigation-metrics/src/test/java/com/mapbox/navigation/metrics/MapboxMetricsReporterTest.kt b/libnavigation-metrics/src/test/java/com/mapbox/navigation/metrics/MapboxMetricsReporterTest.kt index 57f56b6c0fd..300499f1161 100644 --- a/libnavigation-metrics/src/test/java/com/mapbox/navigation/metrics/MapboxMetricsReporterTest.kt +++ b/libnavigation-metrics/src/test/java/com/mapbox/navigation/metrics/MapboxMetricsReporterTest.kt @@ -4,35 +4,77 @@ import android.os.Parcel import com.google.gson.Gson import com.mapbox.android.telemetry.Event import com.mapbox.android.telemetry.MapboxTelemetry +import com.mapbox.bindgen.Value +import com.mapbox.common.* import com.mapbox.navigation.base.metrics.MetricEvent import com.mapbox.navigation.base.metrics.NavigationMetrics import com.mapbox.navigation.metrics.extensions.toTelemetryEvent import com.mapbox.navigation.testing.MainCoroutineRule import com.mapbox.navigation.utils.internal.InternalJobControlFactory import com.mapbox.navigation.utils.internal.JobControl -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.unmockkObject -import io.mockk.verify +import io.mockk.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.SupervisorJob import org.junit.Rule import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.annotation.Implementation +import org.robolectric.annotation.Implements + + +@Implements(EventsService::class) +object ShadowEventsService { + @Implementation + open fun setEventsCollectionState( + enableCollection: Boolean, + callback: EventsServiceResponseCallback? + ) {} + + + @Implementation + open fun getEventsCollectionState(): Boolean { + return true + } +} @ExperimentalCoroutinesApi +@Config(shadows = [ShadowEventsService::class]) +@RunWith(RobolectricTestRunner::class) class MapboxMetricsReporterTest { @get:Rule var coroutineRule = MainCoroutineRule() + private var eventsServiceMock = object : EventsServiceInterface { + override fun registerObserver(observer: EventsServiceObserver) {} + + override fun unregisterObserver(observer: EventsServiceObserver) {} + + override fun sendTurnstileEvent( + turnstileEvent: TurnstileEvent, + callback: EventsServiceResponseCallback? + ) {} + + override fun sendEvent( + event: com.mapbox.common.Event, + callback: EventsServiceResponseCallback? + ) {} + + override fun pauseEventsCollection() {} + + override fun resumeEventsCollection() {} + + } + @Test fun telemetryEnabledWhenReporterInit() { val mapboxTelemetry = mockk(relaxed = true) + val eventsService: EventsServiceInterface = mockk() - MapboxMetricsReporter.init(mapboxTelemetry, InternalJobControlFactory) - + MapboxMetricsReporter.init(mapboxTelemetry, eventsServiceMock, InternalJobControlFactory) verify { mapboxTelemetry.enable() } } @@ -93,8 +135,12 @@ class MapboxMetricsReporterTest { private fun initMetricsReporterWithTelemetry(): MapboxTelemetry { val mapboxTelemetry = mockk(relaxed = true) - MapboxMetricsReporter.init(mapboxTelemetry, InternalJobControlFactory) + val eventsService: EventsServiceInterface = mockk() + mockkStatic(EventsService::class) + every { EventsService.setEventsCollectionState(any(), any()) } returns + + MapboxMetricsReporter.init(mapboxTelemetry, eventsServiceMock, InternalJobControlFactory) return mapboxTelemetry } @@ -115,5 +161,7 @@ class MapboxMetricsReporterTest { override fun describeContents(): Int = 0 override fun toJson(gson: Gson): String = gson.toJson(this) + + override fun toValue(): Value = Value.nullValue() } } diff --git a/libtrip-notification/src/main/java/com/mapbox/navigation/trip/notification/MapboxTripNotificationView.kt b/libtrip-notification/src/main/java/com/mapbox/navigation/trip/notification/MapboxTripNotificationView.kt index 02de0f63e00..291760e9373 100644 --- a/libtrip-notification/src/main/java/com/mapbox/navigation/trip/notification/MapboxTripNotificationView.kt +++ b/libtrip-notification/src/main/java/com/mapbox/navigation/trip/notification/MapboxTripNotificationView.kt @@ -66,7 +66,7 @@ internal class MapboxTripNotificationView( */ fun setEndNavigationButtonText(textResource: Int) { expandedView?.setTextViewText( - R.id.endNavigationBtnText, + R.id.endNavigationBtn, context.getString(textResource) ) } @@ -145,7 +145,7 @@ internal class MapboxTripNotificationView( setTextViewText(R.id.notificationDistanceText, "") setTextViewText(R.id.notificationArrivalText, "") setTextViewText(R.id.notificationInstructionText, "") - setTextViewText(R.id.endNavigationBtnText, "") + setTextViewText(R.id.endNavigationBtn, "") setViewVisibility(R.id.etaContent, View.GONE) setViewVisibility(R.id.notificationInstructionText, View.GONE) setViewVisibility(R.id.freeDriveText, View.GONE) diff --git a/libtrip-notification/src/main/java/com/mapbox/navigation/trip/notification/internal/MapboxTripNotification.kt b/libtrip-notification/src/main/java/com/mapbox/navigation/trip/notification/internal/MapboxTripNotification.kt index 77ff10d0487..9fa89250bcb 100644 --- a/libtrip-notification/src/main/java/com/mapbox/navigation/trip/notification/internal/MapboxTripNotification.kt +++ b/libtrip-notification/src/main/java/com/mapbox/navigation/trip/notification/internal/MapboxTripNotification.kt @@ -13,11 +13,13 @@ import android.graphics.Canvas import android.graphics.Matrix import android.graphics.drawable.Drawable import android.os.Build +import android.text.SpannableString import android.text.TextUtils import android.text.format.DateFormat import android.view.View.GONE import android.view.View.VISIBLE import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat import com.mapbox.annotation.module.MapboxModule import com.mapbox.annotation.module.MapboxModuleType import com.mapbox.api.directions.v5.models.BannerInstructions @@ -39,7 +41,6 @@ import com.mapbox.navigation.utils.internal.NAVIGATION_NOTIFICATION_CHANNEL import com.mapbox.navigation.utils.internal.NOTIFICATION_CHANNEL import com.mapbox.navigation.utils.internal.NOTIFICATION_ID import com.mapbox.navigation.utils.internal.ifChannelException -import com.mapbox.navigation.utils.internal.ifNonNull import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ClosedReceiveChannelException import kotlinx.coroutines.channels.ClosedSendChannelException @@ -74,9 +75,12 @@ class MapboxTripNotification constructor( var currentManeuverModifier: String? = null private set private var currentRoundaboutAngle: Float? = null + private var currentManeuverImage: Bitmap? = null private var currentInstructionText: String? = null private var currentDistanceText: Double? = null + private var currentFormattedDistance: SpannableString? = null + private var currentFormattedTime: String? = null private var pendingOpenIntent: PendingIntent? = null private var pendingCloseIntent: PendingIntent? = null private val etaFormat: String = applicationContext.getString(R.string.mapbox_eta_format) @@ -205,6 +209,11 @@ class MapboxTripNotification constructor( .setCustomBigContentView(notificationView.expandedView) .setOngoing(true) + if (Build.VERSION.SDK_INT >= 31) { + val color = ContextCompat.getColor(applicationContext, R.color.mapbox_notification_blue) + builder.setColor(color).setColorized(true) + } + pendingOpenIntent?.let { pendingOpenIntent -> builder.setContentIntent(pendingOpenIntent) } @@ -253,21 +262,10 @@ class MapboxTripNotification constructor( when (state) { is TripNotificationState.TripNotificationFreeState -> setFreeDriveMode(true) is TripNotificationState.TripNotificationData -> { - if (isDistanceTextChanged(state.distanceRemaining)) { - updateDistanceText(state.distanceRemaining) - } - generateArrivalTime(state.durationRemaining)?.let { formattedTime -> - updateViewsWithArrival(formattedTime) - } - state.bannerInstructions?.let { bannerInstructions -> - val primary = bannerInstructions.primary().text() - if (isInstructionTextChanged(primary)) { - updateInstructionText(primary) - } - if (isManeuverStateChanged(bannerInstructions)) { - updateManeuverImage(state.drivingSide ?: ManeuverModifier.RIGHT) - } - } + updateDistanceText(state.distanceRemaining) + updateViewsWithArrival(state.durationRemaining) + updateInstructionText(state.bannerInstructions) + updateManeuverImage(state.bannerInstructions, state.drivingSide) setFreeDriveMode(false) } } @@ -291,11 +289,11 @@ class MapboxTripNotification constructor( } private fun updateDistanceText(distanceRemaining: Double?) { - val formattedDistance = distanceRemaining?.let { - distanceFormatter.formatDistance(distanceRemaining) - } ?: return - currentDistanceText = distanceRemaining - notificationView.updateDistanceText(formattedDistance) + if (isDistanceTextChanged(distanceRemaining) && distanceRemaining != null) { + currentDistanceText = distanceRemaining + currentFormattedDistance = distanceFormatter.formatDistance(distanceRemaining) + } + currentFormattedDistance?.let { notificationView.updateDistanceText(it) } } private fun generateArrivalTime( @@ -314,17 +312,20 @@ class MapboxTripNotification constructor( } } - private fun updateViewsWithArrival(time: String) { - notificationView.updateArrivalTime(time) + private fun updateViewsWithArrival(durationRemaining: Double?) { + generateArrivalTime(durationRemaining)?.let { currentFormattedTime = it } + currentFormattedTime?.let { notificationView.updateArrivalTime(it) } } private fun isInstructionTextChanged(primaryText: String): Boolean { return currentInstructionText.isNullOrEmpty() || currentInstructionText != primaryText } - private fun updateInstructionText(primaryText: String) { - notificationView.updateInstructionText(primaryText) - currentInstructionText = primaryText + private fun updateInstructionText(bannerInstructions: BannerInstructions?) { + bannerInstructions?.primary()?.text() + ?.takeIf { isInstructionTextChanged(it) } + ?.let { currentInstructionText = it } + currentInstructionText?.let { notificationView.updateInstructionText(it) } } private fun isManeuverStateChanged(bannerInstruction: BannerInstructions): Boolean { @@ -341,23 +342,21 @@ class MapboxTripNotification constructor( currentRoundaboutAngle != previousRoundaboutAngle } - private fun updateManeuverImage(drivingSide: String) { - val notificationTurnIcon = turnIconHelper.retrieveTurnIcon( - currentManeuverType, - currentRoundaboutAngle, - currentManeuverModifier, - drivingSide - ) - ifNonNull(notificationTurnIcon) { turnIcon -> - ifNonNull(turnIcon.icon) { image -> - val originalDrawable = notificationView.getImageDrawable(image) - ifNonNull(originalDrawable) { drawable -> - ifNonNull(getManeuverBitmap(drawable, turnIcon.shouldFlipIcon)) { bitmap -> - notificationView.updateImage(bitmap) - } - } + private fun updateManeuverImage(bannerInstructions: BannerInstructions?, drivingSide: String?) { + if (bannerInstructions != null && isManeuverStateChanged(bannerInstructions)) { + turnIconHelper.retrieveTurnIcon( + currentManeuverType, + currentRoundaboutAngle, + currentManeuverModifier, + drivingSide = drivingSide ?: ManeuverModifier.RIGHT, + )?.let { turnIcon -> + turnIcon.icon + ?.let { notificationView.getImageDrawable(it) } + ?.let { getManeuverBitmap(it, turnIcon.shouldFlipIcon) } + ?.let { currentManeuverImage = it } } } + currentManeuverImage?.let { notificationView.updateImage(it) } } private fun getManeuverBitmap(drawable: Drawable, shouldFlipIcon: Boolean): Bitmap? { diff --git a/libtrip-notification/src/main/res/layout/mapbox_notification_navigation_collapsed.xml b/libtrip-notification/src/main/res/layout/mapbox_notification_navigation_collapsed.xml index ede98fed391..9c58ceebbf2 100644 --- a/libtrip-notification/src/main/res/layout/mapbox_notification_navigation_collapsed.xml +++ b/libtrip-notification/src/main/res/layout/mapbox_notification_navigation_collapsed.xml @@ -3,8 +3,9 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/navigationCollapsedNotificationLayout" android:layout_width="match_parent" - android:layout_height="64dp" - android:background="@color/mapbox_notification_blue"> + android:layout_height="wrap_content" + android:background="@color/mapbox_notification_blue" + android:padding="@dimen/mapbox_notification_padding"> diff --git a/libtrip-notification/src/main/res/layout/mapbox_notification_navigation_expanded.xml b/libtrip-notification/src/main/res/layout/mapbox_notification_navigation_expanded.xml index 485806f7a8b..4105a44d6a2 100644 --- a/libtrip-notification/src/main/res/layout/mapbox_notification_navigation_expanded.xml +++ b/libtrip-notification/src/main/res/layout/mapbox_notification_navigation_expanded.xml @@ -11,33 +11,21 @@ - - - - - - - + android:paddingBottom="@dimen/mapbox_notification_padding" + android:paddingEnd="@dimen/mapbox_notification_padding" + android:paddingStart="@dimen/mapbox_notification_padding" + android:drawablePadding="8dp" + android:drawableStart="@drawable/mapbox_ic_close" + android:textAllCaps="true" + android:textColor="@android:color/white" + tools:text="@string/mapbox_end_navigation" /> diff --git a/libtrip-notification/src/main/res/values-v31/dimens.xml b/libtrip-notification/src/main/res/values-v31/dimens.xml new file mode 100644 index 00000000000..77d5ad95159 --- /dev/null +++ b/libtrip-notification/src/main/res/values-v31/dimens.xml @@ -0,0 +1,4 @@ + + + 0dp + \ No newline at end of file diff --git a/libtrip-notification/src/main/res/values/dimens.xml b/libtrip-notification/src/main/res/values/dimens.xml index e972ea2a250..fb311bf50d3 100644 --- a/libtrip-notification/src/main/res/values/dimens.xml +++ b/libtrip-notification/src/main/res/values/dimens.xml @@ -2,4 +2,5 @@ 48dp 48dp + 8dp \ No newline at end of file diff --git a/libtrip-notification/src/test/java/com/mapbox/navigation/trip/notification/MapboxTripNotificationViewTest.kt b/libtrip-notification/src/test/java/com/mapbox/navigation/trip/notification/MapboxTripNotificationViewTest.kt index 73461385a9e..4eebcd20854 100644 --- a/libtrip-notification/src/test/java/com/mapbox/navigation/trip/notification/MapboxTripNotificationViewTest.kt +++ b/libtrip-notification/src/test/java/com/mapbox/navigation/trip/notification/MapboxTripNotificationViewTest.kt @@ -15,7 +15,7 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkObject import io.mockk.verify -import junit.framework.Assert.assertNotNull +import org.junit.Assert.assertNotNull import org.junit.Before import org.junit.Test import java.util.Locale @@ -86,7 +86,7 @@ class MapboxTripNotificationViewTest { it.setEndNavigationButtonText(R.string.mapbox_stop_session) } - verify { view.expandedView!!.setTextViewText(R.id.endNavigationBtnText, STOP_SESSION) } + verify { view.expandedView!!.setTextViewText(R.id.endNavigationBtn, STOP_SESSION) } } @Test @@ -206,7 +206,7 @@ class MapboxTripNotificationViewTest { R.drawable.mapbox_ic_navigation ) } - verify { view.expandedView!!.setTextViewText(R.id.endNavigationBtnText, STOP_SESSION) } + verify { view.expandedView!!.setTextViewText(R.id.endNavigationBtn, STOP_SESSION) } } @Test @@ -228,7 +228,7 @@ class MapboxTripNotificationViewTest { verify { view.expandedView!!.setViewVisibility(R.id.notificationInstructionText, View.VISIBLE) } - verify { view.expandedView!!.setTextViewText(R.id.endNavigationBtnText, END_NAVIGATION) } + verify { view.expandedView!!.setTextViewText(R.id.endNavigationBtn, END_NAVIGATION) } } private fun createContext(): Context { diff --git a/libtrip-notification/src/test/java/com/mapbox/navigation/trip/notification/internal/MapboxTripNotificationTest.kt b/libtrip-notification/src/test/java/com/mapbox/navigation/trip/notification/internal/MapboxTripNotificationTest.kt index 20b83317359..fce08007690 100644 --- a/libtrip-notification/src/test/java/com/mapbox/navigation/trip/notification/internal/MapboxTripNotificationTest.kt +++ b/libtrip-notification/src/test/java/com/mapbox/navigation/trip/notification/internal/MapboxTripNotificationTest.kt @@ -16,7 +16,6 @@ import android.widget.RemoteViews import com.mapbox.api.directions.v5.models.BannerInstructions import com.mapbox.api.directions.v5.models.BannerText import com.mapbox.navigation.base.formatter.DistanceFormatter -import com.mapbox.navigation.base.internal.factory.TripNotificationStateFactory import com.mapbox.navigation.base.internal.factory.TripNotificationStateFactory.buildTripNotificationState import com.mapbox.navigation.base.internal.time.TimeFormatter import com.mapbox.navigation.base.options.NavigationOptions @@ -34,6 +33,7 @@ import io.mockk.mockkObject import io.mockk.mockkStatic import io.mockk.slot import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull @@ -157,6 +157,7 @@ class MapboxTripNotificationTest { verify(exactly = 1) { mockedContext.registerReceiver(any(), any()) } } + @ExperimentalCoroutinesApi @Test fun whenTripStoppedThenCleanupIsDone() { val notificationManager = @@ -242,8 +243,8 @@ class MapboxTripNotificationTest { mockUpdateNotificationAndroidInteractions() val suffix = "this is nice formatting" mockTimeFormatter(suffix) - val result = String.format(FORMAT_STRING, suffix + duration.toDouble().toString()) - val state = TripNotificationStateFactory.buildTripNotificationState( + val result = String.format(FORMAT_STRING, suffix + duration.toString()) + val state = buildTripNotificationState( null, distance, duration, @@ -257,7 +258,7 @@ class MapboxTripNotificationTest { } @Test - fun whenUpdateNotificationCalledTwiceWithSameDataThenRemoteViewAreNotUpdatedTwice() { + fun whenUpdateNotificationCalledTwiceWithSameDataThenRemoteViewUpdatedTwice() { val state = mockk(relaxed = true) val primaryText = { "Primary Text" } val bannerText = mockBannerText(state, primaryText) @@ -272,8 +273,8 @@ class MapboxTripNotificationTest { notification.updateNotification(state) verify(exactly = 2) { bannerText.text() } - verify(exactly = 1) { collapsedViews.setTextViewText(any(), primaryText()) } - verify(exactly = 1) { expandedViews.setTextViewText(any(), primaryText()) } + verify(exactly = 2) { collapsedViews.setTextViewText(any(), primaryText()) } + verify(exactly = 2) { expandedViews.setTextViewText(any(), primaryText()) } assertEquals(notification.currentManeuverType, MANEUVER_TYPE) assertEquals(notification.currentManeuverModifier, MANEUVER_MODIFIER) } @@ -412,7 +413,7 @@ class MapboxTripNotificationTest { expandedViews.setViewVisibility(R.id.freeDriveText, any()) } verify(exactly = 2) { - expandedViews.setTextViewText(R.id.endNavigationBtnText, STOP_SESSION) + expandedViews.setTextViewText(R.id.endNavigationBtn, STOP_SESSION) } }