diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java index 07dbf7b1..d1fc2942 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java @@ -74,8 +74,6 @@ static void doTask(Runnable func) { public final ActivityScenarioRule testScenario = new ActivityScenarioRule<>(TestActivity.class); - @Rule - public TimberLoggingRule timberLoggingRule = new TimberLoggingRule(); @Rule public EasyMockRule easyMockRule = new EasyMockRule(this); @Rule @@ -92,6 +90,7 @@ static void doTask(Runnable func) { private ConnectivityManager connectivityManager; private MockWebServer mockStreamServer; private ActivityScenario scenario; + private LDLogger logger; static { StrictMode.setThreadPolicy(ThreadPolicy.LAX); @@ -113,6 +112,7 @@ public void before() { } }); }); + logger = LDLogger.withAdapter(LDAndroidLogging.adapter(), "ConnectivityManagerTest"); } @After @@ -146,7 +146,7 @@ private void createTestManager(boolean setOffline, boolean streaming, boolean ba HttpConfiguration httpConfig = simpleClientContext(config).getHttp(); connectivityManager = new ConnectivityManager(app, config, dataSourceConfig, httpConfig, eventProcessor, userManager, "default", - null, null, LDLogger.none()); + null, null, logger); } private void awaitStartUp() throws ExecutionException { @@ -217,6 +217,14 @@ public void initBackgroundDisabled() throws ExecutionException { @Test public void initBackgroundPolling() throws ExecutionException { + // This test simulates starting up the SDK when the app is already in background mode. + // We should see an initial poll in this case. + final Capture> callbackCapture = Capture.newInstance(); + userManager.updateCurrentUser(capture(callbackCapture)); + expectLastCall().andAnswer(() -> { + callbackCapture.getValue().onSuccess(null); + return null; + }); eventProcessor.start(); replayAll(); @@ -229,7 +237,10 @@ public void initBackgroundPolling() throws ExecutionException { assertTrue(connectivityManager.isInitialized()); assertFalse(connectivityManager.isOffline()); assertEquals(ConnectionMode.BACKGROUND_POLLING, connectivityManager.getConnectionInformation().getConnectionMode()); - assertNoConnection(); + assertNull(connectivityManager.getConnectionInformation().getLastFailure()); + assertNull(connectivityManager.getConnectionInformation().getLastFailedConnection()); + assertNotNull(connectivityManager.getConnectionInformation().getLastSuccessfulConnection()); + Assert.assertEquals(0, mockStreamServer.getRequestCount()); } @Test @@ -374,8 +385,25 @@ public void reloadBackgroundDisabled() throws ExecutionException { @Test public void reloadBackgroundPolling() throws ExecutionException { + // This test simulates switching users when the app is in the background. + final Capture> callbackCapture1 = Capture.newInstance(); + final Capture> callbackCapture2 = Capture.newInstance(); + userManager.updateCurrentUser(capture(callbackCapture1)); + expectLastCall().andAnswer(() -> { + callbackCapture1.getValue().onSuccess(null); + return null; + }); + userManager.updateCurrentUser(capture(callbackCapture2)); + expectLastCall().andAnswer(() -> { + callbackCapture2.getValue().onSuccess(null); + return null; + }); + eventProcessor.start(); + expectLastCall().times(2); + replayAll(); + ForegroundTestController.setup(false); - createTestManager(false, true, false); + createTestManager(false, false, false); awaitStartUp(); awaitReloadUser(); @@ -383,7 +411,6 @@ public void reloadBackgroundPolling() throws ExecutionException { assertTrue(connectivityManager.isInitialized()); assertFalse(connectivityManager.isOffline()); assertEquals(ConnectionMode.BACKGROUND_POLLING, connectivityManager.getConnectionInformation().getConnectionMode()); - assertNoConnection(); } @Test diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ConnectivityManager.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ConnectivityManager.java index ed73eb2b..ee271c0d 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ConnectivityManager.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ConnectivityManager.java @@ -14,6 +14,7 @@ import java.net.URI; import java.util.Calendar; import java.util.TimeZone; +import java.util.concurrent.atomic.AtomicBoolean; import static com.launchdarkly.sdk.android.ConnectionInformation.ConnectionMode; import static com.launchdarkly.sdk.android.LDUtil.isInternetConnected; @@ -38,6 +39,7 @@ class ConnectivityManager { private final String environmentName; private final int pollingInterval; private final LDUtil.ResultCallback monitor; + private final AtomicBoolean dataSourceHasAlreadyStarted = new AtomicBoolean(false); private final LDLogger logger; private LDUtil.ResultCallback initCallback = null; private volatile boolean initialized = false; @@ -189,20 +191,30 @@ private synchronized void saveConnectionInformation() { } private void stopPolling() { - PollingUpdater.stop(application); + PollingUpdater.stop(application, logger); } private void startPolling() { triggerPoll(); - PollingUpdater.startPolling(application, pollingInterval, pollingInterval); + PollingUpdater.startPolling(application, pollingInterval, pollingInterval, logger); + dataSourceHasAlreadyStarted.set(true); } private void startBackgroundPolling() { - if (initCallback != null) { - initCallback.onSuccess(null); - initCallback = null; + boolean wasPreviouslyActive = dataSourceHasAlreadyStarted.getAndSet(true); + + // If we're transitioning from foreground to background, then we don't want to do a poll + // right away because we already have recent flag data; start polling *after* the first + // first background poll interval. But if we're in the background but we started out that + // way rather than transitioning, then we should do the first poll right away. + if (wasPreviouslyActive) { + initialized = true; // assume the SDK is in a valid state already + callInitCallback(); + } else { + initialized = false; // only report successful init once a poll has succeeded + triggerPoll(); } - PollingUpdater.startBackgroundPolling(application); + PollingUpdater.startBackgroundPolling(application, logger); } private void stopStreaming() { @@ -222,6 +234,7 @@ private void stopStreaming(final LDUtil.ResultCallback onCompleteListener) private void startStreaming() { if (streamUpdateProcessor != null) { streamUpdateProcessor.start(); + dataSourceHasAlreadyStarted.set(true); } } @@ -277,8 +290,6 @@ private synchronized void attemptTransition(ConnectionMode nextState) { startPolling(); break; case BACKGROUND_POLLING: - initialized = true; - callInitCallback(); stopStreaming(); stopPolling(); startBackgroundPolling(); @@ -370,22 +381,36 @@ boolean isOffline() { } synchronized void reloadUser(final LDUtil.ResultCallback onCompleteListener) { + ConnectionMode oldConnectionMode = connectionInformation == null ? null : + connectionInformation.getConnectionMode(); throttler.cancel(); callInitCallback(); removeForegroundListener(); removeNetworkListener(); - stopPolling(); - stopStreaming(new LDUtil.ResultCallback() { - @Override - public void onSuccess(Void result) { - startUp(onCompleteListener); - } + if (streamUpdateProcessor != null) { + stopStreaming(new LDUtil.ResultCallback() { + @Override + public void onSuccess(Void result) { + startUp(onCompleteListener); + } - @Override - public void onError(Throwable e) { - startUp(onCompleteListener); + @Override + public void onError(Throwable e) { + startUp(onCompleteListener); + } + }); + } else { + stopPolling(); + initCallback = onCompleteListener; + switch (oldConnectionMode) { + case POLLING: + startPolling(); + case BACKGROUND_POLLING: + startBackgroundPolling(); + default: + startUp(onCompleteListener); } - }); + } } private synchronized void updateConnectionMode(ConnectionMode connectionMode) { diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java index d9d75b3e..59c7265a 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java @@ -110,7 +110,7 @@ public static Future init(@NonNull Application application, initSharedLogger(config); // Clear any obsolete polling alarm that might exist; we'll start a new one if appropriate. - PollingUpdater.stop(application); + PollingUpdater.stop(application, getSharedLogger()); // Acquire the `initLock` to ensure that if `init()` is called multiple times, we will only // initialize the client(s) once. diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/PollingUpdater.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/PollingUpdater.java index c6f5c363..b443dedb 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/PollingUpdater.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/PollingUpdater.java @@ -10,6 +10,7 @@ import static android.app.PendingIntent.FLAG_IMMUTABLE; +import com.launchdarkly.logging.LDLogger; import com.launchdarkly.logging.LogValues; import java.util.concurrent.atomic.AtomicBoolean; @@ -40,23 +41,25 @@ public void onReceive(Context context, Intent intent) { // of the app, which may have crashed or forgotten to shut down the SDK. If so, // AlarmManager might have restarted the app just for this alarm. That's unfortunate but // at least we can stop it from happening again, by cancelling the alarm now. - stop(context); + stop(context, LDClient.getSharedLogger()); } } - synchronized static void startBackgroundPolling(Context context) { - LDClient.getSharedLogger().debug("Starting background polling"); - startPolling(context, backgroundPollingIntervalMillis, backgroundPollingIntervalMillis); + synchronized static void startBackgroundPolling(Context context, LDLogger logger) { + logger.debug("Starting background polling"); + startPolling(context, backgroundPollingIntervalMillis, backgroundPollingIntervalMillis, + logger); } - synchronized static void startPolling(Context context, int initialDelayMillis, int intervalMillis) { + synchronized static void startPolling(Context context, int initialDelayMillis, int intervalMillis, + LDLogger logger) { if (pollingActive.get()) { if (pollingInterval.get() == intervalMillis) { return; } } - stop(context); - LDClient.getSharedLogger().debug("startPolling with initialDelayMillis: %d and intervalMillis: %d", initialDelayMillis, intervalMillis); + stop(context, logger); + logger.debug("startPolling with initialDelayMillis: {} and intervalMillis: {}", initialDelayMillis, intervalMillis); PendingIntent pendingIntent = getPendingIntent(context); AlarmManager alarmMgr = getAlarmManager(context); @@ -69,18 +72,18 @@ synchronized static void startPolling(Context context, int initialDelayMillis, i intervalMillis, pendingIntent); } catch (Exception ex) { - LDUtil.logExceptionAtWarnLevel(LDClient.getSharedLogger(), ex, + LDUtil.logExceptionAtWarnLevel(logger, ex, "Exception occurred when creating [background] polling alarm, likely due to the host application having too many existing alarms"); pollingActive.set(false); } } - synchronized static void stop(Context context) { + synchronized static void stop(Context context, LDLogger logger) { if (pollingActive.get()) { // We may have been called even if pollingActive wasn't true, just to stop any obsolete // alarm that may have been set in the past. But there's no point in logging a message // in that case. - LDClient.getSharedLogger().debug("Stopping pollingUpdater"); + logger.debug("Stopping pollingUpdater"); } PendingIntent pendingIntent = getPendingIntent(context); AlarmManager alarmMgr = getAlarmManager(context); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/PollingDataSourceBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/PollingDataSourceBuilder.java index f8cf01ee..4cbade7d 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/PollingDataSourceBuilder.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/PollingDataSourceBuilder.java @@ -62,8 +62,7 @@ public PollingDataSourceBuilder backgroundPollIntervalMillis(int backgroundPollI /** * Sets the interval between feature flag updates when the application is running in the foreground. *

- * The default value is {@link LDConfig#DEFAULT_BACKGROUND_POLL_INTERVAL_MILLIS}. That is also - * the minimum value. + * The default value is {@link #DEFAULT_POLL_INTERVAL_MILLIS}. That is also the minimum value. * * @param pollIntervalMillis the reconnect time base value in milliseconds * @return the builder