Unregister ActivityLifecycleCallbacks on shutdown.
This ensures that no lifecycle callbacks are queued on a executor that
has been shutdown.
MODIFY
analytics/src/main/java/com/segment/analytics/Analytics.java
analytics/src/main/java/com/segment/analytics/Analytics.java
@@ -126,6 +126,7 @@ public class Analytics {
final Cartographer cartographer;
private final ProjectSettings.Cache projectSettingsCache;
final Crypto crypto;
+ @Private final Application.ActivityLifecycleCallbacks activityLifecycleCallback;
ProjectSettings projectSettings; // todo: make final (non-final for testing).
@Private final String writeKey;
final int flushQueueSize;
@@ -279,7 +280,7 @@ public class Analytics {
logger.debug("Created analytics client for project with tag:%s.", tag);
- application.registerActivityLifecycleCallbacks(
+ activityLifecycleCallback =
new Application.ActivityLifecycleCallbacks() {
final AtomicBoolean trackedApplicationLifecycleEvents = new AtomicBoolean(false);
@@ -334,7 +335,8 @@ public class Analytics {
public void onActivityDestroyed(Activity activity) {
runOnMainThread(IntegrationOperation.onActivityDestroyed(activity));
}
- });
+ };
+ application.registerActivityLifecycleCallbacks(activityLifecycleCallback);
}
@Private
@@ -438,6 +440,9 @@ public class Analytics {
@Private
void runOnMainThread(final IntegrationOperation operation) {
+ if (shutdown) {
+ return;
+ }
analyticsExecutor.submit(
new Runnable() {
@Override
@@ -993,6 +998,7 @@ public class Analytics {
if (shutdown) {
return;
}
+ application.unregisterActivityLifecycleCallbacks(activityLifecycleCallback);
// Only supplied by us for testing, so it's ok to shut it down. If we were to make this public,
// we'll have to add a check similar to that of AnalyticsNetworkExecutorService below.
analyticsExecutor.shutdown();

MODIFY
analytics/src/test/java/com/segment/analytics/AnalyticsTest.java
analytics/src/test/java/com/segment/analytics/AnalyticsTest.java
package com.segment.analytics;
-import static android.content.Context.MODE_PRIVATE;
-import static com.segment.analytics.Analytics.LogLevel.NONE;
-import static com.segment.analytics.Analytics.LogLevel.VERBOSE;
-import static com.segment.analytics.TestUtils.SynchronousExecutor;
-import static com.segment.analytics.TestUtils.grantPermission;
-import static com.segment.analytics.TestUtils.mockApplication;
-import static com.segment.analytics.Utils.createContext;
-import static com.segment.analytics.internal.Utils.DEFAULT_FLUSH_INTERVAL;
-import static com.segment.analytics.internal.Utils.DEFAULT_FLUSH_QUEUE_SIZE;
-import static com.segment.analytics.internal.Utils.isNullOrEmpty;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.fail;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.argThat;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.when;
-import static org.mockito.MockitoAnnotations.initMocks;
-
import android.Manifest;
import android.app.Activity;
import android.app.Application;
@@ -33,8 +10,8 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
+
import com.segment.analytics.TestUtils.NoDescriptionMatcher;
-import com.segment.analytics.core.BuildConfig;
import com.segment.analytics.integrations.AliasPayload;
import com.segment.analytics.integrations.GroupPayload;
import com.segment.analytics.integrations.IdentifyPayload;
@@ -43,11 +20,7 @@ import com.segment.analytics.integrations.Logger;
import com.segment.analytics.integrations.ScreenPayload;
import com.segment.analytics.integrations.TrackPayload;
import com.segment.analytics.internal.Utils.AnalyticsNetworkExecutorService;
-import java.io.IOException;
-import java.util.Collections;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.atomic.AtomicReference;
+
import org.assertj.core.data.MapEntry;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
@@ -62,6 +35,36 @@ import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static android.content.Context.MODE_PRIVATE;
+import static com.segment.analytics.Analytics.LogLevel.NONE;
+import static com.segment.analytics.Analytics.LogLevel.VERBOSE;
+import static com.segment.analytics.TestUtils.SynchronousExecutor;
+import static com.segment.analytics.TestUtils.grantPermission;
+import static com.segment.analytics.TestUtils.mockApplication;
+import static com.segment.analytics.Utils.createContext;
+import static com.segment.analytics.internal.Utils.DEFAULT_FLUSH_INTERVAL;
+import static com.segment.analytics.internal.Utils.DEFAULT_FLUSH_QUEUE_SIZE;
+import static com.segment.analytics.internal.Utils.isNullOrEmpty;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class AnalyticsTest {
@@ -588,10 +591,10 @@ public class AnalyticsTest {
public void shutdown() {
assertThat(analytics.shutdown).isFalse();
analytics.shutdown();
+ verify(application).unregisterActivityLifecycleCallbacks(analytics.activityLifecycleCallback);
verify(stats).shutdown();
verify(networkExecutor).shutdown();
assertThat(analytics.shutdown).isTrue();
-
try {
analytics.track("foo");
fail("Enqueuing a message after shutdown should throw.");
@@ -1010,4 +1013,94 @@ public class AnalyticsTest {
verifyNoMoreInteractions(integration);
}
+
+ @Test
+ public void unregisterActivityLifecycleCallbacks() throws NameNotFoundException {
+ Analytics.INSTANCES.clear();
+
+ final AtomicReference<Application.ActivityLifecycleCallbacks> registeredCallback =
+ new AtomicReference<>();
+ final AtomicReference<Application.ActivityLifecycleCallbacks> unregisteredCallback =
+ new AtomicReference<>();
+ doNothing()
+ .when(application)
+ .registerActivityLifecycleCallbacks(
+ argThat(new NoDescriptionMatcher<Application.ActivityLifecycleCallbacks>() {
+ @Override
+ protected boolean matchesSafely(Application.ActivityLifecycleCallbacks item) {
+ registeredCallback.set(item);
+ return true;
+ }
+ }));
+ doNothing()
+ .when(application)
+ .unregisterActivityLifecycleCallbacks(
+ argThat(new NoDescriptionMatcher<Application.ActivityLifecycleCallbacks>() {
+ @Override
+ protected boolean matchesSafely(Application.ActivityLifecycleCallbacks item) {
+ unregisteredCallback.set(item);
+ return true;
+ }
+ }));
+
+ analytics =
+ new Analytics(
+ application,
+ networkExecutor,
+ stats,
+ traitsCache,
+ analyticsContext,
+ defaultOptions,
+ Logger.with(NONE),
+ "qaz",
+ Collections.singletonList(factory),
+ client,
+ Cartographer.INSTANCE,
+ projectSettingsCache,
+ "foo",
+ DEFAULT_FLUSH_QUEUE_SIZE,
+ DEFAULT_FLUSH_INTERVAL,
+ analyticsExecutor,
+ false,
+ new CountDownLatch(0),
+ false,
+ false,
+ optOut,
+ Crypto.none(),
+ Collections.<Middleware>emptyList());
+
+ assertThat(analytics.shutdown).isFalse();
+ analytics.shutdown();
+
+ // Same callback was registered and unregistered
+ assertThat(analytics.activityLifecycleCallback).isSameAs(registeredCallback.get());
+ assertThat(analytics.activityLifecycleCallback).isSameAs(unregisteredCallback.get());
+
+ Activity activity = mock(Activity.class);
+ Bundle bundle = new Bundle();
+
+ // Verify callbacks do not call through after shutdown
+ registeredCallback.get().onActivityCreated(activity, bundle);
+ verify(integration, never()).onActivityCreated(activity, bundle);
+
+ registeredCallback.get().onActivityStarted(activity);
+ verify(integration, never()).onActivityStarted(activity);
+
+ registeredCallback.get().onActivityResumed(activity);
+ verify(integration, never()).onActivityResumed(activity);
+
+ registeredCallback.get().onActivityPaused(activity);
+ verify(integration, never()).onActivityPaused(activity);
+
+ registeredCallback.get().onActivityStopped(activity);
+ verify(integration, never()).onActivityStopped(activity);
+
+ registeredCallback.get().onActivitySaveInstanceState(activity, bundle);
+ verify(integration, never()).onActivitySaveInstanceState(activity, bundle);
+
+ registeredCallback.get().onActivityDestroyed(activity);
+ verify(integration, never()).onActivityDestroyed(activity);
+
+ verifyNoMoreInteractions(integration);
+ }
}

Unregister ActivityLifecycleCallbacks on shutdown.

This ensures that no lifecycle callbacks are queued on a executor that
has been shutdown.