diff --git a/libnavigation-core/api/current.txt b/libnavigation-core/api/current.txt index 4060ee7435e..f80f03f90bc 100644 --- a/libnavigation-core/api/current.txt +++ b/libnavigation-core/api/current.txt @@ -426,9 +426,6 @@ package com.mapbox.navigation.core.sensors { package com.mapbox.navigation.core.telemetry { - public final class TelemetryLocationAndProgressDispatcherKt { - } - public final class TelemetryUtilsKt { } 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 73bb5d5bb46..84bfd50778e 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 @@ -12,7 +12,6 @@ import com.mapbox.api.directions.v5.models.DirectionsRoute import com.mapbox.api.directions.v5.models.RouteOptions import com.mapbox.base.common.logger.Logger import com.mapbox.base.common.logger.model.Message -import com.mapbox.base.common.logger.model.Tag import com.mapbox.common.module.provider.MapboxModuleProvider import com.mapbox.common.module.provider.ModuleProviderArgument import com.mapbox.navigation.base.internal.VoiceUnit @@ -192,7 +191,7 @@ class MapboxNavigation( navigationSession.registerNavigationSessionStateObserver(navigationAccountsSession) ifNonNull(accessToken) { token -> logger.d( - Tag(MapboxNavigationTelemetry.TAG), + MapboxNavigationTelemetry.TAG, Message("MapboxMetricsReporter.init from MapboxNavigation main") ) MapboxMetricsReporter.init( @@ -202,13 +201,10 @@ class MapboxNavigation( ) MapboxMetricsReporter.toggleLogging(navigationOptions.isDebugLoggingEnabled) MapboxNavigationTelemetry.initialize( - navigationOptions.applicationContext, this, - MapboxMetricsReporter, - navigationOptions.locationEngine.javaClass.name, - ThreadController.getMainScopeAndRootJob(), navigationOptions, - obtainUserAgent(navigationOptions.isFromNavigationUi) + MapboxMetricsReporter, + logger ) } @@ -331,8 +327,8 @@ class MapboxNavigation( */ fun onDestroy() { logger.d( - Tag(MapboxNavigationTelemetry.TAG), - Message("onDestroy") + MapboxNavigationTelemetry.TAG, + Message("MapboxNavigation onDestroy") ) MapboxNavigationTelemetry.unregisterListeners(this@MapboxNavigation) directionsSession.shutdown() diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/EventLocations.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/EventLocations.kt new file mode 100644 index 00000000000..39e32a6fe89 --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/EventLocations.kt @@ -0,0 +1,19 @@ +package com.mapbox.navigation.core.telemetry + +import android.location.Location + +internal class EventLocations( + private val preEventLocations: List, + private val postEventLocations: MutableList, + private val onBufferFull: (List, List) -> Unit +) { + fun onBufferFull() { + onBufferFull(preEventLocations, postEventLocations) + } + + fun addPostEventLocation(location: Location) { + postEventLocations.add(location) + } + + fun postEventLocationsSize() = postEventLocations.size +} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/ItemAccumulationEventDescriptor.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/ItemAccumulationEventDescriptor.kt deleted file mode 100644 index 9f5e83e7376..00000000000 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/ItemAccumulationEventDescriptor.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.mapbox.navigation.core.telemetry - -import java.util.ArrayDeque - -internal data class ItemAccumulationEventDescriptor( - val preEventBuffer: ArrayDeque, - val postEventBuffer: ArrayDeque, - val onBufferFull: (ArrayDeque, ArrayDeque) -> Unit -) diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/LocationsCollector.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/LocationsCollector.kt new file mode 100644 index 00000000000..355d04fe08a --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/LocationsCollector.kt @@ -0,0 +1,11 @@ +package com.mapbox.navigation.core.telemetry + +import android.location.Location +import com.mapbox.navigation.core.trip.session.LocationObserver + +internal interface LocationsCollector : LocationObserver { + val lastLocation: Location? + + fun flushBuffers() + fun collectLocations(onBufferFull: (List, List) -> Unit) +} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/LocationsCollectorImpl.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/LocationsCollectorImpl.kt new file mode 100644 index 00000000000..4651c45854b --- /dev/null +++ b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/LocationsCollectorImpl.kt @@ -0,0 +1,77 @@ +package com.mapbox.navigation.core.telemetry + +import android.location.Location +import com.mapbox.base.common.logger.Logger +import com.mapbox.base.common.logger.model.Message +import com.mapbox.navigation.core.telemetry.MapboxNavigationTelemetry.TAG + +internal class LocationsCollectorImpl( + private val logger: Logger? +) : LocationsCollector { + + companion object { + private const val LOCATION_BUFFER_MAX_SIZE = 20 + } + + private val locationsBuffer = mutableListOf() + private val eventsLocationsBuffer = mutableListOf() + + override val lastLocation: Location? + get() = locationsBuffer.lastOrNull() + + private fun accumulatePostEventLocation(location: Location) { + val iterator = eventsLocationsBuffer.iterator() + while (iterator.hasNext()) { + iterator.next().let { + it.addPostEventLocation(location) + if (it.postEventLocationsSize() >= LOCATION_BUFFER_MAX_SIZE) { + it.onBufferFull() + iterator.remove() + } + } + } + } + + private fun accumulateLocation(location: Location) { + locationsBuffer.run { + if (size >= LOCATION_BUFFER_MAX_SIZE) { + removeAt(0) + } + add(location) + } + } + + override fun collectLocations( + onBufferFull: (List, List) -> Unit + ) { + eventsLocationsBuffer.add( + EventLocations( + locationsBuffer.getCopy(), + mutableListOf(), + onBufferFull + ) + ) + } + + override fun flushBuffers() { + logger?.d(TAG, Message("flush buffer. Pending events = ${eventsLocationsBuffer.size}")) + eventsLocationsBuffer.forEach { it.onBufferFull() } + eventsLocationsBuffer.clear() + } + + override fun onRawLocationChanged(rawLocation: Location) { + accumulateLocation(rawLocation) + accumulatePostEventLocation(rawLocation) + } + + override fun onEnhancedLocationChanged(enhancedLocation: Location, keyPoints: List) { + // Do nothing + } + + @Synchronized + private fun MutableList.getCopy(): List { + return mutableListOf().also { + it.addAll(this) + } + } +} 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 7736a47e9b3..9f347dce130 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 @@ -1,23 +1,30 @@ package com.mapbox.navigation.core.telemetry -import android.annotation.SuppressLint import android.app.Application import android.content.Context import android.location.Location import android.os.Build -import android.util.Log import com.mapbox.android.core.location.LocationEngine import com.mapbox.android.telemetry.AppUserTurnstile -import com.mapbox.android.telemetry.TelemetryUtils +import com.mapbox.android.telemetry.TelemetryUtils.generateCreateDateFormatted +import com.mapbox.android.telemetry.TelemetryUtils.obtainUniversalUniqueIdentifier import com.mapbox.api.directions.v5.models.DirectionsRoute +import com.mapbox.base.common.logger.Logger +import com.mapbox.base.common.logger.model.Message +import com.mapbox.base.common.logger.model.Tag import com.mapbox.navigation.base.metrics.MetricEvent import com.mapbox.navigation.base.metrics.MetricsReporter import com.mapbox.navigation.base.options.NavigationOptions -import com.mapbox.navigation.base.trip.model.RouteProgressState +import com.mapbox.navigation.base.trip.model.RouteProgress +import com.mapbox.navigation.base.trip.model.RouteProgressState.ROUTE_COMPLETE import com.mapbox.navigation.core.BuildConfig import com.mapbox.navigation.core.MapboxNavigation import com.mapbox.navigation.core.NavigationSession +import com.mapbox.navigation.core.NavigationSession.State.ACTIVE_GUIDANCE +import com.mapbox.navigation.core.NavigationSession.State.FREE_DRIVE +import com.mapbox.navigation.core.NavigationSession.State.IDLE import com.mapbox.navigation.core.NavigationSessionStateObserver +import com.mapbox.navigation.core.directions.session.RoutesObserver import com.mapbox.navigation.core.internal.accounts.MapboxNavigationAccounts import com.mapbox.navigation.core.telemetry.events.AppMetadata import com.mapbox.navigation.core.telemetry.events.FeedbackEvent @@ -29,65 +36,36 @@ import com.mapbox.navigation.core.telemetry.events.NavigationEvent import com.mapbox.navigation.core.telemetry.events.NavigationFeedbackEvent import com.mapbox.navigation.core.telemetry.events.NavigationRerouteEvent import com.mapbox.navigation.core.telemetry.events.PhoneState -import com.mapbox.navigation.core.telemetry.events.RerouteEvent -import com.mapbox.navigation.core.telemetry.events.SessionState import com.mapbox.navigation.core.telemetry.events.TelemetryLocation +import com.mapbox.navigation.core.trip.session.OffRouteObserver +import com.mapbox.navigation.core.trip.session.RouteProgressObserver import com.mapbox.navigation.metrics.MapboxMetricsReporter import com.mapbox.navigation.metrics.internal.event.NavigationAppUserTurnstileEvent -import com.mapbox.navigation.utils.internal.JobControl import com.mapbox.navigation.utils.internal.Time -import com.mapbox.navigation.utils.internal.ifChannelException import com.mapbox.navigation.utils.internal.ifNonNull -import com.mapbox.navigation.utils.internal.monitorChannelWithException -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancelChildren -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlinx.coroutines.selects.select -import java.lang.ref.WeakReference -import java.util.ArrayDeque import java.util.Date -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicInteger -import java.util.concurrent.atomic.AtomicLong -import java.util.concurrent.atomic.AtomicReference -import kotlin.coroutines.coroutineContext - -private data class DynamicallyUpdatedRouteValues( - val distanceRemaining: AtomicLong = AtomicLong(0), - val timeRemaining: AtomicInteger = AtomicInteger(0), - val rerouteCount: AtomicInteger = AtomicInteger(0), - val routeArrived: AtomicBoolean = AtomicBoolean(false), - val timeOfRerouteEvent: AtomicLong = AtomicLong(0), - val timeSinceLastReroute: AtomicInteger = AtomicInteger(0), - var sessionId: String = TelemetryUtils.obtainUniversalUniqueIdentifier(), - val distanceCompleted: AtomicReference = AtomicReference(0f), - val durationRemaining: AtomicInteger = AtomicInteger(0), - val tripIdentifier: AtomicReference = - AtomicReference(TelemetryUtils.obtainUniversalUniqueIdentifier()), - var sessionStartTime: Date = Date(), - var sessionArrivalTime: AtomicReference = AtomicReference(null), - var sdkId: String = "none", - val sessionStarted: AtomicBoolean = AtomicBoolean(false), - val originalRoute: AtomicReference = AtomicReference(null) + +private data class DynamicSessionValues( + var rerouteCount: Int = 0, + var timeOfReroute: Long = 0L, + var timeSinceLastReroute: Int = 0, + var sessionId: String? = null, + var tripIdentifier: String? = null, + var sessionStartTime: Date? = null, + var sessionArrivalTime: Date? = null, + var sessionStarted: Boolean = false, + var handleArrive: Boolean = false ) { fun reset() { - distanceRemaining.set(0) - timeRemaining.set(0) - rerouteCount.set(0) - routeArrived.set(false) - timeOfRerouteEvent.set(0) - sessionId = TelemetryUtils.obtainUniversalUniqueIdentifier() - distanceCompleted.set(0f) - durationRemaining.set(0) - timeSinceLastReroute.set(0) - tripIdentifier.set(TelemetryUtils.obtainUniversalUniqueIdentifier()) - sessionArrivalTime.set(null) - sessionStarted.set(false) + rerouteCount = 0 + timeOfReroute = 0 + timeSinceLastReroute = 0 + sessionId = null + tripIdentifier = null + sessionStartTime = null + sessionArrivalTime = null + sessionStarted = false + handleArrive = false } } @@ -98,29 +76,25 @@ private data class DynamicallyUpdatedRouteValues( - navigation.depart - navigation.feedback - navigation.reroute -- navigation.fasterRoute - navigation.arrive - navigation.cancel The class must be initialized before any telemetry events are reported. Attempting to use telemetry before initialization is called will throw an exception. Initialization may be called multiple times, the call is idempotent. The class has two public methods, postUserFeedback() and initialize(). */ -@SuppressLint("StaticFieldLeak") -internal object MapboxNavigationTelemetry : MapboxNavigationTelemetryInterface { - internal const val LOCATION_BUFFER_MAX_SIZE = 20 - private const val ONE_SECOND = 1000 - private const val MOCK_PROVIDER = - "com.mapbox.navigation.core.replay.ReplayLocationEngine" +internal object MapboxNavigationTelemetry : + RouteProgressObserver, + RoutesObserver, + OffRouteObserver, + NavigationSessionStateObserver { + internal val TAG = Tag("MAPBOX_TELEMETRY") - internal const val TAG = "MAPBOX_TELEMETRY" + private const val ONE_SECOND = 1000 + private const val MOCK_PROVIDER = "com.mapbox.navigation.core.replay.ReplayLocationEngine" private const val EVENT_VERSION = 7 + private lateinit var context: Context // Must be context.getApplicationContext - private lateinit var telemetryThreadControl: JobControl - private val telemetryScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - private var monitorSession: Job? = null private lateinit var metricsReporter: MetricsReporter private lateinit var navigationOptions: NavigationOptions - private lateinit var localUserAgent: String - private var weakMapboxNavigation = WeakReference(null) private var lifecycleMonitor: ApplicationLifecycleMonitor? = null private var appInstance: Application? = null set(value) { @@ -130,374 +104,351 @@ internal object MapboxNavigationTelemetry : MapboxNavigationTelemetryInterface { } field = value ifNonNull(value) { app -> - Log.d(TAG, "Lifecycle monitor created") + logger?.d(TAG, Message("Lifecycle monitor created")) lifecycleMonitor = ApplicationLifecycleMonitor(app) } } + private val dynamicValues = DynamicSessionValues() + private var locationEngineNameExternal: String = LocationEngine::javaClass.name + private lateinit var locationsCollector: LocationsCollector + private lateinit var sdkIdentifier: String + private var logger: Logger? = null + + private var needHandleReroute = false + private var sessionState: NavigationSession.State = IDLE + private var routeProgress: RouteProgress? = null + private var originalRoute: DirectionsRoute? = null + private var needStartSession = false /** - * This class holds all mutable state of the Telemetry object + * This method must be called before using the Telemetry object */ - private val dynamicValues = DynamicallyUpdatedRouteValues() - - private var locationEngineNameExternal: String = LocationEngine::javaClass.name - - private lateinit var callbackDispatcher: TelemetryLocationAndProgressDispatcher - private val navigationSessionObserver = object : NavigationSessionStateObserver { - override fun onNavigationSessionStateChanged(navigationSession: NavigationSession.State) { - when (navigationSession) { - NavigationSession.State.FREE_DRIVE -> { - Log.d(TAG, "Navigation state is $navigationSession") - switchToNotActiveGuidanceBehavior() - } - NavigationSession.State.ACTIVE_GUIDANCE -> { - Log.d(TAG, "Navigation state is $navigationSession") - sessionStart() - } - NavigationSession.State.IDLE -> { - Log.d(TAG, "Navigation state is $navigationSession") - switchToNotActiveGuidanceBehavior() - } - } - Log.d(TAG, "Current session state is: $navigationSession") + fun initialize( + mapboxNavigation: MapboxNavigation, + options: NavigationOptions, + reporter: MetricsReporter, + logger: Logger?, + locationsCollector: LocationsCollector = LocationsCollectorImpl(logger) + ) { + this.logger = logger + this.locationsCollector = locationsCollector + navigationOptions = options + context = options.applicationContext + locationEngineNameExternal = options.locationEngine.javaClass.name + sdkIdentifier = if (options.isFromNavigationUi) { + "mapbox-navigation-ui-android" + } else { + "mapbox-navigation-android" } + metricsReporter = reporter + + registerListeners(mapboxNavigation) + postTurnstileEvent() + log("Valid initialization") } - private fun switchToNotActiveGuidanceBehavior() { - sessionEndPredicate() - sessionEndPredicate = {} - postUserEventDelegate = postUserEventBeforeInit - monitorSession?.cancel() - monitorSession = null + fun setApplicationInstance(app: Application) { + appInstance = app } - private fun telemetryEventGate(event: MetricEvent) = - when (isTelemetryAvailable()) { - false -> { - Log.i( - TAG, - "Route not selected. Telemetry event not sent. Caused by: Navigation " + - "Session started: ${dynamicValues.sessionStarted.get()} Route exists: " + - "${dynamicValues.originalRoute.get() != null}" - ) - false + fun postUserFeedback( + @FeedbackEvent.Type feedbackType: String, + description: String, + @FeedbackEvent.Source feedbackSource: String, + screenshot: String?, + feedbackSubType: Array?, + appMetadata: AppMetadata? + ) { + if (dynamicValues.sessionStarted) { + log("collect post event locations for user feedback") + val feedbackEvent = NavigationFeedbackEvent( + PhoneState(context), + MetricsRouteProgress(routeProgress) + ).apply { + this.feedbackType = feedbackType + this.source = feedbackSource + this.description = description + this.screenshot = screenshot + this.feedbackSubType = feedbackSubType + this.appMetadata = appMetadata + populate() } - true -> { - metricsReporter.addEvent(event) - true + + locationsCollector.collectLocations { preEventBuffer, postEventBuffer -> + log("locations ready") + feedbackEvent.apply { + locationsBefore = preEventBuffer.toTelemetryLocations() + locationsAfter = postEventBuffer.toTelemetryLocations() + } + sendMetricEvent(feedbackEvent) } } + } -// ********** EVENT OBSERVERS *************** - - private fun populateOriginalRouteConditionally() { - ifNonNull(weakMapboxNavigation.get()) { mapboxNavigation -> - val routes = mapboxNavigation.getRoutes() - if (routes.isNotEmpty()) { - Log.d(TAG, "Getting last route from MapboxNavigation") - callbackDispatcher.clearOriginalRoute() - callbackDispatcher.getOriginalRouteReadWrite() - .set(RouteAvailable(routes[0], Date())) - } + fun unregisterListeners(mapboxNavigation: MapboxNavigation) { + mapboxNavigation.run { + unregisterLocationObserver(locationsCollector) + unregisterRouteProgressObserver(this@MapboxNavigationTelemetry) + unregisterRoutesObserver(this@MapboxNavigationTelemetry) + unregisterOffRouteObserver(this@MapboxNavigationTelemetry) + unregisterNavigationSessionObserver(this@MapboxNavigationTelemetry) } + MapboxMetricsReporter.disable() } - private fun sessionStart() { - var isActiveGuidance = true - ifNonNull(weakMapboxNavigation.get()?.getRoutes()) { routes -> - isActiveGuidance = routes.isNotEmpty() + override fun onRouteProgressChanged(routeProgress: RouteProgress) { + this.routeProgress = routeProgress + startSessionIfNeedAndCan() + + if (routeProgress.currentState == ROUTE_COMPLETE) { + processArrival() } - when (isActiveGuidance) { - true -> { - telemetryThreadControl.scope.launch { - callbackDispatcher.resetRouteProgressProcessor() - postUserEventDelegate = - postUserFeedbackEventAfterInit // Telemetry is initialized and the user selected a route. Allow user feedback events to be posted - handleSessionStart() - sessionEndPredicate = { - telemetryScope.launch { - sessionStop() - } + } + + override fun onRoutesChanged(routes: List) { + log("onRoutesChanged. size = ${routes.size}") + routes.getOrNull(0)?.let { + if (sessionState == ACTIVE_GUIDANCE) { + if (originalRoute != null) { + if (needHandleReroute) { + needHandleReroute = false + handleReroute(it) + } else { + log("handle ExternalRoute") + sessionStop() + originalRoute = it + needStartSession = true + startSessionIfNeedAndCan() } + } else { + originalRoute = it + needStartSession = true + startSessionIfNeedAndCan() } - } - false -> { - // Do nothing - Log.d(TAG, "Only Active Guidance supported") + } else { + originalRoute = it } } } - private suspend fun sessionStop() { - Log.d(TAG, "sessionStop") - // The navigation session is over, disallow posting user feedback events - postUserEventDelegate = postUserEventBeforeInit - // Cancellation events will be sent after an arrival event. - when (dynamicValues.routeArrived.get()) { - true, false -> { - telemetryScope.launch { - Log.d(TAG, "calling processCancellationAfterArrival()") - processCancellation() - }.join() + override fun onNavigationSessionStateChanged(navigationSession: NavigationSession.State) { + log("session state is $navigationSession") + sessionState = navigationSession + when (navigationSession) { + IDLE, FREE_DRIVE -> sessionStop() + ACTIVE_GUIDANCE -> { + locationsCollector.flushBuffers() + needStartSession = true + startSessionIfNeedAndCan() } } } - /** - * The Navigation session is considered to be guided if it has been started and at least one route is active, - * it is a free guided / idle session otherwise - */ - private fun isTelemetryAvailable(): Boolean { - return dynamicValues.originalRoute.get() != null && dynamicValues.sessionStarted.get() + override fun onOffRouteStateChanged(offRoute: Boolean) { + log("onOffRouteStateChanged $offRoute") + if (offRoute) { + needHandleReroute = true + } } - /** - * This method generates an off-route telemetry event. Part of the code is suspendable - * because it waits for a new route to be offered by the SDK in response to a reroute - */ - private fun handleOffRouteEvent() { - telemetryThreadControl.scope.launch { - dynamicValues.timeOfRerouteEvent.set(Time.SystemImpl.millis()) - dynamicValues.rerouteCount.addAndGet(1) // increment reroute count - val prevRoute = callbackDispatcher.getRouteProgress() - val newRoute = callbackDispatcher.getDirectionsRouteChannel() - .receive() // Suspend until we get a value - - dynamicValues.distanceRemaining.set(newRoute.route.distance()?.toLong() ?: -1) - dynamicValues.timeSinceLastReroute.set( - (Time.SystemImpl.millis() - dynamicValues.timeOfRerouteEvent.get()).toInt() - ) - callbackDispatcher.addLocationEventDescriptor( - ItemAccumulationEventDescriptor( - ArrayDeque(callbackDispatcher.getCopyOfCurrentLocationBuffer()), - ArrayDeque() - ) { preEventBuffer, postEventBuffer -> - // Populate the RerouteEvent - val rerouteEvent = RerouteEvent(populateSessionState()).apply { - newDistanceRemaining = newRoute.route.distance()?.toInt() ?: -1 - newDurationRemaining = newRoute.route.duration()?.toInt() ?: -1 - newRouteGeometry = obtainGeometry(newRoute.route) - } + private fun registerListeners(mapboxNavigation: MapboxNavigation) { + mapboxNavigation.run { + registerLocationObserver(locationsCollector) + registerRouteProgressObserver(this@MapboxNavigationTelemetry) + registerRoutesObserver(this@MapboxNavigationTelemetry) + registerOffRouteObserver(this@MapboxNavigationTelemetry) + registerNavigationSessionObserver(this@MapboxNavigationTelemetry) + } + } - // Populate and then send a NavigationRerouteEvent - val metricsRouteProgress = MetricsRouteProgress(prevRoute.routeProgress) - val navigationRerouteEvent = NavigationRerouteEvent( - PhoneState(context), - rerouteEvent, - metricsRouteProgress - ).apply { - locationsBefore = preEventBuffer.toTelemetryLocations().toTypedArray() - locationsAfter = postEventBuffer.toTelemetryLocations().toTypedArray() - secondsSinceLastReroute = - dynamicValues.timeSinceLastReroute.get() / ONE_SECOND - distanceRemaining = dynamicValues.distanceRemaining.get().toInt() - distanceCompleted = dynamicValues.distanceCompleted.get().toInt() - durationRemaining = dynamicValues.durationRemaining.get() - } - populateNavigationEvent(navigationRerouteEvent) - val result = telemetryEventGate( - navigationRerouteEvent - ) - Log.d(TAG, "REROUTE event sent $result") - } - ) + private fun sessionStart() { + log("sessionStart") + dynamicValues.run { + sessionId = obtainUniversalUniqueIdentifier() + sessionStartTime = Date() + sessionStarted = true + handleArrive = true } + + val departEvent = NavigationDepartEvent(PhoneState(context)).apply { populate() } + sendMetricEvent(departEvent) } - /** - * The lambda that is called if the SDK client did not initialize telemetry. If telemetry is not initialized, - * calls to post a user feedback event will fail silently - */ - private val postUserEventBeforeInit: suspend ( - String, - String, - String, - String?, - Array?, - AppMetadata? - ) -> Unit = - { _, _, _, _, _, _ -> - Log.d(TAG, "Not in a navigation session, cannot send user feedback events") + private fun sessionStop() { + if (dynamicValues.sessionStarted) { + log("sessionStop") + handleSessionCanceled() + dynamicValues.reset() + resetOriginalRoute() + resetRouteProgress() } + } - /** - * The lambda that is called once telemetry is initialized. - */ - private val postUserFeedbackEventAfterInit: suspend ( - String, - String, - String, - String?, - Array?, - AppMetadata? - ) -> Unit = - { feedbackType, description, feedbackSource, screenshot, feedbackSubType, appMetadata -> - postUserFeedbackHelper( - feedbackType, - description, - feedbackSource, - screenshot, - feedbackSubType, - appMetadata + private fun sendMetricEvent(event: MetricEvent) { + if (isTelemetryAvailable()) { + logger?.d(TAG, Message("${event::class.java} event sent")) + metricsReporter.addEvent(event) + } else { + log( + "${event::class.java} not sent. Caused by: " + + "Navigation Session started: ${dynamicValues.sessionStarted}. " + + "Route exists: ${originalRoute != null}" ) } + } /** - * The delegate lambda that dispatches either a pre or post initialization userFeedbackEvent + * The Navigation session is considered to be guided if it has been started and at least one route is active, + * it is a free drive / idle session otherwise */ - private var postUserEventDelegate = postUserEventBeforeInit - - private val initializer: ( - Context, - MapboxNavigation, - MetricsReporter, - String, - JobControl, - NavigationOptions, - String - ) -> Boolean = - { context, mapboxNavigation, metricsReporter, name, jobControl, options, userAgent -> - telemetryThreadControl = jobControl - weakMapboxNavigation = WeakReference(mapboxNavigation) - weakMapboxNavigation.get()?.registerNavigationSessionObserver(navigationSessionObserver) - registerForNotification(mapboxNavigation) - monitorOffRouteEvents() - populateOriginalRouteConditionally() - this.context = context - localUserAgent = userAgent - locationEngineNameExternal = name - navigationOptions = options - this.metricsReporter = metricsReporter - postTurnstileEvent() - monitorJobCancelation() - Log.i(TAG, "Valid initialization") - true + private fun isTelemetryAvailable(): Boolean { + return originalRoute != null && dynamicValues.sessionStarted + } + + private fun handleReroute(newRoute: DirectionsRoute) { + log("handleReroute") + dynamicValues.run { + val currentTime = Time.SystemImpl.millis() + timeSinceLastReroute = (currentTime - timeOfReroute).toInt() + timeOfReroute = currentTime + rerouteCount++ } - private var sessionEndPredicate = { } + val navigationRerouteEvent = NavigationRerouteEvent( + PhoneState(context), + MetricsRouteProgress(routeProgress) + ).apply { + secondsSinceLastReroute = dynamicValues.timeSinceLastReroute / ONE_SECOND + newDistanceRemaining = newRoute.distance().toInt() + newDurationRemaining = newRoute.duration().toInt() + newGeometry = obtainGeometry(newRoute) + populate() + } - fun setApplicationInstance(app: Application) { - appInstance = app + locationsCollector.collectLocations { preEventBuffer, postEventBuffer -> + navigationRerouteEvent.apply { + locationsBefore = preEventBuffer.toTelemetryLocations() + locationsAfter = postEventBuffer.toTelemetryLocations() + } + + sendMetricEvent(navigationRerouteEvent) + } } - /** - * This method must be called before using the Telemetry object - */ - fun initialize( - context: Context, - mapboxNavigation: MapboxNavigation, - metricsReporter: MetricsReporter, - locationEngineName: String, - jobControl: JobControl, - options: NavigationOptions, - userAgent: String - ) { - weakMapboxNavigation.get()?.let { - unregisterListeners(it) - telemetryThreadControl.job.cancelChildren() - telemetryThreadControl.job.cancel() + private fun handleSessionCanceled() { + log("handleSessionCanceled") + locationsCollector.flushBuffers() + + val cancelEvent = NavigationCancelEvent(PhoneState(context)).apply { populate() } + ifNonNull(dynamicValues.sessionArrivalTime) { + cancelEvent.arrivalTimestamp = generateCreateDateFormatted(it) } - initializer( - context, - mapboxNavigation, - metricsReporter, - locationEngineName, - jobControl, - options, - userAgent - ) + sendMetricEvent(cancelEvent) } - private fun monitorOffRouteEvents() { - telemetryThreadControl.scope.monitorChannelWithException( - callbackDispatcher.getOffRouteEventChannel(), - { offRoute -> - when (offRoute) { - true -> { - handleOffRouteEvent() - } - false -> { - } - } + private fun postTurnstileEvent() { + val turnstileEvent = + AppUserTurnstile(sdkIdentifier, BuildConfig.MAPBOX_NAVIGATION_VERSION_NAME).also { + it.setSkuId(MapboxNavigationAccounts.getInstance(context).obtainSkuId()) } - ) + val event = NavigationAppUserTurnstileEvent(turnstileEvent) + metricsReporter.addEvent(event) } - private fun monitorJobCancelation() { - telemetryScope.launch { - select { - telemetryThreadControl.job.onJoin { - Log.d(TAG, "master job canceled") - callbackDispatcher.flushBuffers() - MapboxMetricsReporter.disable() // Disable telemetry unconditionally + private fun processArrival() { + if (dynamicValues.sessionStarted && dynamicValues.handleArrive) { + log("you have arrived") + + dynamicValues.run { + tripIdentifier = obtainUniversalUniqueIdentifier() + sessionArrivalTime = Date() + handleArrive = false + } + + val arriveEvent = NavigationArriveEvent(PhoneState(context)).apply { populate() } + sendMetricEvent(arriveEvent) + } + } + + private fun NavigationEvent.populate() { + log("populateNavigationEvent") + + this.apply { + sdkIdentifier = this@MapboxNavigationTelemetry.sdkIdentifier + + routeProgress!!.let { routeProgress -> + stepIndex = routeProgress.currentLegProgress?.currentStepProgress?.stepIndex ?: 0 + + distanceRemaining = routeProgress.distanceRemaining.toInt() + durationRemaining = routeProgress.durationRemaining.toInt() + distanceCompleted = routeProgress.distanceTraveled.toInt() + + routeProgress.route.let { + geometry = it.geometry() + profile = it.routeOptions()?.profile() + requestIdentifier = it.routeOptions()?.requestUuid() + stepCount = obtainStepCount(it) + legIndex = it.routeIndex()?.toInt() ?: 0 + legCount = it.legs()?.size ?: 0 + + absoluteDistanceToDestination = obtainAbsoluteDistance( + locationsCollector.lastLocation, + obtainRouteDestination(it) + ) + estimatedDistance = it.distance().toInt() + estimatedDuration = it.duration().toInt() + totalStepCount = obtainStepCount(it) } } + + originalRoute!!.let { + originalStepCount = obtainStepCount(it) + originalEstimatedDistance = it.distance().toInt() + originalEstimatedDuration = it.duration().toInt() + originalRequestIdentifier = it.routeOptions()?.requestUuid() + originalGeometry = it.geometry() + } + + locationEngine = locationEngineNameExternal + tripIdentifier = obtainUniversalUniqueIdentifier() + lat = locationsCollector.lastLocation?.latitude ?: 0.0 + lng = locationsCollector.lastLocation?.longitude ?: 0.0 + simulation = locationEngineNameExternal == MOCK_PROVIDER + percentTimeInPortrait = lifecycleMonitor?.obtainPortraitPercentage() ?: 100 + percentTimeInForeground = lifecycleMonitor?.obtainForegroundPercentage() ?: 100 + + dynamicValues.let { + startTimestamp = generateCreateDateFormatted(it.sessionStartTime) + rerouteCount = it.rerouteCount + sessionIdentifier = it.sessionId + } + + eventVersion = EVENT_VERSION } } - /** - * This method sends a user feedback event to the back-end servers. The method will suspend because the helper method it calls is itself suspendable - * The method may suspend until it collects 40 location events. The worst case scenario is a 40 location suspension, 20 is best case - */ - override fun postUserFeedback( - @FeedbackEvent.Type feedbackType: String, - description: String, - @FeedbackEvent.Source feedbackSource: String, - screenshot: String?, - feedbackSubType: Array?, - appMetadata: AppMetadata? - ) { - telemetryThreadControl.scope.launch { - postUserEventDelegate( - feedbackType, - description, - feedbackSource, - screenshot, - feedbackSubType, - appMetadata - ) + private fun resetRouteProgress() { + log("resetRouteProgress") + routeProgress = null + } + + private fun resetOriginalRoute() { + log("resetOriginalRoute") + originalRoute = null + } + + private fun startSessionIfNeedAndCan() { + if (needStartSession && canSessionBeStarted()) { + needStartSession = false + sessionStart() } } - /** - * Helper class that posts user feedback. The call is available only after initialization - */ - private fun postUserFeedbackHelper( - @FeedbackEvent.Type feedbackType: String, - description: String, - @FeedbackEvent.Source feedbackSource: String, - screenshot: String?, - feedbackSubType: Array?, - appMetadata: AppMetadata? - ) { - Log.d(TAG, "trying to post a user feedback event") - val lastProgress = callbackDispatcher.getRouteProgress() - callbackDispatcher.addLocationEventDescriptor( - ItemAccumulationEventDescriptor( - ArrayDeque(callbackDispatcher.getCopyOfCurrentLocationBuffer()), - ArrayDeque() - ) { preEventBuffer, postEventBuffer -> - val feedbackEvent = NavigationFeedbackEvent( - PhoneState(context), - MetricsRouteProgress(lastProgress.routeProgress) - ).apply { - this.feedbackType = feedbackType - this.source = feedbackSource - this.description = description - this.screenshot = screenshot - this.locationsBefore = preEventBuffer.toTelemetryLocations().toTypedArray() - this.locationsAfter = postEventBuffer.toTelemetryLocations().toTypedArray() - this.feedbackSubType = feedbackSubType - this.appMetadata = appMetadata - } - populateNavigationEvent(feedbackEvent) - val eventPosted = telemetryEventGate(feedbackEvent) - Log.i(TAG, "Posting a user feedback event $eventPosted") - } - ) + private fun canSessionBeStarted(): Boolean { + return originalRoute != null && routeProgress != null } - private fun ArrayDeque.toTelemetryLocations(): List { + private fun List.toTelemetryLocations(): Array { val feedbackLocations = mutableListOf() this.forEach { feedbackLocations.add( @@ -518,307 +469,10 @@ internal object MapboxNavigationTelemetry : MapboxNavigationTelemetryInterface { ) } - return feedbackLocations - } - - /** - * This method posts a cancel event in response to onSessionEnd - */ - private suspend fun handleSessionCanceled(): CompletableDeferred { - val retVal = CompletableDeferred() - val cancelEvent = NavigationCancelEvent(PhoneState(context)) - ifNonNull(dynamicValues.sessionArrivalTime.get()) { - cancelEvent.arrivalTimestamp = TelemetryUtils.generateCreateDateFormatted(it) - } - populateNavigationEvent(cancelEvent) - val result = telemetryEventGate(cancelEvent) - Log.d(TAG, "CANCEL event sent $result") - callbackDispatcher.cancelCollectionAndPostFinalEvents().join() - retVal.complete(true) - return retVal - } - - /** - * This method clears the state data for the Telemetry object in response to onSessionEnd - */ - private fun handleSessionStop() { - dynamicValues.reset() - callbackDispatcher.clearOriginalRoute() + return feedbackLocations.toTypedArray() } - /** - * This method starts a session. If a session is active it will terminate it, causing an stop/cancel event to be sent to the servers. - * Every session start is guaranteed to have a session end. - */ - private fun handleSessionStart() { - telemetryThreadControl.scope.launch { - Log.d(TAG, "Waiting in handleSessionStart") - dynamicValues.originalRoute.set(callbackDispatcher.getOriginalRouteAsync().await()) - dynamicValues.originalRoute.get()?.let { directionsRoute -> - Log.d(TAG, "The wait is over") - sessionStartHelper(directionsRoute) - } - } + private fun log(message: String) { + logger?.d(TAG, Message(message)) } - - /** - * This method is used by a lambda. Since the Telemetry class is a singleton, U.I. elements may call postTurnstileEvent() before the singleton is initialized. - * A lambda guards against this possibility - */ - private fun postTurnstileEvent() { - // AppUserTurnstile is implemented in mapbox-telemetry-sdk - val sdkId = generateSdkIdentifier() - dynamicValues.sdkId = generateSdkIdentifier() - val appUserTurnstileEvent = - AppUserTurnstile(sdkId, BuildConfig.MAPBOX_NAVIGATION_VERSION_NAME).also { - it.setSkuId( - MapboxNavigationAccounts.getInstance( - context - ).obtainSkuId() - ) - } - val event = NavigationAppUserTurnstileEvent(appUserTurnstileEvent) - metricsReporter.addEvent(event) - } - - /** - * This method starts a session. The start of a session does not result in a telemetry event being sent to the servers. - */ - private fun sessionStartHelper(directionsRoute: DirectionsRoute) { - dynamicValues.sessionId = TelemetryUtils.obtainUniversalUniqueIdentifier() - dynamicValues.sessionStartTime = Date() - dynamicValues.sessionStarted.set(true) - monitorSession = telemetryThreadControl.scope.launch { - val result = telemetryEventGate( - telemetryDeparture( - directionsRoute, - callbackDispatcher.getFirstLocationAsync().await() - ) - ) - Log.d(TAG, "DEPARTURE event sent $result") - monitorSession() - } - } - - private suspend fun processCancellation() { - Log.d(TAG, "Session was canceled") - handleSessionCanceled().await() - handleSessionStop() - } - - private suspend fun processArrival() { - Log.d(TAG, "you have arrived") - dynamicValues.tripIdentifier.set(TelemetryUtils.obtainUniversalUniqueIdentifier()) - dynamicValues.sessionArrivalTime.set(Date()) - val arriveEvent = NavigationArriveEvent(PhoneState(context)) - dynamicValues.routeArrived.set(true) - populateNavigationEvent(arriveEvent) - val result = telemetryEventGate(arriveEvent) - Log.d(TAG, "ARRIVAL event sent $result") - callbackDispatcher.cancelCollectionAndPostFinalEvents().join() - populateOriginalRouteConditionally() - } - - /** - * This method waits for an [RouteProgressState.ROUTE_COMPLETE] event. Once received, it terminates the wait-loop and - * sends the telemetry data to the servers. - */ - private suspend fun monitorSession() { - var continueRunning = true - var trackingEvent = 0 - while (coroutineContext.isActive && continueRunning) { - try { - val routeData = callbackDispatcher.getRouteProgressChannel().receive() - dynamicValues.distanceCompleted.set( - dynamicValues.distanceCompleted.get() + - routeData.routeProgress.distanceTraveled - ) - dynamicValues.distanceRemaining.set( - routeData.routeProgress.distanceRemaining.toLong() - ) - dynamicValues.durationRemaining.set( - routeData.routeProgress.durationRemaining.toInt() - ) - when (routeData.routeProgress.currentState) { - RouteProgressState.ROUTE_COMPLETE -> { - when (dynamicValues.sessionStarted.get()) { - true -> { - processArrival() - continueRunning = false - } - false -> { - // Do nothing. - Log.d(TAG, "route arrival received before a session start") - } - } - } // END - RouteProgressState.LOCATION_TRACKING -> { - dynamicValues.timeRemaining.set( - callbackDispatcher - .getRouteProgress() - .routeProgress - .durationRemaining - .toInt() - ) - dynamicValues.distanceRemaining.set( - callbackDispatcher - .getRouteProgress() - .routeProgress - .distanceRemaining - .toLong() - ) - when (trackingEvent > 20) { - true -> { - Log.i(TAG, "LOCATION_TRACKING received $trackingEvent") - trackingEvent = 0 - } - false -> { - trackingEvent++ - } - } - } - else -> { - // Do nothing - } - } - } catch (e: Exception) { - Log.i(TAG, "monitorSession ${e.localizedMessage}") - e.ifChannelException { - continueRunning = false - } - } - } - } - - private fun telemetryDeparture( - directionsRoute: DirectionsRoute, - startingLocation: Location - ): MetricEvent { - val departEvent = NavigationDepartEvent(PhoneState(context)) - populateNavigationEvent(departEvent, directionsRoute, startingLocation) - return departEvent - } - - private fun registerForNotification(mapboxNavigation: MapboxNavigation) { - callbackDispatcher = - TelemetryLocationAndProgressDispatcher(telemetryThreadControl.scope) // The class responds to most notification events - mapboxNavigation.registerRouteProgressObserver(callbackDispatcher) - mapboxNavigation.registerLocationObserver(callbackDispatcher) - mapboxNavigation.registerRoutesObserver(callbackDispatcher) - mapboxNavigation.registerOffRouteObserver(callbackDispatcher) - } - - override fun unregisterListeners(mapboxNavigation: MapboxNavigation) { - mapboxNavigation.unregisterRouteProgressObserver(callbackDispatcher) - mapboxNavigation.unregisterLocationObserver(callbackDispatcher) - mapboxNavigation.unregisterRoutesObserver(callbackDispatcher) - mapboxNavigation.unregisterOffRouteObserver(callbackDispatcher) - mapboxNavigation.unregisterNavigationSessionObserver(navigationSessionObserver) - Log.d(TAG, "resetting Telemetry initialization") - weakMapboxNavigation.clear() - } - - private fun populateSessionState(newLocation: Location? = null): SessionState { - val timeSinceReroute: Int = if (dynamicValues.timeSinceLastReroute.get() == 0) { - -1 - } else - dynamicValues.timeSinceLastReroute.get() - - return SessionState().apply { - secondsSinceLastReroute = timeSinceReroute - eventLocation = - newLocation ?: callbackDispatcher.getLastLocation() ?: Location("unknown") - - eventDate = Date() - eventRouteDistanceCompleted = - callbackDispatcher.getRouteProgress().routeProgress.distanceTraveled.toDouble() - originalDirectionRoute = callbackDispatcher.getOriginalRouteReadOnly()?.route - currentDirectionRoute = callbackDispatcher.getRouteProgress().routeProgress.route - sessionIdentifier = dynamicValues.sessionId - tripIdentifier = dynamicValues.tripIdentifier.get() - originalRequestIdentifier = - callbackDispatcher.getOriginalRouteReadOnly()?.route?.routeOptions()?.requestUuid() - requestIdentifier = - callbackDispatcher.getRouteProgress().routeProgress.route.routeOptions() - ?.requestUuid() - mockLocation = locationEngineName == MOCK_PROVIDER - rerouteCount = dynamicValues.rerouteCount.get() - startTimestamp = dynamicValues.sessionStartTime - arrivalTimestamp = dynamicValues.sessionArrivalTime.get() - locationEngineName = locationEngineNameExternal - percentInForeground = lifecycleMonitor?.obtainForegroundPercentage() ?: 100 - percentInPortrait = lifecycleMonitor?.obtainPortraitPercentage() ?: 100 - } - } - - private fun populateNavigationEvent( - navigationEvent: NavigationEvent, - route: DirectionsRoute? = null, - newLocation: Location? = null - ) { - val directionsRoute = route ?: callbackDispatcher.getRouteProgress().routeProgress.route - val location = newLocation ?: callbackDispatcher.getLastLocation() - navigationEvent.startTimestamp = - TelemetryUtils.generateCreateDateFormatted(dynamicValues.sessionStartTime) - navigationEvent.sdkIdentifier = generateSdkIdentifier() - navigationEvent.sessionIdentifier = dynamicValues.sessionId - navigationEvent.geometry = - callbackDispatcher.getRouteProgress().routeProgress.route.geometry() - navigationEvent.profile = - callbackDispatcher.getRouteProgress().routeProgress.route.routeOptions()?.profile() - navigationEvent.originalRequestIdentifier = - callbackDispatcher.getOriginalRouteReadOnly()?.route?.routeOptions()?.requestUuid() - navigationEvent.requestIdentifier = - callbackDispatcher.getRouteProgress().routeProgress.route.routeOptions()?.requestUuid() - navigationEvent.originalGeometry = - callbackDispatcher.getOriginalRouteReadOnly()?.route?.geometry() - navigationEvent.locationEngine = locationEngineNameExternal - navigationEvent.tripIdentifier = TelemetryUtils.obtainUniversalUniqueIdentifier() - navigationEvent.lat = location?.latitude ?: 0.0 - navigationEvent.lng = location?.longitude ?: 0.0 - navigationEvent.simulation = locationEngineNameExternal == MOCK_PROVIDER - navigationEvent.absoluteDistanceToDestination = obtainAbsoluteDistance( - callbackDispatcher.getLastLocation(), - obtainRouteDestination(directionsRoute) - ) - navigationEvent.percentTimeInPortrait = lifecycleMonitor?.obtainPortraitPercentage() ?: 100 - navigationEvent.percentTimeInForeground = - lifecycleMonitor?.obtainForegroundPercentage() ?: 100 - navigationEvent.distanceCompleted = dynamicValues.distanceCompleted.get().toInt() - navigationEvent.distanceRemaining = dynamicValues.distanceRemaining.get().toInt() - navigationEvent.durationRemaining = dynamicValues.durationRemaining.get() - navigationEvent.eventVersion = EVENT_VERSION - navigationEvent.estimatedDistance = directionsRoute.distance()?.toInt() ?: 0 - navigationEvent.estimatedDuration = directionsRoute.duration()?.toInt() ?: 0 - navigationEvent.rerouteCount = dynamicValues.rerouteCount.get() - navigationEvent.originalEstimatedDistance = - callbackDispatcher.getOriginalRouteReadOnly()?.route?.distance()?.toInt() ?: 0 - navigationEvent.originalEstimatedDuration = - callbackDispatcher.getOriginalRouteReadOnly()?.route?.duration()?.toInt() ?: 0 - navigationEvent.stepCount = - obtainStepCount(callbackDispatcher.getRouteProgress().routeProgress.route) - navigationEvent.originalStepCount = - obtainStepCount(callbackDispatcher.getOriginalRouteReadOnly()?.route) - navigationEvent.legIndex = - callbackDispatcher.getRouteProgress().routeProgress.route.routeIndex()?.toInt() ?: 0 - navigationEvent.legCount = - callbackDispatcher.getRouteProgress().routeProgress.route.legs()?.size ?: 0 - navigationEvent.stepIndex = callbackDispatcher - .getRouteProgress() - .routeProgress - .currentLegProgress - ?.currentStepProgress - ?.stepIndex - ?: 0 - - // TODO:OZ voiceIndex is not available in SDK 1.0 and was not set in the legacy telemetry navigationEvent.voiceIndex - // TODO:OZ bannerIndex is not available in SDK 1.0 and was not set in the legacy telemetry navigationEvent.bannerIndex - navigationEvent.totalStepCount = obtainStepCount(directionsRoute) - } - - private fun generateSdkIdentifier() = - if (navigationOptions.isFromNavigationUi) - "mapbox-navigation-ui-android" - else - "mapbox-navigation-android" } diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/MapboxNavigationTelemetryInterface.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/MapboxNavigationTelemetryInterface.kt deleted file mode 100644 index e5b8102000a..00000000000 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/MapboxNavigationTelemetryInterface.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.mapbox.navigation.core.telemetry - -import com.mapbox.navigation.core.MapboxNavigation -import com.mapbox.navigation.core.telemetry.events.AppMetadata -import com.mapbox.navigation.core.telemetry.events.FeedbackEvent - -internal interface MapboxNavigationTelemetryInterface { - fun postUserFeedback( - @FeedbackEvent.Type feedbackType: String, - description: String, - @FeedbackEvent.Source feedbackSource: String, - screenshot: String?, - feedbackSubType: Array?, - appMetadata: AppMetadata? - ) - - fun unregisterListeners(mapboxNavigation: MapboxNavigation) -} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/RouteAvailable.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/RouteAvailable.kt deleted file mode 100644 index f59dfe9365c..00000000000 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/RouteAvailable.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.mapbox.navigation.core.telemetry - -import com.mapbox.api.directions.v5.models.DirectionsRoute -import java.util.Date - -internal data class RouteAvailable(val route: DirectionsRoute, val date: Date) diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/RouteProgressWithTimestamp.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/RouteProgressWithTimestamp.kt deleted file mode 100644 index 30d07e3fa93..00000000000 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/RouteProgressWithTimestamp.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.mapbox.navigation.core.telemetry - -import com.mapbox.navigation.base.trip.model.RouteProgress - -internal data class RouteProgressWithTimestamp(val date: Long, val routeProgress: RouteProgress) diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/TelemetryLocationAndProgressDispatcher.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/TelemetryLocationAndProgressDispatcher.kt deleted file mode 100644 index 88696270ea6..00000000000 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/TelemetryLocationAndProgressDispatcher.kt +++ /dev/null @@ -1,346 +0,0 @@ -package com.mapbox.navigation.core.telemetry - -import android.location.Location -import android.util.Log -import com.mapbox.api.directions.v5.models.DirectionsRoute -import com.mapbox.navigation.base.trip.model.RouteProgress -import com.mapbox.navigation.base.trip.model.RouteProgressState -import com.mapbox.navigation.core.directions.session.RoutesObserver -import com.mapbox.navigation.core.telemetry.MapboxNavigationTelemetry.LOCATION_BUFFER_MAX_SIZE -import com.mapbox.navigation.core.telemetry.MapboxNavigationTelemetry.TAG -import com.mapbox.navigation.core.trip.session.LocationObserver -import com.mapbox.navigation.core.trip.session.OffRouteObserver -import com.mapbox.navigation.core.trip.session.RouteProgressObserver -import com.mapbox.navigation.utils.internal.ThreadController -import com.mapbox.navigation.utils.internal.Time -import com.mapbox.navigation.utils.internal.monitorChannelWithException -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.ReceiveChannel -import kotlinx.coroutines.launch -import java.util.Collections -import java.util.Date -import java.util.concurrent.atomic.AtomicReference - -private typealias RouteProgressReference = (RouteProgress) -> Unit - -internal class TelemetryLocationAndProgressDispatcher(scope: CoroutineScope) : - RouteProgressObserver, LocationObserver, RoutesObserver, OffRouteObserver { - private var lastLocation: AtomicReference = AtomicReference(null) - private var routeProgress: AtomicReference = - AtomicReference( - RouteProgressWithTimestamp( - 0, - RouteProgress.Builder( - DirectionsRoute.builder() - .distance(.0) - .duration(.0) - .build() - ).build() - ) - ) - private val channelOffRouteEvent = Channel(Channel.CONFLATED) - private val channelNewRouteAvailable = Channel(Channel.CONFLATED) - private val channelLocationReceived = Channel(Channel.CONFLATED) - private val channelOnRouteProgress = - Channel(Channel.CONFLATED) // we want just the last notification - private var jobControl: CoroutineScope = scope - private var originalRoute = AtomicReference(null) - private var accumulationJob: Job = Job() - private val currentLocationBuffer = SynchronizedItemBuffer() - private val locationEventBuffer = - SynchronizedItemBuffer>() - private val originalRoutePreInit = { routes: List -> - if (originalRoute.get() == null) { - originalRoute.set(RouteAvailable(routes[0], Date())) - originalRouteDelegate = originalRoutePostInit - } - } - private val originalRouteDiffered = CompletableDeferred() - private var originalRouteDifferedValue: DirectionsRoute? = null - - private val originalRoutePostInit = { _: List -> Unit } - private var originalRouteDelegate: (List) -> Unit = originalRoutePreInit - private val firstLocation = CompletableDeferred() - private var firstLocationValue: Location? = null - private var priorState = RouteProgressState.ROUTE_INVALID - private val routeProgressPredicate = AtomicReference() - - init { - routeProgressPredicate.set { routeProgress -> beforeArrival(routeProgress) } - } - - /** - * This class provides thread-safe access to a mutable list of locations - */ - private class SynchronizedItemBuffer { - private val synchronizedCollection: MutableList = - Collections.synchronizedList(mutableListOf()) - - fun addItem(item: T) { - synchronized(synchronizedCollection) { - synchronizedCollection.add(0, item) - } - } - - fun removeItem() { - synchronized(synchronizedCollection) { - if (synchronizedCollection.isNotEmpty()) { - val index = synchronizedCollection.size - 1 - synchronizedCollection.removeAt(index) - } - } - } - - fun getCopy(): List { - val result = mutableListOf() - synchronized(synchronizedCollection) { - result.addAll(synchronizedCollection) - } - return result - } - - fun clear() { - synchronized(synchronizedCollection) { - synchronizedCollection.clear() - } - } - - fun applyToEach(predicate: (T) -> Boolean) { - synchronized(synchronizedCollection) { - val iterator = synchronizedCollection.iterator() - while (iterator.hasNext()) { - val nextItem = iterator.next() - if (!predicate(nextItem)) { - iterator.remove() - } - } - } - } - - fun size() = synchronizedCollection.size - } - - init { - // Unconditionally update the contents of the pre-event buffer - accumulationJob = jobControl.monitorChannelWithException( - channelLocationReceived, - { location -> - accumulateLocationAsync(location, currentLocationBuffer) - processLocationBuffer(location) - } - ) - } - - /** - * Process the location event buffer twice. The first time, update each of it's elements - * with a new location object. On the second pass, execute the stored lambda if the buffer - * size is equal to or greater than a given value. - */ - private fun processLocationBuffer(location: Location) { - // Update each event buffer with a new location - locationEventBuffer.applyToEach { item -> - item.postEventBuffer.addFirst(location) - true - } - locationEventBuffer.applyToEach { item -> - when (item.postEventBuffer.size >= LOCATION_BUFFER_MAX_SIZE) { - true -> { - item.onBufferFull(item.preEventBuffer, item.postEventBuffer) - false - } - else -> { - // Do nothing. - true - } - } - } - } - - fun flushBuffers() { - Log.d(TAG, "flushing buffers before ${currentLocationBuffer.size()}") - locationEventBuffer.applyToEach { item -> - item.onBufferFull(item.preEventBuffer, item.postEventBuffer) - false - } - } - - /** - * This method accumulates locations. The number of locations is limited by [MapboxNavigationTelemetry.LOCATION_BUFFER_MAX_SIZE]. - * Once this limit is reached, an item is removed before another is added. The method returns true if the queue reaches capacity, - * false otherwise - */ - private fun accumulateLocationAsync( - location: Location, - queue: SynchronizedItemBuffer - ): Boolean { - var result = false - when (queue.size() >= LOCATION_BUFFER_MAX_SIZE) { - true -> { - queue.removeItem() - queue.addItem(location) - result = true - } - false -> { - queue.addItem(location) - } - } - return result - } - - fun addLocationEventDescriptor(eventDescriptor: ItemAccumulationEventDescriptor) { - eventDescriptor.preEventBuffer.clear() - eventDescriptor.postEventBuffer.clear() - eventDescriptor.preEventBuffer.addAll(currentLocationBuffer.getCopy()) - locationEventBuffer.addItem(eventDescriptor) - } - - /** - * This method cancels all jobs that accumulate telemetry data. The side effect of this call is to call Telemetry.addEvent(), which may cause events to be sent - * to the back-end server - */ - fun cancelCollectionAndPostFinalEvents(): Job { - return ThreadController.getIOScopeAndRootJob().scope.launch { - flushBuffers() - locationEventBuffer.clear() - } - } - - /** - * This channel becomes signaled if a navigation route is selected - */ - fun getDirectionsRouteChannel(): ReceiveChannel = channelNewRouteAvailable - - fun getCopyOfCurrentLocationBuffer() = currentLocationBuffer.getCopy() - - fun getOriginalRouteReadOnly() = originalRoute.get() - - fun getOriginalRouteReadWrite() = originalRoute - - fun resetRouteProgressProcessor() { - routeProgressPredicate.set { routeProgress -> beforeArrival(routeProgress) } - } - - fun getOffRouteEventChannel(): ReceiveChannel = channelOffRouteEvent - - /** - * This method is called for any state change, excluding RouteProgressState.ROUTE_ARRIVED. - * It forwards the route progress data to a listener and saves it to a local variable - */ - private fun beforeArrival(routeProgress: RouteProgress) { - val data = RouteProgressWithTimestamp(Time.SystemImpl.millis(), routeProgress) - this.routeProgress.set(data) - channelOnRouteProgress.offer(data) - if (routeProgress.currentState == RouteProgressState.ROUTE_COMPLETE) { - routeProgressPredicate.set { progress -> afterArrival(progress) } - } - } - - /** - * This method is called in response to receiving a RouteProgressState.ROUTE_ARRIVED event. - * It stores the route progress data without notifying listeners. - */ - private fun afterArrival(routeProgress: RouteProgress) { - when (routeProgress.currentState) { - priorState -> { - } - else -> { - priorState = routeProgress.currentState - Log.d(TAG, "route progress state = ${routeProgress.currentState}") - } - } - val data = RouteProgressWithTimestamp(Time.SystemImpl.millis(), routeProgress) - this.routeProgress.set(data) - } - - override fun onRouteProgressChanged(routeProgress: RouteProgress) { - routeProgressPredicate.get()(routeProgress) - } - - fun getRouteProgressChannel(): ReceiveChannel = - channelOnRouteProgress - - fun getLastLocation(): Location? = lastLocation.get() - - fun getRouteProgress(): RouteProgressWithTimestamp = routeProgress.get() - - fun clearOriginalRoute() { - originalRoute.set(null) - originalRouteDifferedValue = null - originalRouteDelegate = originalRoutePreInit - } - - override fun onRawLocationChanged(rawLocation: Location) { - channelLocationReceived.offer(rawLocation) - lastLocation.set(rawLocation) - when (firstLocationValue) { - null -> { - firstLocationValue = rawLocation - firstLocationValue?.let { location -> - firstLocation.complete(location) - } - } - else -> { - firstLocationValue?.let { location -> - firstLocation.complete(location) - } - } - } - } - - override fun onEnhancedLocationChanged(enhancedLocation: Location, keyPoints: List) { - // Do nothing - } - - fun getFirstLocationAsync() = firstLocation - - fun getOriginalRouteAsync() = originalRouteDiffered - - private fun notifyOfNewRoute(routes: List) { - when (originalRouteDifferedValue) { - null -> { - Log.d(TAG, "First time route set") - if (routes.isNotEmpty()) { - originalRouteDifferedValue = routes[0] - originalRouteDifferedValue?.let { route -> - originalRouteDiffered.complete(route) - } - } else { - Log.d(TAG, "Empty route list received. Not setting route 2") - } - } - else -> { - if (routes.isNotEmpty()) { - Log.d(TAG, "Subsequent route set") - originalRouteDifferedValue?.let { route -> - originalRouteDiffered.complete(route) - } - } else { - Log.d(TAG, "Empty route list received. Not setting route 2") - } - } - } - } - - override fun onRoutesChanged(routes: List) { - when (routes.isEmpty()) { - true -> { - Log.d(TAG, "onRoutesChanged received an empty route list") - } - false -> { - Log.d(TAG, "onRoutesChanged received a valid route list") - val date = Date() - channelNewRouteAvailable.offer(RouteAvailable(routes[0], date)) - originalRouteDelegate(routes) - notifyOfNewRoute(routes) - } - } - } - - override fun onOffRouteStateChanged(offRoute: Boolean) { - Log.d(TAG, "onOffRouteStateChanged $offRoute") - channelOffRouteEvent.offer(offRoute) - } -} 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 178cfa2f6ed..ac1c02bd490 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 @@ -6,17 +6,16 @@ import com.mapbox.navigation.base.metrics.NavigationMetrics @SuppressLint("ParcelCreator") internal class NavigationRerouteEvent( phoneState: PhoneState, - rerouteEvent: RerouteEvent, metricsRouteProgress: MetricsRouteProgress ) : NavigationEvent(phoneState) { /* * Don't remove any fields, cause they are should match with * the schema downloaded from S3. Look at {@link SchemaTest} */ - val newDistanceRemaining: Int = rerouteEvent.newDistanceRemaining - val newDurationRemaining: Int = rerouteEvent.newDurationRemaining + var newDistanceRemaining: Int = 0 + var newDurationRemaining: Int = 0 val feedbackId: String = phoneState.feedbackId - val newGeometry: String = rerouteEvent.newRouteGeometry + var newGeometry: String? = null val step: NavigationStepData = NavigationStepData(metricsRouteProgress) var secondsSinceLastReroute: Int = 0 var locationsBefore: Array? = emptyArray() diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/RerouteEvent.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/RerouteEvent.kt deleted file mode 100644 index 958b6c7c6d8..00000000000 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/RerouteEvent.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.mapbox.navigation.core.telemetry.events - -import com.mapbox.android.telemetry.TelemetryUtils.obtainUniversalUniqueIdentifier - -internal class RerouteEvent( - override var sessionState: SessionState -) : TelemetryEvent { - override val eventId: String = obtainUniversalUniqueIdentifier() - var newRouteGeometry: String = "" - var newDurationRemaining: Int = 0 - var newDistanceRemaining: Int = 0 -} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/SessionState.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/SessionState.kt deleted file mode 100644 index 009dcc2f5b1..00000000000 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/SessionState.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.mapbox.navigation.core.telemetry.events - -import android.location.Location -import com.mapbox.api.directions.v5.models.DirectionsRoute -import com.mapbox.navigation.core.telemetry.obtainGeometry -import com.mapbox.navigation.core.telemetry.obtainStepCount -import java.util.Date - -internal data class SessionState( - var secondsSinceLastReroute: Int = -1, - var eventRouteProgress: MetricsRouteProgress = MetricsRouteProgress(null), - var eventLocation: Location = Location(MetricsLocation.PROVIDER).apply { - latitude = 0.0 - longitude = 0.0 - }, - var eventDate: Date? = null, - var eventRouteDistanceCompleted: Double = 0.0, - var afterEventLocations: List? = null, - var beforeEventLocations: List? = null, - var originalDirectionRoute: DirectionsRoute? = null, - var currentDirectionRoute: DirectionsRoute? = null, - var sessionIdentifier: String = "", - var tripIdentifier: String = "", - var originalRequestIdentifier: String? = null, - var requestIdentifier: String? = null, - var mockLocation: Boolean = false, - var rerouteCount: Int = 0, - var startTimestamp: Date? = null, - var arrivalTimestamp: Date? = null, - var locationEngineName: String = "", - var percentInForeground: Int = 100, - var percentInPortrait: Int = 100 -) { - - // TODO:OZ Clean up never used functions below - // We should also review SessionState properties as some of them are set but never used - // E.g. percentInForeground and percentInPortrait set twice - // see MapboxNavigationTelemetry#populateSessionState and MapboxNavigationTelemetry#populateNavigationEvent - // and never referenced in RerouteEvent / NavigationRerouteEvent - /* - * Original route values - */ - fun originalStepCount(): Int = obtainStepCount(originalDirectionRoute) - - fun originalGeometry(): String = obtainGeometry(originalDirectionRoute) - - fun originalDistance(): Int = originalDirectionRoute?.distance()?.toInt() ?: 0 - - fun originalDuration(): Int = originalDirectionRoute?.duration()?.toInt() ?: 0 - - /* - * Current route values - */ - fun currentStepCount(): Int = obtainStepCount(currentDirectionRoute) - - fun currentGeometry(): String = obtainGeometry(currentDirectionRoute) -} diff --git a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/TelemetryEvent.kt b/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/TelemetryEvent.kt deleted file mode 100644 index 62647b077c0..00000000000 --- a/libnavigation-core/src/main/java/com/mapbox/navigation/core/telemetry/events/TelemetryEvent.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.mapbox.navigation.core.telemetry.events - -internal interface TelemetryEvent { - val eventId: String - val sessionState: SessionState -} 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 adcb0c56de5..9bbd2e52f00 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 @@ -227,7 +227,7 @@ class MapboxNavigationTest { mapboxNavigation.onDestroy() - verify(exactly = 2) { tripSession.unregisterOffRouteObserver(any()) } + verify(exactly = 1) { tripSession.unregisterOffRouteObserver(any()) } mapboxNavigation.onDestroy() } diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/telemetry/LocationsCollectorTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/telemetry/LocationsCollectorTest.kt new file mode 100644 index 00000000000..4879f83e91d --- /dev/null +++ b/libnavigation-core/src/test/java/com/mapbox/navigation/core/telemetry/LocationsCollectorTest.kt @@ -0,0 +1,126 @@ +package com.mapbox.navigation.core.telemetry + +import android.location.Location +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +@ExperimentalCoroutinesApi +class LocationsCollectorTest { + + private val locationsCollector = LocationsCollectorImpl(null) + + @Test + fun ignoreEnhancedLocationUpdates() { + locationsCollector.onEnhancedLocationChanged(mockk(), mockk()) + + assertNull(locationsCollector.lastLocation) + } + + @Test + fun useRawLocationUpdates() { + val rawLocation: Location = mockk() + locationsCollector.onRawLocationChanged(rawLocation) + + assertEquals(rawLocation, locationsCollector.lastLocation) + } + + @Test + fun lastLocation() = runBlocking { + val firstLocation = mockk() + val secondLocation = mockk() + + locationsCollector.onRawLocationChanged(firstLocation) + assertEquals(firstLocation, locationsCollector.lastLocation) + + locationsCollector.onRawLocationChanged(secondLocation) + assertEquals(secondLocation, locationsCollector.lastLocation) + } + + @Test + fun preAndPostLocationsOrder() = runBlocking { + val preEventLocation = mockk() + val postEventLocation = mockk() + + locationsCollector.onRawLocationChanged(preEventLocation) + locationsCollector.collectLocations { preEventLocations, postEventLocations -> + assertEquals(1, preEventLocations.size) + assertEquals(preEventLocation, preEventLocations[0]) + + assertEquals(1, postEventLocations.size) + assertEquals(postEventLocation, postEventLocations[0]) + } + locationsCollector.onRawLocationChanged(postEventLocation) + locationsCollector.flushBuffers() + } + + @Test + fun preAndPostLocationsMaxSize() = runBlocking { + repeat(25) { locationsCollector.onRawLocationChanged(mockk()) } + locationsCollector.collectLocations { preEventLocations, postEventLocations -> + assertEquals(20, preEventLocations.size) + assertEquals(20, postEventLocations.size) + } + repeat(25) { locationsCollector.onRawLocationChanged(mockk()) } + locationsCollector.flushBuffers() + } + + @Test + fun prePostLocationsEvents() = runBlocking { + val l = mutableListOf() + repeat(42) { l.add(mockk()) } + + // before any location posted. preList will be empty. postList will have 20 items + locationsCollector.collectLocations { preLocations, postLocations -> + val preList = emptyList() + val postList = mutableListOf().apply { for (i in 0 until 20) add(l[i]) } + assertEquals(preList, preLocations) + assertEquals(postList, postLocations) + } + + for (i in 0 until 5) locationsCollector.onRawLocationChanged(l[i]) + + // 5 locations posted. preList will have all of them. postList will have 20 items + locationsCollector.collectLocations { preLocations, postLocations -> + val preList = mutableListOf().apply { for (i in 0 until 5) add(l[i]) } + val postList = mutableListOf().apply { for (i in 5 until 25) add(l[i]) } + assertEquals(preList, preLocations) + assertEquals(postList, postLocations) + } + + for (i in 5 until 17) locationsCollector.onRawLocationChanged(l[i]) + + // 17 locations posted. preList will have all of them. postList will have 20 items + locationsCollector.collectLocations { preLocations, postLocations -> + val preList = mutableListOf().apply { for (i in 0 until 17) add(l[i]) } + val postList = mutableListOf().apply { for (i in 17 until 37) add(l[i]) } + assertEquals(preList, preLocations) + assertEquals(postList, postLocations) + } + + for (i in 17 until 29) locationsCollector.onRawLocationChanged(l[i]) + + // 29 locations posted. preList will have the last 20. postList will have 13 items + locationsCollector.collectLocations { preLocations, postLocations -> + val preList = mutableListOf().apply { for (i in 9 until 29) add(l[i]) } + val postList = mutableListOf().apply { for (i in 29 until 42) add(l[i]) } + assertEquals(preList, preLocations) + assertEquals(postList, postLocations) + } + + for (i in 29 until 42) locationsCollector.onRawLocationChanged(l[i]) + + // 42 locations posted. preList will have the last 20. postList will be empty + locationsCollector.collectLocations { preLocations, postLocations -> + val preList = mutableListOf().apply { for (i in 22 until 42) add(l[i]) } + val postList = emptyList() + assertEquals(preList, preLocations) + assertEquals(postList, postLocations) + } + + locationsCollector.flushBuffers() + } +} 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 9ba2a2d9ce0..19b43eb36a8 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 @@ -1,98 +1,432 @@ package com.mapbox.navigation.core.telemetry +import android.app.ActivityManager import android.app.AlarmManager import android.content.Context import android.content.SharedPreferences +import android.location.Location +import android.media.AudioManager +import android.telephony.TelephonyManager +import com.mapbox.android.telemetry.AppUserTurnstile import com.mapbox.android.telemetry.MapboxTelemetryConstants import com.mapbox.api.directions.v5.models.DirectionsRoute +import com.mapbox.api.directions.v5.models.LegStep +import com.mapbox.api.directions.v5.models.RouteLeg +import com.mapbox.api.directions.v5.models.RouteOptions +import com.mapbox.api.directions.v5.models.StepManeuver +import com.mapbox.geojson.Point +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.LOCATION_TRACKING +import com.mapbox.navigation.base.trip.model.RouteProgressState.ROUTE_COMPLETE +import com.mapbox.navigation.base.trip.model.RouteStepProgress import com.mapbox.navigation.core.MapboxNavigation +import com.mapbox.navigation.core.NavigationSession +import com.mapbox.navigation.core.NavigationSession.State.ACTIVE_GUIDANCE +import com.mapbox.navigation.core.NavigationSession.State.FREE_DRIVE +import com.mapbox.navigation.core.NavigationSession.State.IDLE +import com.mapbox.navigation.core.telemetry.events.NavigationArriveEvent +import com.mapbox.navigation.core.telemetry.events.NavigationCancelEvent +import com.mapbox.navigation.core.telemetry.events.NavigationDepartEvent +import com.mapbox.navigation.core.telemetry.events.NavigationEvent +import com.mapbox.navigation.core.telemetry.events.NavigationFeedbackEvent +import com.mapbox.navigation.core.telemetry.events.NavigationRerouteEvent import com.mapbox.navigation.metrics.MapboxMetricsReporter -import com.mapbox.navigation.testing.MainCoroutineRule -import com.mapbox.navigation.utils.internal.ThreadController +import com.mapbox.navigation.metrics.internal.event.NavigationAppUserTurnstileEvent +import io.mockk.Runs +import io.mockk.coEvery import io.mockk.every +import io.mockk.just import io.mockk.mockk import io.mockk.mockkObject import io.mockk.unmockkObject import io.mockk.verify -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.InternalCoroutinesApi +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotSame +import junit.framework.TestCase.assertTrue import org.junit.After -import org.junit.Before -import org.junit.Rule import org.junit.Test -@InternalCoroutinesApi -@ExperimentalCoroutinesApi class MapboxNavigationTelemetryTest { - @get:Rule - var coroutineRule = MainCoroutineRule() + + private companion object { + private const val LAST_LOCATION_LAT = 55.5 + private const val LAST_LOCATION_LON = 88.8 + + private const val ORIGINAL_ROUTE_GEOMETRY = "" + private const val ORIGINAL_ROUTE_DISTANCE = 1.1 + private const val ORIGINAL_ROUTE_DURATION = 2.2 + private const val ORIGINAL_ROUTE_ROUTE_INDEX = "10" + + private const val ORIGINAL_ROUTE_OPTIONS_PROFILE = "original_profile" + private const val ORIGINAL_ROUTE_OPTIONS_REQUEST_UUID = "original_requestUuid" + + private const val ANOTHER_ROUTE_GEOMETRY = "" + private const val ANOTHER_ROUTE_ROUTE_INDEX = "1" + private const val ANOTHER_ROUTE_DISTANCE = 123.1 + private const val ANOTHER_ROUTE_DURATION = 235.2 + + private const val ANOTHER_ROUTE_OPTIONS_PROFILE = "progress_profile" + private const val ANOTHER_ROUTE_OPTIONS_REQUEST_UUID = "progress_requestUuid" + + private const val ROUTE_PROGRESS_DISTANCE_REMAINING = 11f + private const val ROUTE_PROGRESS_DURATION_REMAINING = 22.22 + private const val ROUTE_PROGRESS_DISTANCE_TRAVELED = 15f + + private const val ORIGINAL_STEP_MANEUVER_LOCATION_LATITUDE = 135.21 + private const val ORIGINAL_STEP_MANEUVER_LOCATION_LONGITUDE = 436.5 + private const val ANOTHER_STEP_MANEUVER_LOCATION_LATITUDE = 42.2 + private const val ANOTHER_STEP_MANEUVER_LOCATION_LONGITUDE = 12.4 + + private const val STEP_INDEX = 5 + private const val SDK_IDENTIFIER = "mapbox-navigation-android" + } private val context: Context = mockk(relaxed = true) private val applicationContext: Context = mockk(relaxed = true) private val mapboxNavigation = mockk(relaxed = true) private val navigationOptions: NavigationOptions = mockk(relaxed = true) - private val route: DirectionsRoute = mockk(relaxed = true) - private val token = "pk.token" + private val locationsCollector: LocationsCollector = mockk() + private val routeProgress = mockk() + private val originalRoute = mockk() + private val anotherRoute = mockk() + private val lastLocation = mockk() + private val originalRouteOptions = mockk() + private val anotherRouteOptions = mockk() + private val originalRouteLeg = mockk() + private val anotherRouteLeg = mockk() + private val originalRouteStep = mockk() + private val anotherRouteStep = mockk() + private val originalRouteSteps = listOf(originalRouteStep) + private val progressRouteSteps = listOf(anotherRouteStep) + private val originalRouteLegs = listOf(originalRouteLeg) + private val progressRouteLegs = listOf(anotherRouteLeg) + private val originalStepManeuver = mockk() + private val anotherStepManeuver = mockk() + private val originalStepManeuverLocation = mockk() + private val anotherStepManeuverLocation = mockk() + private val legProgress = mockk() + private val stepProgress = mockk() - @Before - fun setup() { - every { mapboxNavigation.getRoutes() } answers { listOf(route) } + @After + fun cleanUp() { + updateSessionState(IDLE) + unmockkObject(MapboxMetricsReporter) + } - mockkObject(ThreadController) + @Test + fun turnstileEvent_sent_on_telemetry_init() { + baseMock() - val alarmManager = mockk() - every { - applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager - } returns alarmManager - every { context.applicationContext } returns applicationContext + initTelemetry() - val sharedPreferences = mockk(relaxed = true) - every { - applicationContext.getSharedPreferences( - MapboxTelemetryConstants.MAPBOX_SHARED_PREFERENCES, - Context.MODE_PRIVATE - ) - } returns sharedPreferences - every { - sharedPreferences.getString("mapboxTelemetryState", "ENABLED") - } returns "DISABLED" + val events = captureAndVerifyMetricsReporter(exactly = 1) + assertTrue(events[0] is NavigationAppUserTurnstileEvent) + } + + @Test + fun turnstileEvent_populated_correctly() { + baseMock() + val events = captureMetricsReporter() + + initTelemetry() + + val actualEvent = events[0] as NavigationAppUserTurnstileEvent + val expectedTurnstileEvent = AppUserTurnstile("mock", "mock").also { it.setSkuId("08") } + assertEquals(expectedTurnstileEvent.skuId, actualEvent.event.skuId) + } + + @Test + fun departEvent_sent_on_active_guidance_when_route_and_routeProgress_available() { + baseMock() + + baseInitialization() + + val events = captureAndVerifyMetricsReporter(exactly = 2) + assertTrue(events[0] is NavigationAppUserTurnstileEvent) + assertTrue(events[1] is NavigationDepartEvent) + } + + @Test + fun departEvent_not_sent_without_route_and_routeProgress() { + baseMock() + + initTelemetry() + updateSessionState(ACTIVE_GUIDANCE) + + val events = captureAndVerifyMetricsReporter(exactly = 1) + assertTrue(events[0] is NavigationAppUserTurnstileEvent) + } + + @Test + fun departEvent_not_sent_without_route() { + baseMock() - MapboxMetricsReporter.init(context, token, "userAgent") + initTelemetry() + updateSessionState(ACTIVE_GUIDANCE) + updateRouteProgress(routeProgress) + + val events = captureAndVerifyMetricsReporter(exactly = 1) + assertTrue(events[0] is NavigationAppUserTurnstileEvent) + } + + @Test + fun departEvent_not_sent_without_routeProgress() { + baseMock() + + initTelemetry() + updateSessionState(ACTIVE_GUIDANCE) + updateRoute(originalRoute) + + val events = captureAndVerifyMetricsReporter(exactly = 1) + assertTrue(events[0] is NavigationAppUserTurnstileEvent) + } + + @Test + fun cancelEvent_sent_on_active_guidance_stop() { + baseMock() + + baseInitialization() + updateSessionState(FREE_DRIVE) + + val events = captureAndVerifyMetricsReporter(exactly = 3) + assertTrue(events[0] is NavigationAppUserTurnstileEvent) + assertTrue(events[1] is NavigationDepartEvent) + assertTrue(events[2] is NavigationCancelEvent) + assertEquals(3, events.size) + verify { locationsCollector.flushBuffers() } + } + + @Test + fun arriveEvent_sent_on_arrival() { + baseMock() + mockRouteProgress() + every { routeProgress.currentState } returns ROUTE_COMPLETE + + baseInitialization() + updateRouteProgress(routeProgress) + + val events = captureAndVerifyMetricsReporter(exactly = 3) + assertTrue(events[0] is NavigationAppUserTurnstileEvent) + assertTrue(events[1] is NavigationDepartEvent) + assertTrue(events[2] is NavigationArriveEvent) + assertEquals(3, events.size) + } + + @Test + fun cancel_and_depart_events_sent_on_external_route() { + baseMock() + mockAnotherRoute() + + baseInitialization() + updateRoute(anotherRoute) + updateRouteProgress(routeProgress) + + val events = captureAndVerifyMetricsReporter(exactly = 4) + assertTrue(events[0] is NavigationAppUserTurnstileEvent) + assertTrue(events[1] is NavigationDepartEvent) + assertTrue(events[2] is NavigationCancelEvent) + assertTrue(events[3] is NavigationDepartEvent) + assertEquals(4, events.size) + } + + @Test + fun depart_event_not_sent_on_external_route_without_route_progress() { + baseMock() + mockAnotherRoute() + + baseInitialization() + updateRoute(anotherRoute) + + val events = captureAndVerifyMetricsReporter(exactly = 3) + assertTrue(events[0] is NavigationAppUserTurnstileEvent) + assertTrue(events[1] is NavigationDepartEvent) + assertTrue(events[2] is NavigationCancelEvent) + assertEquals(3, events.size) + } + + @Test + fun depart_events_are_different_on_external_route() { + baseMock() + mockAnotherRoute() + val events = captureMetricsReporter() + + baseInitialization() + updateRoute(anotherRoute) + updateRouteProgress(routeProgress) + + val firstDepart = events[1] as NavigationDepartEvent + val secondDepart = events[3] as NavigationDepartEvent + assertNotSame(firstDepart.originalEstimatedDistance, secondDepart.originalEstimatedDistance) + assertNotSame(firstDepart.originalRequestIdentifier, secondDepart.originalRequestIdentifier) + } + + @Test + fun feedback_and_reroute_events_not_sent_on_arrival() { + baseMock() + mockRouteProgress() + every { routeProgress.currentState } returns ROUTE_COMPLETE + + baseInitialization() + updateRouteProgress(routeProgress) + postUserFeedback() + offRoute() + postUserFeedback() + + val events = captureAndVerifyMetricsReporter(exactly = 3) + assertTrue(events[0] is NavigationAppUserTurnstileEvent) + assertTrue(events[1] is NavigationDepartEvent) + assertTrue(events[2] is NavigationArriveEvent) + assertEquals(3, events.size) + } + + @Test + fun feedback_and_reroute_events_sent_on_free_drive() { + baseMock() + mockAnotherRoute() + + baseInitialization() + postUserFeedback() + offRoute() + updateRoute(anotherRoute) + postUserFeedback() + postUserFeedback() + postUserFeedback() + postUserFeedback() + updateSessionState(FREE_DRIVE) + + val events = captureAndVerifyMetricsReporter(exactly = 9) + assertTrue(events[0] is NavigationAppUserTurnstileEvent) + assertTrue(events[1] is NavigationDepartEvent) + assertTrue(events[2] is NavigationFeedbackEvent) + assertTrue(events[3] is NavigationRerouteEvent) + assertTrue(events[4] is NavigationFeedbackEvent) + assertTrue(events[5] is NavigationFeedbackEvent) + assertTrue(events[6] is NavigationFeedbackEvent) + assertTrue(events[7] is NavigationFeedbackEvent) + assertTrue(events[8] is NavigationCancelEvent) + assertEquals(9, events.size) + } + + @Test + fun feedback_and_reroute_events_sent_on_idle_state() { + baseMock() + mockAnotherRoute() + + baseInitialization() + postUserFeedback() + postUserFeedback() + postUserFeedback() + postUserFeedback() + postUserFeedback() + offRoute() + updateRoute(anotherRoute) + postUserFeedback() + updateSessionState(IDLE) + + val events = captureAndVerifyMetricsReporter(exactly = 10) + assertTrue(events[0] is NavigationAppUserTurnstileEvent) + assertTrue(events[1] is NavigationDepartEvent) + assertTrue(events[2] is NavigationFeedbackEvent) + assertTrue(events[3] is NavigationFeedbackEvent) + assertTrue(events[4] is NavigationFeedbackEvent) + assertTrue(events[5] is NavigationFeedbackEvent) + assertTrue(events[6] is NavigationFeedbackEvent) + assertTrue(events[7] is NavigationRerouteEvent) + assertTrue(events[8] is NavigationFeedbackEvent) + assertTrue(events[9] is NavigationCancelEvent) + assertEquals(10, events.size) + } + + @Test + fun rerouteEvent_sent_on_offRoute() { + baseMock() + mockAnotherRoute() + mockRouteProgress() + + baseInitialization() + updateRouteProgress(routeProgress) + offRoute() + updateRoute(anotherRoute) + locationsCollector.flushBuffers() + + val events = captureAndVerifyMetricsReporter(exactly = 3) + assertTrue(events[0] is NavigationAppUserTurnstileEvent) + assertTrue(events[1] is NavigationDepartEvent) + assertTrue(events[2] is NavigationRerouteEvent) + assertEquals(3, events.size) + } + + @Test + fun departEvent_populated_correctly() { + baseMock() + val events = captureMetricsReporter() + + baseInitialization() + + val departEvent = events[1] as NavigationDepartEvent + checkOriginalParams(departEvent, originalRoute) + } + + @Test + fun rerouteEvent_populated_correctly() { + baseMock() + mockAnotherRoute() + mockRouteProgress() + every { routeProgress.route } returns anotherRoute + val events = captureMetricsReporter() + + baseInitialization() + updateRouteProgress(routeProgress) + offRoute() + updateRoute(anotherRoute) + locationsCollector.flushBuffers() + + val rerouteEvent = events[2] as NavigationRerouteEvent + checkOriginalParams(rerouteEvent, anotherRoute) } @Test fun onInit_registerRouteProgressObserver_called() { + baseMock() + onInit { verify(exactly = 1) { mapboxNavigation.registerRouteProgressObserver(any()) } } } @Test fun onInit_registerLocationObserver_called() { + baseMock() + onInit { verify(exactly = 1) { mapboxNavigation.registerLocationObserver(any()) } } } @Test fun onInit_registerRoutesObserver_called() { + baseMock() + onInit { verify(exactly = 1) { mapboxNavigation.registerRoutesObserver(any()) } } } @Test fun onInit_registerOffRouteObserver_called() { + baseMock() + onInit { verify(exactly = 1) { mapboxNavigation.registerOffRouteObserver(any()) } } } @Test fun onInit_registerNavigationSessionObserver_called() { - onInit { verify(exactly = 1) { mapboxNavigation.registerNavigationSessionObserver(any()) } } - } + baseMock() - @Test - fun onInit_getRoutes_called() { - onInit { verify(exactly = 1) { mapboxNavigation.getRoutes() } } + onInit { verify(exactly = 1) { mapboxNavigation.registerNavigationSessionObserver(any()) } } } @Test fun onUnregisterListener_unregisterRouteProgressObserver_called() { + baseMock() + onUnregister { verify(exactly = 1) { mapboxNavigation.unregisterRouteProgressObserver(any()) } } @@ -100,32 +434,38 @@ class MapboxNavigationTelemetryTest { @Test fun onUnregisterListener_unregisterLocationObserver_called() { + baseMock() + onUnregister { verify(exactly = 1) { mapboxNavigation.unregisterLocationObserver(any()) } } } @Test fun onUnregisterListener_unregisterRoutesObserver_called() { + baseMock() + onUnregister { verify(exactly = 1) { mapboxNavigation.unregisterRoutesObserver(any()) } } } @Test fun onUnregisterListener_unregisterOffRouteObserver_called() { + baseMock() + onUnregister { verify(exactly = 1) { mapboxNavigation.unregisterOffRouteObserver(any()) } } } @Test fun onUnregisterListener_unregisterNavigationSessionObserver_called() { + baseMock() + onUnregister { - verify(exactly = 1) { - mapboxNavigation.unregisterNavigationSessionObserver( - any() - ) - } + verify(exactly = 1) { mapboxNavigation.unregisterNavigationSessionObserver(any()) } } } @Test fun after_unregister_onInit_registers_all_listeners_again() { + baseMock() + initTelemetry() resetTelemetry() initTelemetry() @@ -135,39 +475,163 @@ class MapboxNavigationTelemetryTest { verify(exactly = 2) { mapboxNavigation.registerRoutesObserver(any()) } verify(exactly = 2) { mapboxNavigation.registerOffRouteObserver(any()) } verify(exactly = 2) { mapboxNavigation.registerNavigationSessionObserver(any()) } - verify(exactly = 2) { mapboxNavigation.getRoutes() } resetTelemetry() } - @Test - fun onInitTwice_unregisters_all_listeners() { - initTelemetry() + private fun baseInitialization() { initTelemetry() + updateSessionState(ACTIVE_GUIDANCE) + updateRoute(originalRoute) + updateRouteProgress(routeProgress) + } - verify(exactly = 1) { mapboxNavigation.unregisterRouteProgressObserver(any()) } - verify(exactly = 1) { mapboxNavigation.unregisterLocationObserver(any()) } - verify(exactly = 1) { mapboxNavigation.unregisterRoutesObserver(any()) } - verify(exactly = 1) { mapboxNavigation.unregisterOffRouteObserver(any()) } - verify(exactly = 1) { mapboxNavigation.unregisterNavigationSessionObserver(any()) } + private fun updateSessionState(state: NavigationSession.State) { + MapboxNavigationTelemetry.onNavigationSessionStateChanged(state) + } - resetTelemetry() + private fun updateRoute(route: DirectionsRoute) { + MapboxNavigationTelemetry.onRoutesChanged(listOf(route)) } - @After - fun cleanUp() { - unmockkObject(ThreadController) + private fun updateRouteProgress(routeProgress: RouteProgress) { + MapboxNavigationTelemetry.onRouteProgressChanged(routeProgress) + } + + private fun offRoute() { + MapboxNavigationTelemetry.onOffRouteStateChanged(true) + } + + private fun captureMetricsReporter(): List { + val events = mutableListOf() + every { MapboxMetricsReporter.addEvent(capture(events)) } just Runs + return events + } + + private fun captureAndVerifyMetricsReporter(exactly: Int): List { + val events = mutableListOf() + verify(exactly = exactly) { MapboxMetricsReporter.addEvent(capture(events)) } + return events + } + + private fun baseMock() { + mockMetricsReporter() + mockContext() + mockTelemetryUtils() + + mockLocationCollector() + mockOriginalRoute() + mockRouteProgress() + } + + private fun mockOriginalRoute() { + every { originalRoute.geometry() } returns ORIGINAL_ROUTE_GEOMETRY + every { originalRoute.legs() } returns originalRouteLegs + every { originalRoute.distance() } returns ORIGINAL_ROUTE_DISTANCE + every { originalRoute.duration() } returns ORIGINAL_ROUTE_DURATION + every { originalRoute.routeOptions() } returns originalRouteOptions + every { originalRoute.routeIndex() } returns ORIGINAL_ROUTE_ROUTE_INDEX + every { originalRouteOptions.profile() } returns ORIGINAL_ROUTE_OPTIONS_PROFILE + every { originalRouteLeg.steps() } returns originalRouteSteps + every { originalRouteStep.maneuver() } returns originalStepManeuver + every { originalStepManeuver.location() } returns originalStepManeuverLocation + every { originalStepManeuverLocation.latitude() } returns + ORIGINAL_STEP_MANEUVER_LOCATION_LATITUDE + every { originalStepManeuverLocation.longitude() } returns + ORIGINAL_STEP_MANEUVER_LOCATION_LONGITUDE + every { originalRouteOptions.requestUuid() } returns + ORIGINAL_ROUTE_OPTIONS_REQUEST_UUID + } + + private fun mockAnotherRoute() { + every { anotherRoute.geometry() } returns ANOTHER_ROUTE_GEOMETRY + every { anotherRoute.distance() } returns ANOTHER_ROUTE_DISTANCE + every { anotherRoute.duration() } returns ANOTHER_ROUTE_DURATION + every { anotherRoute.legs() } returns progressRouteLegs + every { anotherRoute.routeIndex() } returns ANOTHER_ROUTE_ROUTE_INDEX + every { anotherRoute.routeOptions() } returns anotherRouteOptions + every { anotherRouteOptions.profile() } returns ANOTHER_ROUTE_OPTIONS_PROFILE + every { anotherRouteOptions.requestUuid() } returns ANOTHER_ROUTE_OPTIONS_REQUEST_UUID + every { anotherRouteLeg.steps() } returns progressRouteSteps + every { anotherRouteStep.maneuver() } returns anotherStepManeuver + every { anotherStepManeuver.location() } returns anotherStepManeuverLocation + every { anotherStepManeuverLocation.latitude() } returns + ANOTHER_STEP_MANEUVER_LOCATION_LATITUDE + every { anotherStepManeuverLocation.longitude() } returns + ANOTHER_STEP_MANEUVER_LOCATION_LONGITUDE + } + + private fun mockRouteProgress() { + every { routeProgress.route } returns originalRoute + every { routeProgress.currentState } returns LOCATION_TRACKING + every { routeProgress.currentLegProgress } returns legProgress + every { routeProgress.distanceRemaining } returns ROUTE_PROGRESS_DISTANCE_REMAINING + every { routeProgress.durationRemaining } returns ROUTE_PROGRESS_DURATION_REMAINING + every { routeProgress.distanceTraveled } returns ROUTE_PROGRESS_DISTANCE_TRAVELED + every { legProgress.currentStepProgress } returns stepProgress + every { legProgress.upcomingStep } returns null + every { legProgress.legIndex } returns 0 + every { legProgress.routeLeg } returns null + every { stepProgress.stepIndex } returns STEP_INDEX + every { stepProgress.step } returns null + every { stepProgress.distanceRemaining } returns 0f + every { stepProgress.durationRemaining } returns 0.0 + } + + private fun mockMetricsReporter() { + initMapboxMetricsReporter() + mockkObject(MapboxMetricsReporter) + every { MapboxMetricsReporter.addEvent(any()) } just Runs + } + + private fun mockContext() { + every { navigationOptions.applicationContext } returns applicationContext + every { context.applicationContext } returns applicationContext + } + + private fun mockLocationCollector() { + coEvery { locationsCollector.flushBuffers() } just Runs + every { locationsCollector.lastLocation } returns lastLocation + every { lastLocation.latitude } returns LAST_LOCATION_LAT + every { lastLocation.longitude } returns LAST_LOCATION_LON + + val onBufferFull = mutableListOf<(List, List) -> Unit>() + every { locationsCollector.collectLocations(capture(onBufferFull)) } just Runs + every { locationsCollector.flushBuffers() } answers { + onBufferFull.forEach { it.invoke(listOf(), listOf()) } + } + } + + private fun mockTelemetryUtils() { + val audioManager = mockk() + every { + applicationContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager + } returns audioManager + every { audioManager.getStreamVolume(any()) } returns 1 + every { audioManager.getStreamMaxVolume(any()) } returns 2 + every { audioManager.isBluetoothScoOn } returns true + + val telephonyManager = mockk() + every { + applicationContext.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + } returns telephonyManager + every { telephonyManager.dataNetworkType } returns 5 + every { telephonyManager.networkType } returns 6 + + val activityManager = mockk() + every { + applicationContext.getSystemService(Context.ACTIVITY_SERVICE) + } returns activityManager + every { activityManager.runningAppProcesses } returns listOf() } private fun initTelemetry() { MapboxNavigationTelemetry.initialize( - context, mapboxNavigation, - MapboxMetricsReporter, - "locationEngine", - ThreadController.getMainScopeAndRootJob(), navigationOptions, - "userAgent" + MapboxMetricsReporter, + mockk(relaxed = true), + locationsCollector ) } @@ -186,4 +650,79 @@ class MapboxNavigationTelemetryTest { resetTelemetry() block() } + + private fun postUserFeedback() { + MapboxNavigationTelemetry.postUserFeedback("", "", "", null, null, null) + } + + /** + * Inside MapboxNavigationTelemetry.initialize method we call postTurnstileEvent and build + * AppUserTurnstile. It checks a static context field inside MapboxTelemetry. + * To set that context field we need to init MapboxTelemetry. + * It is done inside MapboxMetricsReporter. + * After that method we mock MapboxMetricsReporter to use it in tests. + */ + private fun initMapboxMetricsReporter() { + val alarmManager = mockk() + every { + applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager + } returns alarmManager + every { context.applicationContext } returns applicationContext + + val sharedPreferences = mockk(relaxed = true) + every { + applicationContext.getSharedPreferences( + MapboxTelemetryConstants.MAPBOX_SHARED_PREFERENCES, + Context.MODE_PRIVATE + ) + } returns sharedPreferences + every { + sharedPreferences.getString("mapboxTelemetryState", "ENABLED") + } returns "DISABLED" + + MapboxMetricsReporter.init(context, "pk.token", "userAgent") + } + + private fun checkOriginalParams(event: NavigationEvent, currentRoute: DirectionsRoute) { + assertEquals(SDK_IDENTIFIER, event.sdkIdentifier) + assertEquals(obtainStepCount(originalRoute), event.originalStepCount) + assertEquals(originalRoute.distance().toInt(), event.originalEstimatedDistance) + assertEquals(originalRoute.duration().toInt(), event.originalEstimatedDuration) + assertEquals(originalRoute.routeOptions()?.requestUuid(), event.originalRequestIdentifier) + assertEquals(originalRoute.geometry(), event.originalGeometry) + assertEquals(locationsCollector.lastLocation?.latitude, event.lat) + assertEquals(locationsCollector.lastLocation?.longitude, event.lng) + assertEquals(false, event.simulation) + assertEquals(7, event.eventVersion) + + assertEquals( + routeProgress.currentLegProgress?.currentStepProgress?.stepIndex, + event.stepIndex + ) + 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) + assertEquals(obtainStepCount(currentRoute), event.stepCount) + assertEquals(currentRoute.legs()?.size, event.legCount) + + if (event is NavigationRerouteEvent) { + assertEquals(anotherRoute.distance().toInt(), event.newDistanceRemaining) + assertEquals(anotherRoute.duration().toInt(), event.newDurationRemaining) + assertEquals(anotherRoute.geometry(), event.newGeometry) + assertEquals(1, event.rerouteCount) + } else { + assertEquals(0, event.rerouteCount) + } + + assertEquals( + obtainAbsoluteDistance(lastLocation, obtainRouteDestination(currentRoute)), + event.absoluteDistanceToDestination + ) + assertEquals(currentRoute.distance().toInt(), event.estimatedDistance) + assertEquals(currentRoute.duration().toInt(), event.estimatedDuration) + assertEquals(obtainStepCount(currentRoute), event.totalStepCount) + } } diff --git a/libnavigation-core/src/test/java/com/mapbox/navigation/core/telemetry/TelemetryLocationAndProgressDispatcherTest.kt b/libnavigation-core/src/test/java/com/mapbox/navigation/core/telemetry/TelemetryLocationAndProgressDispatcherTest.kt deleted file mode 100644 index 1ce83d21e62..00000000000 --- a/libnavigation-core/src/test/java/com/mapbox/navigation/core/telemetry/TelemetryLocationAndProgressDispatcherTest.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.mapbox.navigation.core.telemetry - -import android.location.Location -import com.mapbox.navigation.testing.MainCoroutineRule -import com.mapbox.navigation.utils.internal.ThreadController -import io.mockk.mockk -import kotlinx.coroutines.ExperimentalCoroutinesApi -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNull -import org.junit.Rule -import org.junit.Test - -@ExperimentalCoroutinesApi -class TelemetryLocationAndProgressDispatcherTest { - @get:Rule - var coroutineRule = MainCoroutineRule() - - private val locationAndProgressDispatcher = TelemetryLocationAndProgressDispatcher( - ThreadController.getMainScopeAndRootJob().scope - ) - - @Test - fun ignoreEnhancedLocationUpdates() { - val enhancedLocation: Location = mockk() - locationAndProgressDispatcher.onEnhancedLocationChanged(enhancedLocation, mockk()) - - assertFalse( - "this job should not be completed", - locationAndProgressDispatcher.getFirstLocationAsync().isCompleted - ) - assertNull(locationAndProgressDispatcher.getLastLocation()) - } - - @Test - fun useRawLocationUpdates() { - val rawLocation: Location = mockk() - locationAndProgressDispatcher.onRawLocationChanged(rawLocation) - - assertEquals( - rawLocation, - locationAndProgressDispatcher.getFirstLocationAsync().getCompleted() - ) - assertEquals(rawLocation, locationAndProgressDispatcher.getLastLocation()) - } -}