Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.embrace.android.embracesdk.internal.arch.InstrumentationArgs
import io.embrace.android.embracesdk.internal.arch.SessionPartChangeListener
import io.embrace.android.embracesdk.internal.arch.SessionPartEndListener
import io.embrace.android.embracesdk.internal.arch.datasource.TelemetryDestination
import io.embrace.android.embracesdk.internal.arch.navigation.NavigationTrackingService
import io.embrace.android.embracesdk.internal.arch.state.AppStateTracker
import io.embrace.android.embracesdk.internal.capture.session.UserSessionPropertiesService
import io.embrace.android.embracesdk.internal.clock.Clock
Expand Down Expand Up @@ -35,6 +36,7 @@ internal class InstrumentationArgsImpl(
override val ordinalStore: OrdinalStore,
override val processIdentifier: String,
override val appStateTracker: AppStateTracker,
override val navigationTrackingService: NavigationTrackingService,
override val telemetryService: TelemetryService,
private val workerThreadModule: WorkerThreadModule,
private val sessionPartTracker: SessionPartTracker,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.embrace.android.embracesdk.internal.injection

import io.embrace.android.embracesdk.internal.arch.datasource.TelemetryDestination
import io.embrace.android.embracesdk.internal.arch.navigation.NavigationTrackingService
import io.embrace.android.embracesdk.internal.arch.state.AppStateTracker
import io.embrace.android.embracesdk.internal.capture.connectivity.NetworkConnectivityService
import io.embrace.android.embracesdk.internal.capture.session.UserSessionPropertiesService
Expand All @@ -13,6 +14,7 @@ import io.embrace.android.embracesdk.internal.session.id.SessionPartTracker
*/
interface EssentialServiceModule {
val appStateTracker: AppStateTracker
val navigationTrackingService: NavigationTrackingService
val userService: UserService
val networkConnectivityService: NetworkConnectivityService
val sessionPartTracker: SessionPartTracker
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import io.embrace.android.embracesdk.internal.arch.datasource.TelemetryDestination
import io.embrace.android.embracesdk.internal.arch.destination.TelemetryDestinationImpl
import io.embrace.android.embracesdk.internal.arch.navigation.NavigationTrackingService
import io.embrace.android.embracesdk.internal.arch.state.AppStateTracker
import io.embrace.android.embracesdk.internal.capture.connectivity.EmbraceNetworkConnectivityService
import io.embrace.android.embracesdk.internal.capture.connectivity.NetworkCallbackConnectivityService
Expand All @@ -15,6 +16,7 @@ import io.embrace.android.embracesdk.internal.capture.session.UserSessionPropert
import io.embrace.android.embracesdk.internal.capture.user.EmbraceUserService
import io.embrace.android.embracesdk.internal.capture.user.UserService
import io.embrace.android.embracesdk.internal.config.ConfigService
import io.embrace.android.embracesdk.internal.navigation.NavigationTrackingServiceImpl
import io.embrace.android.embracesdk.internal.session.id.SessionPartTracker
import io.embrace.android.embracesdk.internal.session.id.SessionPartTrackerImpl
import io.embrace.android.embracesdk.internal.session.lifecycle.AppStateTrackerImpl
Expand All @@ -39,6 +41,10 @@ class EssentialServiceModuleImpl(
}
}

override val navigationTrackingService: NavigationTrackingService by singleton {
NavigationTrackingServiceImpl()
}

override val userService: UserService by singleton {
EmbTrace.trace("user-service-init") {
EmbraceUserService(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class InstrumentationModuleImpl(
processIdentifier = openTelemetryModule.otelSdkConfig.processIdentifier,
crashMarkerFileProvider = { storageService.getFileForWrite("embrace_crash_marker") },
appStateTracker = essentialServiceModule.appStateTracker,
navigationTrackingService = essentialServiceModule.navigationTrackingService,
telemetryService = initModule.telemetryService,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.embrace.android.embracesdk.internal.navigation

import android.app.Activity
import io.embrace.android.embracesdk.internal.arch.navigation.NavigationControllerEventListener
import io.embrace.android.embracesdk.internal.arch.navigation.NavigationTrackingInitListener
import io.embrace.android.embracesdk.internal.arch.navigation.NavigationTrackingService

internal class NavigationTrackingServiceImpl : NavigationTrackingService {

private var navigationTrackingInitListener: NavigationTrackingInitListener = NoopNavigationTrackingInitListener
private var navigationControllerEventListener: NavigationControllerEventListener = NoopNavigationControllerEventListener

override fun setTrackingInitListener(listener: NavigationTrackingInitListener) {
navigationTrackingInitListener = listener
}

override fun setControllerEventListener(listener: NavigationControllerEventListener) {
navigationControllerEventListener = listener
}

override fun trackNavigation(activity: Activity, controller: Any?) {
navigationTrackingInitListener.trackNavigation(activity, controller)
}

override fun onControllerAttached(activity: Activity, timestampMs: Long) {
navigationControllerEventListener.onControllerAttached(activity, timestampMs)
}

override fun onDestinationChange(activity: Activity, screenName: String, timestampMs: Long) {
navigationControllerEventListener.onDestinationChange(activity, screenName, timestampMs)
}
}

private object NoopNavigationTrackingInitListener : NavigationTrackingInitListener {
override fun trackNavigation(activity: Activity, controller: Any?) {}
}

private object NoopNavigationControllerEventListener : NavigationControllerEventListener {
override fun onControllerAttached(activity: Activity, timestampMs: Long) {}
override fun onDestinationChange(activity: Activity, screenName: String, timestampMs: Long) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ class FakeInstrumentationArgs(

override val crashMarkerFile: File by lazy { File.createTempFile("crash_marker", "") }

override val navigationTrackingService = FakeNavigationTrackingService()

override fun registerSessionPartChangeListener(listener: SessionPartChangeListener) {
sessionChangeListeners.add(listener)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.embrace.android.embracesdk.fakes

import android.app.Activity
import io.embrace.android.embracesdk.internal.arch.navigation.NavigationControllerEventListener
import io.embrace.android.embracesdk.internal.arch.navigation.NavigationTrackingInitListener
import io.embrace.android.embracesdk.internal.arch.navigation.NavigationTrackingService

class FakeNavigationTrackingService : NavigationTrackingService {
val attachedCalls = mutableListOf<AttachedCall>()
val destinationChangedCalls = mutableListOf<DestinationChangedCall>()

override fun setTrackingInitListener(listener: NavigationTrackingInitListener) {}
override fun setControllerEventListener(listener: NavigationControllerEventListener) {}
override fun trackNavigation(activity: Activity, controller: Any?) {}

override fun onControllerAttached(activity: Activity, timestampMs: Long) {
attachedCalls.add(AttachedCall(activity, timestampMs))
}

override fun onDestinationChange(activity: Activity, screenName: String, timestampMs: Long) {
destinationChangedCalls.add(DestinationChangedCall(activity, screenName, timestampMs))
}

data class AttachedCall(val activity: Activity, val timestampMs: Long)
data class DestinationChangedCall(val activity: Activity, val screenName: String, val timestampMs: Long)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.embrace.android.embracesdk.internal.arch
import android.app.Application
import android.content.Context
import io.embrace.android.embracesdk.internal.arch.datasource.TelemetryDestination
import io.embrace.android.embracesdk.internal.arch.navigation.NavigationTrackingService
import io.embrace.android.embracesdk.internal.arch.state.AppStateTracker
import io.embrace.android.embracesdk.internal.clock.Clock
import io.embrace.android.embracesdk.internal.config.ConfigService
Expand Down Expand Up @@ -115,6 +116,11 @@ interface InstrumentationArgs {
*/
val crashMarkerFile: File

/**
* Service where navigation controllers and navigation event listeners can be registered
*/
val navigationTrackingService: NavigationTrackingService

/**
* Sets a listener that is invoked after a session changes.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.embrace.android.embracesdk.internal.arch.navigation

import android.app.Activity

/**
* Listener that receives events related to components that control navigation
*/
interface NavigationControllerEventListener {
/**
* Called when a component that controls navigation is attached
*/
fun onControllerAttached(activity: Activity, timestampMs: Long)

/**
* Called when a navigation component attached to the given activity updates its destination
*/
fun onDestinationChange(activity: Activity, screenName: String, timestampMs: Long)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.embrace.android.embracesdk.internal.arch.navigation

import android.app.Activity

/**
* Receives events related to the initialization of components that control navigation
*/
interface NavigationTrackingInitListener {
/**
* Track navigation events from a controller associated with the given Activity. If the controller is null, the implementation
* will try to find the controller within the given activity.
*
* The implementation is responsible for casting the controller to whatever interface it needs in order to do its tracking,
* as the interface is generic and agnostic to any navigation controller implementation details.
*/
fun trackNavigation(activity: Activity, controller: Any? = null)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What type do we expect the controller to be? Is there any way to make it more specific than Any??

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, it's NavController. And I'd be happy to have that explicitly until there's another type, but then that means we'll have to expose this Compose Navigation dependency at the public API level.

I started out with way, but then went to this generic method. It'll be more obvious in my next PR.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.embrace.android.embracesdk.internal.arch.navigation

/**
* Service where navigation controllers can be registered, and when they fire events, they will be dispatched to the listeners.
*/
interface NavigationTrackingService : NavigationTrackingInitListener, NavigationControllerEventListener {
/**
* Register listener that receives events related to the initialization of components that control navigation
*/
fun setTrackingInitListener(listener: NavigationTrackingInitListener)

/**
* Register listener that receives events related to components that control navigation
*/
fun setControllerEventListener(listener: NavigationControllerEventListener)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ android {
}

dependencies {
implementation(project(":embrace-android-instrumentation-api"))
implementation(project(":embrace-android-instrumentation-navigation"))
implementation(libs.androidx.navigation.fragment)
implementation(libs.androidx.navigation.common)
testImplementation(project(":embrace-android-instrumentation-api-fakes"))
testImplementation(libs.robolectric)
testImplementation(libs.androidx.navigation.testing)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.embrace.android.embracesdk.internal.instrumentation.compose.navigation

import io.embrace.android.embracesdk.internal.arch.InstrumentationArgs
import io.embrace.android.embracesdk.internal.arch.InstrumentationProvider
import io.embrace.android.embracesdk.internal.arch.datasource.DataSourceState
import io.embrace.android.embracesdk.internal.arch.navigation.NavigationTrackingService

/**
* Instrumentation provider that creates and registers [NavControllerTracker] with the [NavigationTrackingService].
*/
class ComposeNavigationInstrumentationProvider : InstrumentationProvider {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is a good way to to use the data source interface because it doesn't actually provide a datasource, but it allows for the dynamic loading of the compose navigation tracking via a new module. Is there a better way to do this?


override fun register(args: InstrumentationArgs): DataSourceState<*>? {
val tracker = NavControllerTracker(args.navigationTrackingService, args.clock, args.logger)
args.navigationTrackingService.setTrackingInitListener(tracker)
return null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package io.embrace.android.embracesdk.internal.instrumentation.compose.navigation

import android.app.Activity
import androidx.fragment.app.FragmentActivity
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.fragment.NavHostFragment
import io.embrace.android.embracesdk.internal.arch.navigation.NavigationTrackingInitListener
import io.embrace.android.embracesdk.internal.arch.navigation.NavigationTrackingService
import io.embrace.android.embracesdk.internal.clock.Clock
import io.embrace.android.embracesdk.internal.logging.InternalErrorType
import io.embrace.android.embracesdk.internal.logging.InternalLogger

/**
* Discovers and attaches [NavController.OnDestinationChangedListener] instances to Activities that use a [NavController].
* Implements [NavigationTrackingInitListener] so it can be registered with [NavigationTrackingService] tracking events.
*/
internal class NavControllerTracker(
private val navigationTrackingService: NavigationTrackingService,
private val clock: Clock,
private val logger: InternalLogger,
) : NavigationTrackingInitListener {

private val trackAttemptStatus = mutableMapOf<Int, Boolean>()

override fun trackNavigation(activity: Activity, controller: Any?) {
runCatching {
val activityId = System.identityHashCode(activity)
if (trackAttemptStatus[activityId] != true) {
synchronized(trackAttemptStatus) {
if (controller == null && trackAttemptStatus.put(activityId, false) == null) {
findNavController(activity)?.trackForActivity(activity)
} else if (controller is NavController && trackAttemptStatus[activityId] != true) {
controller.trackForActivity(activity)
}
}
}
}.onFailure {
logger.trackInternalError(InternalErrorType.NAV_CONTROLLER_TRACKING_FAIL, it)
}
}

private fun NavController.trackForActivity(activity: Activity) {
navigationTrackingService.onControllerAttached(activity, clock.now())
addOnDestinationChangedListener { _, destination, _ ->
navigationTrackingService.onDestinationChange(activity, extractScreenName(destination), clock.now())
}
trackAttemptStatus[System.identityHashCode(activity)] = true
}

private fun findNavController(activity: Activity): NavController? {
if (activity is FragmentActivity) {
val navHostFragment = activity.supportFragmentManager.fragments
.firstNotNullOfOrNull { it as? NavHostFragment }
if (navHostFragment != null) {
return navHostFragment.navController
}
}
return null
}

private fun extractScreenName(destination: NavDestination): String {
return destination.route
?: destination.label?.toString()
?: destination.navigatorName
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.embrace.android.embracesdk.internal.instrumentation.compose.navigation.ComposeNavigationInstrumentationProvider
Loading
Loading