diff --git a/CHANGELOG.md b/CHANGELOG.md index d84b9032c7..c09bed0b35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixes + +- Hook User Interaction integration into running Activity in case of deferred SDK init ([#4337](https://github.com/getsentry/sentry-java/pull/4337)) + ## 8.11.1 ### Fixes diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index fd3b261925..afb6b6a175 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -198,24 +198,12 @@ public final class io/sentry/android/core/ContextUtils { public class io/sentry/android/core/CurrentActivityHolder { public fun clearActivity ()V + public fun clearActivity (Landroid/app/Activity;)V public fun getActivity ()Landroid/app/Activity; public static fun getInstance ()Lio/sentry/android/core/CurrentActivityHolder; public fun setActivity (Landroid/app/Activity;)V } -public final class io/sentry/android/core/CurrentActivityIntegration : android/app/Application$ActivityLifecycleCallbacks, io/sentry/Integration, java/io/Closeable { - public fun (Landroid/app/Application;)V - public fun close ()V - public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V - public fun onActivityDestroyed (Landroid/app/Activity;)V - public fun onActivityPaused (Landroid/app/Activity;)V - public fun onActivityResumed (Landroid/app/Activity;)V - public fun onActivitySaveInstanceState (Landroid/app/Activity;Landroid/os/Bundle;)V - public fun onActivityStarted (Landroid/app/Activity;)V - public fun onActivityStopped (Landroid/app/Activity;)V - public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V -} - public final class io/sentry/android/core/DeviceInfoUtil { public fun (Landroid/content/Context;Lio/sentry/android/core/SentryAndroidOptions;)V public fun collectDeviceInformation (ZZ)Lio/sentry/protocol/Device; @@ -502,7 +490,10 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr public fun isAppLaunchedInForeground ()Z public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V public fun onActivityDestroyed (Landroid/app/Activity;)V + public fun onActivityPaused (Landroid/app/Activity;)V + public fun onActivityResumed (Landroid/app/Activity;)V public fun onActivityStarted (Landroid/app/Activity;)V + public fun onActivityStopped (Landroid/app/Activity;)V public fun onAppStartSpansSent ()V public static fun onApplicationCreate (Landroid/app/Application;)V public static fun onApplicationPostCreate (Landroid/app/Application;)V diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index b5cc3ddc93..302f4627f2 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -363,7 +363,6 @@ static void installDefaultIntegrations( new ActivityLifecycleIntegration( (Application) context, buildInfoProvider, activityFramesTracker)); options.addIntegration(new ActivityBreadcrumbsIntegration((Application) context)); - options.addIntegration(new CurrentActivityIntegration((Application) context)); options.addIntegration(new UserInteractionIntegration((Application) context, loadClass)); if (isFragmentAvailable) { options.addIntegration(new FragmentLifecycleIntegration((Application) context, true, true)); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/CurrentActivityHolder.java b/sentry-android-core/src/main/java/io/sentry/android/core/CurrentActivityHolder.java index 8da322b20b..a7733821c0 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/CurrentActivityHolder.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/CurrentActivityHolder.java @@ -1,11 +1,10 @@ package io.sentry.android.core; import android.app.Activity; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import java.lang.ref.WeakReference; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; @ApiStatus.Internal public class CurrentActivityHolder { @@ -16,7 +15,7 @@ private CurrentActivityHolder() {} private @Nullable WeakReference currentActivity; - public static @NonNull CurrentActivityHolder getInstance() { + public static @NotNull CurrentActivityHolder getInstance() { return instance; } @@ -27,7 +26,7 @@ private CurrentActivityHolder() {} return null; } - public void setActivity(final @NonNull Activity activity) { + public void setActivity(final @NotNull Activity activity) { if (currentActivity != null && currentActivity.get() == activity) { return; } @@ -38,4 +37,11 @@ public void setActivity(final @NonNull Activity activity) { public void clearActivity() { currentActivity = null; } + + public void clearActivity(final @NotNull Activity activity) { + if (currentActivity != null && currentActivity.get() != activity) { + return; + } + currentActivity = null; + } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/CurrentActivityIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/CurrentActivityIntegration.java deleted file mode 100644 index 3ce358efbc..0000000000 --- a/sentry-android-core/src/main/java/io/sentry/android/core/CurrentActivityIntegration.java +++ /dev/null @@ -1,85 +0,0 @@ -package io.sentry.android.core; - -import static io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion; - -import android.app.Activity; -import android.app.Application; -import android.os.Bundle; -import androidx.annotation.NonNull; -import io.sentry.IScopes; -import io.sentry.Integration; -import io.sentry.SentryLevel; -import io.sentry.SentryOptions; -import io.sentry.util.Objects; -import java.io.Closeable; -import java.io.IOException; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -@ApiStatus.Internal -public final class CurrentActivityIntegration - implements Integration, Closeable, Application.ActivityLifecycleCallbacks { - - private final @NotNull Application application; - - public CurrentActivityIntegration(final @NotNull Application application) { - this.application = Objects.requireNonNull(application, "Application is required"); - } - - @Override - public void register(@NotNull IScopes scopes, @NotNull SentryOptions options) { - application.registerActivityLifecycleCallbacks(this); - options.getLogger().log(SentryLevel.DEBUG, "CurrentActivityIntegration installed."); - addIntegrationToSdkVersion("CurrentActivity"); - } - - @Override - public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { - setCurrentActivity(activity); - } - - @Override - public void onActivityStarted(@NonNull Activity activity) { - setCurrentActivity(activity); - } - - @Override - public void onActivityResumed(@NonNull Activity activity) { - setCurrentActivity(activity); - } - - @Override - public void onActivityPaused(@NonNull Activity activity) { - cleanCurrentActivity(activity); - } - - @Override - public void onActivityStopped(@NonNull Activity activity) { - cleanCurrentActivity(activity); - } - - @Override - public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {} - - @Override - public void onActivityDestroyed(@NonNull Activity activity) { - cleanCurrentActivity(activity); - } - - @Override - public void close() throws IOException { - application.unregisterActivityLifecycleCallbacks(this); - CurrentActivityHolder.getInstance().clearActivity(); - } - - private void cleanCurrentActivity(final @NotNull Activity activity) { - if (CurrentActivityHolder.getInstance().getActivity() == activity) { - CurrentActivityHolder.getInstance().clearActivity(); - } - } - - private void setCurrentActivity(final @NotNull Activity activity) { - CurrentActivityHolder.getInstance().setActivity(activity); - } -} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java index 653cfd2ee1..9f47fc8666 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/UserInteractionIntegration.java @@ -6,6 +6,8 @@ import android.app.Application; import android.os.Bundle; import android.view.Window; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; import io.sentry.IScopes; import io.sentry.Integration; import io.sentry.SentryLevel; @@ -27,12 +29,15 @@ public final class UserInteractionIntegration private @Nullable SentryAndroidOptions options; private final boolean isAndroidXAvailable; + private final boolean isAndroidxLifecycleAvailable; public UserInteractionIntegration( final @NotNull Application application, final @NotNull io.sentry.util.LoadClass classLoader) { this.application = Objects.requireNonNull(application, "Application is required"); isAndroidXAvailable = classLoader.isClassAvailable("androidx.core.view.GestureDetectorCompat", options); + isAndroidxLifecycleAvailable = + classLoader.isClassAvailable("androidx.lifecycle.Lifecycle", options); } private void startTracking(final @NotNull Activity activity) { @@ -127,6 +132,17 @@ public void register(@NotNull IScopes scopes, @NotNull SentryOptions options) { application.registerActivityLifecycleCallbacks(this); this.options.getLogger().log(SentryLevel.DEBUG, "UserInteractionIntegration installed."); addIntegrationToSdkVersion("UserInteraction"); + + // In case of a deferred init, we hook into any resumed activity + if (isAndroidxLifecycleAvailable) { + final @Nullable Activity activity = CurrentActivityHolder.getInstance().getActivity(); + if (activity instanceof LifecycleOwner) { + if (((LifecycleOwner) activity).getLifecycle().getCurrentState() + == Lifecycle.State.RESUMED) { + startTracking(activity); + } + } + } } else { options .getLogger() diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java index 562c894991..add5762fbd 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java @@ -17,6 +17,7 @@ import io.sentry.TracesSamplingDecision; import io.sentry.android.core.BuildInfoProvider; import io.sentry.android.core.ContextUtils; +import io.sentry.android.core.CurrentActivityHolder; import io.sentry.android.core.SentryAndroidOptions; import io.sentry.android.core.internal.util.FirstDrawDoneListener; import io.sentry.util.AutoClosableReentrantLock; @@ -42,7 +43,6 @@ */ @ApiStatus.Internal public class AppStartMetrics extends ActivityLifecycleCallbacksAdapter { - public enum AppStartType { UNKNOWN, COLD, @@ -342,10 +342,12 @@ private void checkCreateTimeOnMain() { @Override public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { - final long nowUptimeMs = SystemClock.uptimeMillis(); + CurrentActivityHolder.getInstance().setActivity(activity); // the first activity determines the app start type if (activeActivitiesCounter.incrementAndGet() == 1 && !firstDrawDone.get()) { + final long nowUptimeMs = SystemClock.uptimeMillis(); + // If the app (process) was launched more than 1 minute ago, it's likely wrong final long durationSinceAppStartMillis = nowUptimeMs - appStartSpan.getStartUptimeMs(); if (!appLaunchedInForeground || durationSinceAppStartMillis > TimeUnit.MINUTES.toMillis(1)) { @@ -367,6 +369,8 @@ public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle saved @Override public void onActivityStarted(@NonNull Activity activity) { + CurrentActivityHolder.getInstance().setActivity(activity); + if (firstDrawDone.get()) { return; } @@ -378,8 +382,25 @@ public void onActivityStarted(@NonNull Activity activity) { } } + @Override + public void onActivityResumed(@NonNull Activity activity) { + CurrentActivityHolder.getInstance().setActivity(activity); + } + + @Override + public void onActivityPaused(@NonNull Activity activity) { + CurrentActivityHolder.getInstance().clearActivity(activity); + } + + @Override + public void onActivityStopped(@NonNull Activity activity) { + CurrentActivityHolder.getInstance().clearActivity(activity); + } + @Override public void onActivityDestroyed(@NonNull Activity activity) { + CurrentActivityHolder.getInstance().clearActivity(activity); + final int remainingActivities = activeActivitiesCounter.decrementAndGet(); // if the app is moving into background // as the next Activity is considered like a new app start diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index 504d2bda83..dccce75bb8 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -634,15 +634,6 @@ class AndroidOptionsInitializerTest { assertTrue { fixture.sentryOptions.envelopeDiskCache is AndroidEnvelopeCache } } - @Test - fun `CurrentActivityIntegration is added by default`() { - fixture.initSut(useRealContext = true) - - val actual = - fixture.sentryOptions.integrations.firstOrNull { it is CurrentActivityIntegration } - assertNotNull(actual) - } - @Test fun `When Activity Frames Tracking is enabled, the Activity Frames Tracker should be unavailable`() { fixture.initSut( diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/CurrentActivityIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/CurrentActivityIntegrationTest.kt deleted file mode 100644 index ecdbff5104..0000000000 --- a/sentry-android-core/src/test/java/io/sentry/android/core/CurrentActivityIntegrationTest.kt +++ /dev/null @@ -1,107 +0,0 @@ -package io.sentry.android.core - -import android.app.Activity -import android.app.Application -import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.sentry.IScopes -import org.junit.runner.RunWith -import org.mockito.kotlin.any -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNull - -@RunWith(AndroidJUnit4::class) -class CurrentActivityIntegrationTest { - - private class Fixture { - val application = mock() - val activity = mock() - val scopes = mock() - - val options = SentryAndroidOptions().apply { - dsn = "https://key@sentry.io/proj" - } - - fun getSut(): CurrentActivityIntegration { - val integration = CurrentActivityIntegration(application) - integration.register(scopes, options) - return integration - } - } - - private lateinit var fixture: Fixture - - @BeforeTest - fun `set up`() { - fixture = Fixture() - } - - @Test - fun `when the integration is added registerActivityLifecycleCallbacks is called`() { - fixture.getSut() - verify(fixture.application).registerActivityLifecycleCallbacks(any()) - } - - @Test - fun `when the integration is closed unregisterActivityLifecycleCallbacks is called`() { - val sut = fixture.getSut() - sut.close() - - verify(fixture.application).unregisterActivityLifecycleCallbacks(any()) - } - - @Test - fun `when an activity is created the activity holder provides it`() { - val sut = fixture.getSut() - - sut.onActivityCreated(fixture.activity, null) - assertEquals(fixture.activity, CurrentActivityHolder.getInstance().activity) - } - - @Test - fun `when there is no active activity the holder does not provide an outdated one`() { - val sut = fixture.getSut() - - sut.onActivityCreated(fixture.activity, null) - sut.onActivityDestroyed(fixture.activity) - - assertNull(CurrentActivityHolder.getInstance().activity) - } - - @Test - fun `when a second activity is started it gets the current one`() { - val sut = fixture.getSut() - - sut.onActivityCreated(fixture.activity, null) - sut.onActivityStarted(fixture.activity) - sut.onActivityResumed(fixture.activity) - - val secondActivity = mock() - sut.onActivityCreated(secondActivity, null) - sut.onActivityStarted(secondActivity) - - assertEquals(secondActivity, CurrentActivityHolder.getInstance().activity) - } - - @Test - fun `destroying an old activity keeps the current one`() { - val sut = fixture.getSut() - - sut.onActivityCreated(fixture.activity, null) - sut.onActivityStarted(fixture.activity) - sut.onActivityResumed(fixture.activity) - - val secondActivity = mock() - sut.onActivityCreated(secondActivity, null) - sut.onActivityStarted(secondActivity) - - sut.onActivityPaused(fixture.activity) - sut.onActivityStopped(fixture.activity) - sut.onActivityDestroyed(fixture.activity) - - assertEquals(secondActivity, CurrentActivityHolder.getInstance().activity) - } -} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt index 1f52891724..bb3401aa59 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt @@ -474,7 +474,7 @@ class SentryAndroidTest { fixture.initSut(context = mock()) { options -> optionsRef = options options.dsn = "https://key@sentry.io/123" - assertEquals(19, options.integrations.size) + assertEquals(18, options.integrations.size) options.integrations.removeAll { it is UncaughtExceptionHandlerIntegration || it is ShutdownHookIntegration || @@ -485,7 +485,6 @@ class SentryAndroidTest { it is AnrIntegration || it is ActivityLifecycleIntegration || it is ActivityBreadcrumbsIntegration || - it is CurrentActivityIntegration || it is UserInteractionIntegration || it is FragmentLifecycleIntegration || it is SentryTimberIntegration || diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt index 239aa85dbe..7862884afb 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/UserInteractionIntegrationTest.kt @@ -3,6 +3,8 @@ package io.sentry.android.core import android.app.Activity import android.app.Application import android.view.Window +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.Scopes import io.sentry.android.core.internal.gestures.NoOpWindowCallback @@ -11,13 +13,17 @@ import junit.framework.TestCase.assertNull import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never +import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.robolectric.Robolectric.buildActivity +import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertIs +import kotlin.test.assertIsNot import kotlin.test.assertNotEquals import kotlin.test.assertSame @@ -30,15 +36,17 @@ class UserInteractionIntegrationTest { val options = SentryAndroidOptions().apply { dsn = "https://key@sentry.io/proj" } - val activity: Activity = buildActivity(EmptyActivity::class.java).setup().get() + val activity: EmptyActivity = buildActivity(EmptyActivity::class.java).setup().get() val window: Window = activity.window val loadClass = mock() fun getSut( callback: Window.Callback? = null, - isAndroidXAvailable: Boolean = true + isAndroidXAvailable: Boolean = true, + isLifecycleAvailable: Boolean = true ): UserInteractionIntegration { - whenever(loadClass.isClassAvailable(any(), anyOrNull())).thenReturn(isAndroidXAvailable) + whenever(loadClass.isClassAvailable(eq("androidx.core.view.GestureDetectorCompat"), anyOrNull())).thenReturn(isAndroidXAvailable) + whenever(loadClass.isClassAvailable(eq("androidx.lifecycle.Lifecycle"), anyOrNull())).thenReturn(isLifecycleAvailable) whenever(scopes.options).thenReturn(options) if (callback != null) { window.callback = callback @@ -49,6 +57,11 @@ class UserInteractionIntegrationTest { private val fixture = Fixture() + @BeforeTest + fun setup() { + CurrentActivityHolder.getInstance().clearActivity() + } + @Test fun `when user interaction breadcrumb is enabled registers a callback`() { val sut = fixture.getSut() @@ -156,6 +169,50 @@ class UserInteractionIntegrationTest { assertNotEquals(existingCallback, (fixture.window.callback as SentryWindowCallback).delegate) } + + @Test + fun `when androidx lifecycle is unavailable doesn't hook into activity`() { + val sut = fixture.getSut(isLifecycleAvailable = false) + CurrentActivityHolder.getInstance().setActivity(fixture.activity) + sut.register(fixture.scopes, fixture.options) + assertIsNot(fixture.window) + } + + @Test + fun `when activity is resumed and is a LifecycleOwner, starts tracking immediately`() { + val sut = fixture.getSut() + whenever(fixture.activity.lifecycle.currentState).thenReturn(Lifecycle.State.RESUMED) + CurrentActivityHolder.getInstance().setActivity(fixture.activity) + + sut.register(fixture.scopes, fixture.options) + assertIs(fixture.window.callback) + } + + @Test + fun `when activity is resumed but not a LifecycleOwner, does not start tracking immediately`() { + val sut = fixture.getSut() + val activity = mock() + val window = mock() + whenever(activity.window).thenReturn(window) + + CurrentActivityHolder.getInstance().setActivity(activity) + sut.register(fixture.scopes, fixture.options) + + verify(window, never()).callback = any() + } + + @Test + fun `when activity is not in RESUMED state, does not start tracking immediately`() { + val sut = fixture.getSut() + whenever(fixture.activity.lifecycle.currentState).thenReturn(Lifecycle.State.CREATED) + CurrentActivityHolder.getInstance().setActivity(fixture.activity) + + sut.register(fixture.scopes, fixture.options) + assertIsNot(fixture.activity.window.callback) + } } -private class EmptyActivity : Activity() +private class EmptyActivity() : Activity(), LifecycleOwner { + + override val lifecycle: Lifecycle = mock() +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt index 114ea06d2d..7a272b33e7 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt @@ -12,6 +12,7 @@ import io.sentry.DateUtils import io.sentry.IContinuousProfiler import io.sentry.ITransactionProfiler import io.sentry.SentryNanotimeDate +import io.sentry.android.core.CurrentActivityHolder import io.sentry.android.core.SentryAndroidOptions import io.sentry.android.core.SentryShadowProcess import org.junit.Before @@ -507,4 +508,60 @@ class AppStartMetricsTest { // Class loaded uptimeMs is 10 ms, and process init span should finish at the same ms assertEquals(10, span.projectedStopTimestampMs) } + + @Test + fun `when an activity is created the activity holder provides it`() { + val metrics = AppStartMetrics.getInstance() + val activity = mock() + + metrics.onActivityCreated(activity, null) + assertEquals(activity, CurrentActivityHolder.getInstance().activity) + } + + @Test + fun `when there is no active activity the holder does not provide an outdated one`() { + val metrics = AppStartMetrics.getInstance() + val activity = mock() + + metrics.onActivityCreated(activity, null) + metrics.onActivityDestroyed(activity) + + assertNull(CurrentActivityHolder.getInstance().activity) + } + + @Test + fun `when a second activity is started it gets the current one`() { + val metrics = AppStartMetrics.getInstance() + val firstActivity = mock() + + metrics.onActivityCreated(firstActivity, null) + metrics.onActivityStarted(firstActivity) + metrics.onActivityResumed(firstActivity) + + val secondActivity = mock() + metrics.onActivityCreated(secondActivity, null) + metrics.onActivityStarted(secondActivity) + + assertEquals(secondActivity, CurrentActivityHolder.getInstance().activity) + } + + @Test + fun `destroying an old activity keeps the current one`() { + val metrics = AppStartMetrics.getInstance() + val firstActivity = mock() + + metrics.onActivityCreated(firstActivity, null) + metrics.onActivityStarted(firstActivity) + metrics.onActivityResumed(firstActivity) + + val secondActivity = mock() + metrics.onActivityCreated(secondActivity, null) + metrics.onActivityStarted(secondActivity) + + metrics.onActivityPaused(firstActivity) + metrics.onActivityStopped(firstActivity) + metrics.onActivityDestroyed(firstActivity) + + assertEquals(secondActivity, CurrentActivityHolder.getInstance().activity) + } }