Skip to content

Commit 5081e7c

Browse files
Disconnect from Remote Config real-time server when app is backgrounded. (#6816)
This PR focuses on enhancing the reliability and efficiency of the Remote Config real-time stream, particularly concerning background app states. We've refined the ``ConfigRealtimeHttpClient`` to retrieve and manage the underlying input and error streams more effectively, ensuring proper closure even in error scenarios. Additionally, the ``closeRealtimeHttpStream`` method now explicitly closes these streams using the acquired references. A key improvement is the robust handling of the app's background state. The ``setRealtimeBackgroundState`` method is now synchronized to guarantee thread-safe updates and will immediately close the real-time HTTP connection when the app transitions to the background. This background state is also reflected in ``ConfigAutoFetch``, allowing for conditional exception logging during real-time stream listening, ensuring logs are only generated when the app is actively in the foreground. Furthermore, ``ConfigAutoFetch`` now maintains a dedicated ``InputStream`` reference to ensure proper stream closure, and a separate reference is kept in ``beginRealtimeHttpStream`` to handle potential early closure scenarios before ``ConfigAutoFetch`` is reached.
1 parent 58b9da4 commit 5081e7c

File tree

5 files changed

+164
-51
lines changed

5 files changed

+164
-51
lines changed

firebase-config/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Unreleased
2-
2+
[fixed] Fixed an issue where the connection to the real-time Remote Config backend could remain
3+
open in the background.
34

45
# 22.1.0
56
* [feature] Added support for custom signal targeting in Remote Config. Use `setCustomSignals` API for setting custom signals and use them to build custom targeting conditions in Remote Config.

firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigAutoFetch.java

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public class ConfigAutoFetch {
5454
private final ConfigUpdateListener retryCallback;
5555
private final ScheduledExecutorService scheduledExecutorService;
5656
private final Random random;
57+
private boolean isInBackground;
5758

5859
public ConfigAutoFetch(
5960
HttpURLConnection httpURLConnection,
@@ -69,6 +70,7 @@ public ConfigAutoFetch(
6970
this.retryCallback = retryCallback;
7071
this.scheduledExecutorService = scheduledExecutorService;
7172
this.random = new Random();
73+
this.isInBackground = false;
7274
}
7375

7476
private synchronized void propagateErrors(FirebaseRemoteConfigException exception) {
@@ -87,6 +89,10 @@ private synchronized boolean isEventListenersEmpty() {
8789
return this.eventListeners.isEmpty();
8890
}
8991

92+
public void setIsInBackground(boolean isInBackground) {
93+
this.isInBackground = isInBackground;
94+
}
95+
9096
private String parseAndValidateConfigUpdateMessage(String message) {
9197
int left = message.indexOf('{');
9298
int right = message.lastIndexOf('}');
@@ -105,15 +111,29 @@ public void listenForNotifications() {
105111
return;
106112
}
107113

114+
// Maintain a reference to the InputStream to guarantee its closure upon completion or in case
115+
// of an exception.
116+
InputStream inputStream = null;
108117
try {
109-
InputStream inputStream = httpURLConnection.getInputStream();
118+
inputStream = httpURLConnection.getInputStream();
110119
handleNotifications(inputStream);
111-
inputStream.close();
112120
} catch (IOException ex) {
113-
// Stream was interrupted due to a transient issue and the system will retry the connection.
114-
Log.d(TAG, "Stream was cancelled due to an exception. Retrying the connection...", ex);
121+
// If the real-time connection is at an unexpected lifecycle state when the app is
122+
// backgrounded, it's expected closing the httpURLConnection will throw an exception.
123+
if (!isInBackground) {
124+
// Otherwise, the real-time server connection was closed due to a transient issue.
125+
Log.d(TAG, "Real-time connection was closed due to an exception.", ex);
126+
}
115127
} finally {
116-
httpURLConnection.disconnect();
128+
if (inputStream != null) {
129+
try {
130+
// Only need to close the InputStream, ConfigRealtimeHttpClient will disconnect
131+
// HttpUrlConnection
132+
inputStream.close();
133+
} catch (IOException ex) {
134+
Log.d(TAG, "Exception thrown when closing connection stream. Retrying connection...", ex);
135+
}
136+
}
117137
}
118138
}
119139

@@ -186,7 +206,6 @@ private void handleNotifications(InputStream inputStream) throws IOException {
186206
}
187207

188208
reader.close();
189-
inputStream.close();
190209
}
191210

192211
private void autoFetch(int remainingAttempts, long targetVersion) {

firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public synchronized ConfigUpdateListenerRegistration addRealtimeConfigUpdateList
9191
}
9292

9393
public synchronized void setBackgroundState(boolean isInBackground) {
94-
configRealtimeHttpClient.setRealtimeBackgroundState(isInBackground);
94+
configRealtimeHttpClient.setIsInBackground(isInBackground);
9595
if (!isInBackground) {
9696
beginRealtime();
9797
}

firebase-config/src/main/java/com/google/firebase/remoteconfig/internal/ConfigRealtimeHttpClient.java

Lines changed: 84 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ public class ConfigRealtimeHttpClient {
100100
/** Flag to indicate whether or not the app is in the background or not. */
101101
private boolean isInBackground;
102102

103+
// The HttpUrlConnection and auto-fetcher for this client. Only one of each exist at a time.
104+
private HttpURLConnection httpURLConnection;
105+
private ConfigAutoFetch configAutoFetch;
106+
103107
private final int ORIGINAL_RETRIES = 8;
104108
private final ScheduledExecutorService scheduledExecutorService;
105109
private final ConfigFetchHandler configFetchHandler;
@@ -111,6 +115,7 @@ public class ConfigRealtimeHttpClient {
111115
private final Random random;
112116
private final Clock clock;
113117
private final ConfigSharedPrefsClient sharedPrefsClient;
118+
private final Object backgroundLock;
114119

115120
public ConfigRealtimeHttpClient(
116121
FirebaseApp firebaseApp,
@@ -145,6 +150,7 @@ public ConfigRealtimeHttpClient(
145150
this.sharedPrefsClient = sharedPrefsClient;
146151
this.isRealtimeDisabled = false;
147152
this.isInBackground = false;
153+
this.backgroundLock = new Object();
148154
}
149155

150156
private static String extractProjectNumberFromAppId(String gmpAppId) {
@@ -391,14 +397,42 @@ public void run() {
391397
}
392398
}
393399

394-
void setRealtimeBackgroundState(boolean backgroundState) {
395-
isInBackground = backgroundState;
400+
public void setIsInBackground(boolean isInBackground) {
401+
// Make changes in synchronized block so only one thread sets the background state and calls
402+
// disconnect.
403+
synchronized (backgroundLock) {
404+
this.isInBackground = isInBackground;
405+
406+
// Propagate to ConfigAutoFetch as well.
407+
if (configAutoFetch != null) {
408+
configAutoFetch.setIsInBackground(isInBackground);
409+
}
410+
// Close the connection if the app is in the background and there is an active
411+
// HttpUrlConnection.
412+
if (isInBackground && httpURLConnection != null) {
413+
httpURLConnection.disconnect();
414+
}
415+
}
396416
}
397417

398418
private synchronized void resetRetryCount() {
399419
httpRetriesRemaining = ORIGINAL_RETRIES;
400420
}
401421

422+
/**
423+
* The check and set http connection method are combined so that when canMakeHttpStreamConnection
424+
* returns true, the same thread can mark isHttpConnectionIsRunning as true to prevent a race
425+
* condition with another thread.
426+
*/
427+
private synchronized boolean checkAndSetHttpConnectionFlagIfNotRunning() {
428+
boolean canMakeConnection = canMakeHttpStreamConnection();
429+
if (canMakeConnection) {
430+
setIsHttpConnectionRunning(true);
431+
}
432+
433+
return canMakeConnection;
434+
}
435+
402436
private synchronized void setIsHttpConnectionRunning(boolean connectionRunning) {
403437
isHttpConnectionRunning = connectionRunning;
404438
}
@@ -469,7 +503,7 @@ private String parseForbiddenErrorResponseMessage(InputStream inputStream) {
469503
*/
470504
@SuppressLint({"VisibleForTests", "DefaultLocale"})
471505
public void beginRealtimeHttpStream() {
472-
if (!canMakeHttpStreamConnection()) {
506+
if (!checkAndSetHttpConnectionFlagIfNotRunning()) {
473507
return;
474508
}
475509

@@ -489,17 +523,21 @@ public void beginRealtimeHttpStream() {
489523
this.scheduledExecutorService,
490524
(completedHttpUrlConnectionTask) -> {
491525
Integer responseCode = null;
492-
HttpURLConnection httpURLConnection = null;
526+
// Get references to InputStream and ErrorStream before listening on the stream so
527+
// that they can be closed without getting them from HttpUrlConnection.
528+
InputStream inputStream = null;
529+
InputStream errorStream = null;
493530

494531
try {
495532
// If HTTP connection task failed throw exception to move to the catch block.
496533
if (!httpURLConnectionTask.isSuccessful()) {
497534
throw new IOException(httpURLConnectionTask.getException());
498535
}
499-
setIsHttpConnectionRunning(true);
500536

501537
// Get HTTP connection and check response code.
502538
httpURLConnection = httpURLConnectionTask.getResult();
539+
inputStream = httpURLConnection.getInputStream();
540+
errorStream = httpURLConnection.getErrorStream();
503541
responseCode = httpURLConnection.getResponseCode();
504542

505543
// If the connection returned a 200 response code, start listening for messages.
@@ -509,23 +547,32 @@ public void beginRealtimeHttpStream() {
509547
sharedPrefsClient.resetRealtimeBackoff();
510548

511549
// Start listening for realtime notifications.
512-
ConfigAutoFetch configAutoFetch = startAutoFetch(httpURLConnection);
550+
configAutoFetch = startAutoFetch(httpURLConnection);
513551
configAutoFetch.listenForNotifications();
514552
}
515553
} catch (IOException e) {
516-
// Stream could not be open due to a transient issue and the system will retry the
517-
// connection
518-
// without user intervention.
519-
Log.d(
520-
TAG,
521-
"Exception connecting to real-time RC backend. Retrying the connection...",
522-
e);
554+
if (isInBackground) {
555+
// It's possible the app was backgrounded while the connection was open, which
556+
// threw an exception trying to read the response. No real error here, so treat
557+
// this as a success, even if we haven't read a 200 response code yet.
558+
resetRetryCount();
559+
} else {
560+
// If it's not in the background, there might have been a transient error so the
561+
// client will retry the connection.
562+
Log.d(
563+
TAG,
564+
"Exception connecting to real-time RC backend. Retrying the connection...",
565+
e);
566+
}
523567
} finally {
524-
closeRealtimeHttpStream(httpURLConnection);
568+
// Close HTTP connection and associated streams.
569+
closeRealtimeHttpConnection(inputStream, errorStream);
525570
setIsHttpConnectionRunning(false);
526571

572+
// Update backoff metadata if the connection failed in the foreground.
527573
boolean connectionFailed =
528-
responseCode == null || isStatusCodeRetryable(responseCode);
574+
!isInBackground
575+
&& (responseCode == null || isStatusCodeRetryable(responseCode));
529576
if (connectionFailed) {
530577
updateBackoffMetadataWithLastFailedStreamConnectionTime(
531578
new Date(clock.currentTimeMillis()));
@@ -556,24 +603,34 @@ public void beginRealtimeHttpStream() {
556603
}
557604
}
558605

606+
// Reset parameters.
607+
httpURLConnection = null;
608+
configAutoFetch = null;
609+
559610
return Tasks.forResult(null);
560611
});
561612
}
562613

563-
// Pauses Http stream listening
564-
public void closeRealtimeHttpStream(HttpURLConnection httpURLConnection) {
565-
if (httpURLConnection != null) {
566-
httpURLConnection.disconnect();
567-
568-
// Explicitly close the input stream due to a bug in the Android okhttp implementation.
569-
// See github.com/firebase/firebase-android-sdk/pull/808.
614+
private void closeHttpConnectionInputStream(InputStream inputStream) {
615+
if (inputStream != null) {
570616
try {
571-
httpURLConnection.getInputStream().close();
572-
if (httpURLConnection.getErrorStream() != null) {
573-
httpURLConnection.getErrorStream().close();
574-
}
575-
} catch (IOException e) {
617+
inputStream.close();
618+
} catch (IOException ex) {
619+
Log.d(TAG, "Error closing connection stream.", ex);
576620
}
577621
}
578622
}
623+
624+
// Pauses Http stream listening by disconnecting the HttpUrlConnection and underlying InputStream
625+
// and ErrorStream if they exist.
626+
@VisibleForTesting
627+
public void closeRealtimeHttpConnection(InputStream inputStream, InputStream errorStream) {
628+
// Disconnect only if the connection is not null and in the foreground.
629+
if (httpURLConnection != null && !isInBackground) {
630+
httpURLConnection.disconnect();
631+
}
632+
633+
closeHttpConnectionInputStream(inputStream);
634+
closeHttpConnectionInputStream(errorStream);
635+
}
579636
}

0 commit comments

Comments
 (0)