diff --git a/firebase-firestore/CHANGELOG.md b/firebase-firestore/CHANGELOG.md index 1f77303acea..7effdbe2657 100644 --- a/firebase-firestore/CHANGELOG.md +++ b/firebase-firestore/CHANGELOG.md @@ -1,4 +1,8 @@ # Unreleased +- [changed] Increases the aggressiveness of network retires when an app's + foreground status changes. + +# 23.0.1 - [changed] The SDK now tries to immediately establish a connection to the backend when the app enters the foreground. diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java index e30431dbcb9..88608b24190 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java @@ -81,16 +81,16 @@ public ImmutableSortedSet getRemoteKeysForTarget(int targetId) { RemoteStore remoteStore = new RemoteStore(callback, localStore, datastore, testQueue, connectivityMonitor); - waitFor(testQueue.enqueue(() -> remoteStore.forceEnableNetwork())); + waitFor(testQueue.enqueue(remoteStore::forceEnableNetwork)); drain(testQueue); networkChangeSemaphore.drainPermits(); connectivityMonitor.goOffline(); waitFor(networkChangeSemaphore); drain(testQueue); - - waitFor(testQueue.enqueue(() -> remoteStore.forceEnableNetwork())); networkChangeSemaphore.drainPermits(); + + waitFor(testQueue.enqueue(remoteStore::forceEnableNetwork)); connectivityMonitor.goOnline(); waitFor(networkChangeSemaphore); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/AndroidConnectivityMonitor.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/AndroidConnectivityMonitor.java index 6663cdbf33f..e8077e46089 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/AndroidConnectivityMonitor.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/AndroidConnectivityMonitor.java @@ -17,18 +17,25 @@ import static com.google.firebase.firestore.util.Assert.hardAssert; import android.annotation.TargetApi; +import android.app.Activity; +import android.app.Application; import android.content.BroadcastReceiver; +import android.content.ComponentCallbacks2; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.res.Configuration; import android.net.ConnectivityManager; import android.net.Network; import android.os.Build; +import android.os.Bundle; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.google.android.gms.common.api.internal.BackgroundDetector; import com.google.firebase.firestore.util.Consumer; +import com.google.firebase.firestore.util.Logger; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; /** * Android implementation of ConnectivityMonitor. Parallel implementations exist for N+ and pre-N. @@ -36,8 +43,9 @@ *

Implementation note: Most of the code here was shamelessly stolen from * https://github.com/grpc/grpc-java/blob/master/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java */ -public final class AndroidConnectivityMonitor - implements ConnectivityMonitor, BackgroundDetector.BackgroundStateChangeListener { +public final class AndroidConnectivityMonitor implements ConnectivityMonitor { + + private static final String LOG_TAG = "AndroidConnectivityMonitor"; private final Context context; @Nullable private final ConnectivityManager connectivityManager; @@ -78,34 +86,80 @@ private void configureNetworkMonitoring() { final DefaultNetworkCallback defaultNetworkCallback = new DefaultNetworkCallback(); connectivityManager.registerDefaultNetworkCallback(defaultNetworkCallback); unregisterRunnable = - new Runnable() { - @Override - public void run() { - connectivityManager.unregisterNetworkCallback(defaultNetworkCallback); - } - }; + () -> connectivityManager.unregisterNetworkCallback(defaultNetworkCallback); } else { NetworkReceiver networkReceiver = new NetworkReceiver(); @SuppressWarnings("deprecation") IntentFilter networkIntentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); context.registerReceiver(networkReceiver, networkIntentFilter); - unregisterRunnable = - new Runnable() { - @Override - public void run() { - context.unregisterReceiver(networkReceiver); - } - }; + unregisterRunnable = () -> context.unregisterReceiver(networkReceiver); } } private void configureBackgroundStateListener() { - BackgroundDetector.getInstance().addListener(this); + Application application = (Application) context.getApplicationContext(); + final AtomicBoolean inBackground = new AtomicBoolean(); + + // Manually register an ActivityLifecycleCallback. Android's BackgroundDetector only notifies + // when it is certain that the app transitioned from background to foreground. Instead, we + // want to be notified whenever there is a slight chance that this transition happened. + application.registerActivityLifecycleCallbacks( + new Application.ActivityLifecycleCallbacks() { + @Override + public void onActivityCreated(@NonNull Activity activity, Bundle savedInstanceState) { + if (inBackground.compareAndSet(true, false)) { + raiseForegroundNotification(); + } + } + + @Override + public void onActivityStarted(@NonNull Activity activity) { + if (inBackground.compareAndSet(true, false)) { + raiseForegroundNotification(); + } + } + + @Override + public void onActivityResumed(@NonNull Activity activity) { + if (inBackground.compareAndSet(true, false)) { + raiseForegroundNotification(); + } + } + + @Override + public void onActivityPaused(@NonNull Activity activity) {} + + @Override + public void onActivityStopped(@NonNull Activity activity) {} + + @Override + public void onActivitySaveInstanceState( + @NonNull Activity activity, @NonNull Bundle outState) {} + + @Override + public void onActivityDestroyed(@NonNull Activity activity) {} + }); + + application.registerComponentCallbacks( + new ComponentCallbacks2() { + @Override + public void onTrimMemory(int level) { + if (level == ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) { + inBackground.set(true); + } + } + + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) {} + + @Override + public void onLowMemory() {} + }); } - @Override - public void onBackgroundStateChanged(boolean background) { - if (!background && isConnected()) { + public void raiseForegroundNotification() { + Logger.debug(LOG_TAG, "App has entered the foreground."); + if (isConnected()) { raiseCallbacks(/* connected= */ true); } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/OnlineStateTracker.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/OnlineStateTracker.java index e365749945d..fe49870fc96 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/OnlineStateTracker.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/OnlineStateTracker.java @@ -88,6 +88,11 @@ interface OnlineStateCallback { shouldWarnClientIsOffline = true; } + /** Returns the current online state. */ + OnlineState getState() { + return state; + } + /** * Called by RemoteStore when a watch stream is started (including on each backoff attempt). * diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java index b9066197060..9714a505aa7 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java @@ -214,14 +214,29 @@ public void onClose(Status status) { // Porting Note: Unlike iOS, `restartNetwork()` is called even when the network // becomes unreachable as we don't have any other way to tear down our streams. + // We only invoke restartNetwork() when the network status differs from our online + // state. This prevents frequent reconnects as the callback is invoked whenever + // the app reaches the foreground. + if (networkStatus.equals(NetworkStatus.REACHABLE) + && onlineStateTracker.getState().equals(OnlineState.ONLINE)) { + return; + } + + if (networkStatus.equals(NetworkStatus.UNREACHABLE) + && onlineStateTracker.getState().equals(OnlineState.OFFLINE)) { + return; + } + // If the network has been explicitly disabled, make sure we don't accidentally // re-enable it. - if (canUseNetwork()) { - // Tear down and re-create our network streams. This will ensure the backoffs are - // reset. - Logger.debug(LOG_TAG, "Restarting streams for network reachability change."); - restartNetwork(); + if (!canUseNetwork()) { + return; } + + // Tear down and re-create our network streams. This will ensure the backoffs are + // reset. + Logger.debug(LOG_TAG, "Restarting streams for network reachability change."); + restartNetwork(); }); }); }