Skip to content

Commit

Permalink
Component to map lifecycle events to the activity open events
Browse files Browse the repository at this point in the history
  • Loading branch information
bidetofevil committed Sep 6, 2024
1 parent 17ed531 commit 9806c9a
Show file tree
Hide file tree
Showing 8 changed files with 586 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package io.embrace.android.embracesdk.internal.capture.activity

import android.app.Activity
import android.app.Application.ActivityLifecycleCallbacks
import android.os.Build
import android.os.Bundle
import io.embrace.android.embracesdk.internal.clock.nanosToMillis
import io.embrace.android.embracesdk.internal.session.lifecycle.ActivityLifecycleListener
import io.embrace.android.embracesdk.internal.utils.VersionChecker
import io.opentelemetry.sdk.common.Clock

/**
* Maps [ActivityLifecycleCallbacks] events to [OpenEvents] depending on the current state of the app and capabilities of the OS.
*
* The purpose of this is to leverage Activity lifecycle events to provide data for the underlying workflow to bring a new Activity on
* screen. Due to the varying capabilities of the APIs available on the different versions of Android, the precise triggering events for
* the start and intermediate steps may differ.
*
* See [OpenTraceEmitter] for details.
*/
public class OpenEventEmitter(
private val openEvents: OpenEvents,
private val clock: Clock,
private val versionChecker: VersionChecker,
) : ActivityLifecycleListener {

override fun onActivityPreCreated(activity: Activity, savedInstanceState: Bundle?) {
create(activity)
}

override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
if (!versionChecker.firePrePostEvents()) {
create(activity)
}
}

override fun onActivityPostCreated(activity: Activity, savedInstanceState: Bundle?) {
createEnd(activity)
}

override fun onActivityPreStarted(activity: Activity) {
start(activity)
}

override fun onActivityStarted(activity: Activity) {
if (!versionChecker.firePrePostEvents()) {
createEnd(activity)
start(activity)
}
}

override fun onActivityPostStarted(activity: Activity) {
startEnd(activity)
}

override fun onActivityPreResumed(activity: Activity) {
resume(activity)
}

override fun onActivityResumed(activity: Activity) {
if (!versionChecker.firePrePostEvents()) {
startEnd(activity)
resume(activity)
}
}

override fun onActivityPostResumed(activity: Activity) {
resumeEnd(activity)
}

override fun onActivityPaused(activity: Activity) {
if (!versionChecker.firePrePostEvents()) {
resetTrace(activity)
}
}

override fun onActivityPostPaused(activity: Activity) {
resetTrace(activity)
}

override fun onActivityStopped(activity: Activity) {
hibernate(activity)
}

private fun resetTrace(activity: Activity) {
openEvents.resetTrace(
instanceId = traceInstanceId(activity),
activityName = activity.localClassName,
timestampMs = nowMs()
)
}

private fun hibernate(activity: Activity) {
openEvents.hibernate(
instanceId = traceInstanceId(activity),
activityName = activity.localClassName,
timestampMs = nowMs()
)
}

private fun create(activity: Activity) {
openEvents.create(
instanceId = traceInstanceId(activity),
activityName = activity.localClassName,
timestampMs = nowMs()
)
}

private fun createEnd(activity: Activity) {
openEvents.createEnd(
instanceId = traceInstanceId(activity),
timestampMs = nowMs()
)
}

private fun start(activity: Activity) {
openEvents.start(
instanceId = traceInstanceId(activity),
activityName = activity.localClassName,
timestampMs = nowMs()
)
}

private fun startEnd(activity: Activity) {
openEvents.startEnd(
instanceId = traceInstanceId(activity),
timestampMs = nowMs()
)
}

private fun resume(activity: Activity) {
openEvents.resume(
instanceId = traceInstanceId(activity),
activityName = activity.localClassName,
timestampMs = nowMs()
)
}

private fun resumeEnd(activity: Activity) {
openEvents.resumeEnd(
instanceId = traceInstanceId(activity),
timestampMs = nowMs()
)
}

private fun VersionChecker.firePrePostEvents(): Boolean = isAtLeast(Build.VERSION_CODES.Q)

private fun traceInstanceId(activity: Activity): Int = activity.hashCode()

private fun nowMs(): Long = clock.now().nanosToMillis()
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.embrace.android.embracesdk.internal.injection

import io.embrace.android.embracesdk.internal.capture.activity.OpenEventEmitter
import io.embrace.android.embracesdk.internal.capture.activity.OpenEvents
import io.embrace.android.embracesdk.internal.capture.crumbs.ActivityBreadcrumbTracker
import io.embrace.android.embracesdk.internal.capture.crumbs.PushNotificationCaptureService
import io.embrace.android.embracesdk.internal.capture.startup.AppStartupDataCollector
Expand Down Expand Up @@ -35,7 +37,11 @@ public interface DataCaptureServiceModule {
*/
public val startupService: StartupService

public val appStartupDataCollector: AppStartupDataCollector

public val startupTracker: StartupTracker

public val appStartupDataCollector: AppStartupDataCollector
public val activityOpenTraceEmitter: OpenEvents

public val activityOpenTracker: OpenEventEmitter
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package io.embrace.android.embracesdk.internal.injection

import io.embrace.android.embracesdk.internal.Systrace
import io.embrace.android.embracesdk.internal.capture.activity.OpenEventEmitter
import io.embrace.android.embracesdk.internal.capture.activity.OpenEvents
import io.embrace.android.embracesdk.internal.capture.activity.OpenTraceEmitter
import io.embrace.android.embracesdk.internal.capture.crumbs.ActivityBreadcrumbTracker
import io.embrace.android.embracesdk.internal.capture.crumbs.PushNotificationCaptureService
import io.embrace.android.embracesdk.internal.capture.startup.AppStartupDataCollector
Expand Down Expand Up @@ -60,6 +63,21 @@ internal class DataCaptureServiceModuleImpl @JvmOverloads constructor(
)
}

override val activityOpenTraceEmitter: OpenEvents by singleton {
OpenTraceEmitter(
spanService = openTelemetryModule.spanService,
versionChecker = versionChecker,
)
}

override val activityOpenTracker: OpenEventEmitter by singleton {
OpenEventEmitter(
openEvents = activityOpenTraceEmitter,
clock = openTelemetryModule.openTelemetryClock,
versionChecker = versionChecker,
)
}

override val startupTracker: StartupTracker by singleton {
StartupTracker(
appStartupDataCollector = appStartupDataCollector,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package io.embrace.android.embracesdk.internal.capture.activity

import android.app.Activity
import android.os.Build
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.embrace.android.embracesdk.fakes.FakeClock
import io.embrace.android.embracesdk.fakes.injection.FakeInitModule
import io.embrace.android.embracesdk.internal.utils.BuildVersionChecker
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.android.controller.ActivityController
import org.robolectric.annotation.Config

@RunWith(AndroidJUnit4::class)
internal class OpenEventEmitterTest {
private lateinit var clock: FakeClock
private lateinit var openEvents: FakeOpenEvents
private lateinit var eventEmitter: OpenEventEmitter
private lateinit var activityController: ActivityController<*>

@Before
fun setUp() {
clock = FakeClock()
val initModule = FakeInitModule(clock = clock)
clock.tick(100L)
openEvents = FakeOpenEvents()
eventEmitter = OpenEventEmitter(
openEvents = openEvents,
clock = initModule.openTelemetryModule.openTelemetryClock,
versionChecker = BuildVersionChecker,
)
activityController = Robolectric.buildActivity(Activity::class.java)
}

@Config(sdk = [Build.VERSION_CODES.UPSIDE_DOWN_CAKE])
@Test
fun `check open event stages in U`() {
with(activityController) {
clock.tick()
create()
clock.tick()
start()
clock.tick()
resume()
clock.tick()
pause()
clock.tick()
stop()
clock.tick()
}
assertEquals(8, openEvents.events.count())
}

class FakeOpenEvents : OpenEvents {
val events = mutableListOf<EventData>()

override fun resetTrace(instanceId: Int, activityName: String, timestampMs: Long) {
events.add(
EventData(
stage = "resetTrace",
instanceId = instanceId,
activityName = activityName,
timestampMs = timestampMs
)
)
}

override fun hibernate(instanceId: Int, activityName: String, timestampMs: Long) {
events.add(
EventData(
stage = "hibernate",
instanceId = instanceId,
activityName = activityName,
timestampMs = timestampMs
)
)
}

override fun create(instanceId: Int, activityName: String, timestampMs: Long) {
events.add(
EventData(
stage = "create",
instanceId = instanceId,
activityName = activityName,
timestampMs = timestampMs
)
)
}

override fun createEnd(instanceId: Int, timestampMs: Long) {
events.add(
EventData(
stage = "createEnd",
instanceId = instanceId,
activityName = null,
timestampMs = timestampMs
)
)
}

override fun start(instanceId: Int, activityName: String, timestampMs: Long) {
events.add(
EventData(
stage = "start",
instanceId = instanceId,
activityName = activityName,
timestampMs = timestampMs
)
)
}

override fun startEnd(instanceId: Int, timestampMs: Long) {
events.add(
EventData(
stage = "startEnd",
instanceId = instanceId,
timestampMs = timestampMs
)
)
}

override fun resume(instanceId: Int, activityName: String, timestampMs: Long) {
events.add(
EventData(
stage = "resume",
instanceId = instanceId,
activityName = activityName,
timestampMs = timestampMs
)
)
}

override fun resumeEnd(instanceId: Int, timestampMs: Long) {
events.add(
EventData(
stage = "resumeEnd",
instanceId = instanceId,
timestampMs = timestampMs
)
)
}

override fun render(instanceId: Int, activityName: String, timestampMs: Long) {
events.add(
EventData(
stage = "render",
instanceId = instanceId,
activityName = activityName,
timestampMs = timestampMs
)
)
}

override fun renderEnd(instanceId: Int, timestampMs: Long) {
events.add(
EventData(
stage = "renderEnd",
instanceId = instanceId,
timestampMs = timestampMs
)
)
}

data class EventData(
val stage: String,
val instanceId: Int,
val activityName: String? = null,
val timestampMs: Long,
)
}
}
Loading

0 comments on commit 9806c9a

Please sign in to comment.