Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion firebase-config/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Unreleased

[fixed] Fixed an issue where the connection to the real-time Remote Config backend could remain
open in the background.

# 22.1.0
* [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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public class ConfigAutoFetch {
private final ConfigUpdateListener retryCallback;
private final ScheduledExecutorService scheduledExecutorService;
private final Random random;
private boolean isInBackground;

public ConfigAutoFetch(
HttpURLConnection httpURLConnection,
Expand All @@ -69,6 +70,7 @@ public ConfigAutoFetch(
this.retryCallback = retryCallback;
this.scheduledExecutorService = scheduledExecutorService;
this.random = new Random();
this.isInBackground = false;
}

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

public void setIsInBackground(boolean isInBackground) {
this.isInBackground = isInBackground;
}

private String parseAndValidateConfigUpdateMessage(String message) {
int left = message.indexOf('{');
int right = message.lastIndexOf('}');
Expand All @@ -105,15 +111,29 @@ public void listenForNotifications() {
return;
}

// Maintain a reference to the InputStream to guarantee its closure upon completion or in case
// of an exception.
InputStream inputStream = null;
try {
InputStream inputStream = httpURLConnection.getInputStream();
inputStream = httpURLConnection.getInputStream();
handleNotifications(inputStream);
inputStream.close();
} catch (IOException ex) {
// Stream was interrupted due to a transient issue and the system will retry the connection.
Log.d(TAG, "Stream was cancelled due to an exception. Retrying the connection...", ex);
// If the real-time connection is at an unexpected lifecycle state when the app is
// backgrounded, it's expected closing the httpURLConnection will throw an exception.
if (!isInBackground) {
// Otherwise, the real-time server connection was closed due to a transient issue.
Log.d(TAG, "Real-time connection was closed due to an exception.", ex);
}
} finally {
httpURLConnection.disconnect();
if (inputStream != null) {
try {
// Only need to close the InputStream, ConfigRealtimeHttpClient will disconnect
// HttpUrlConnection
inputStream.close();
} catch (IOException ex) {
Log.d(TAG, "Exception thrown when closing connection stream. Retrying connection...", ex);
}
}
}
}

Expand Down Expand Up @@ -186,7 +206,6 @@ private void handleNotifications(InputStream inputStream) throws IOException {
}

reader.close();
inputStream.close();
}

private void autoFetch(int remainingAttempts, long targetVersion) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public synchronized ConfigUpdateListenerRegistration addRealtimeConfigUpdateList
}

public synchronized void setBackgroundState(boolean isInBackground) {
configRealtimeHttpClient.setRealtimeBackgroundState(isInBackground);
configRealtimeHttpClient.setIsInBackground(isInBackground);
if (!isInBackground) {
beginRealtime();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ public class ConfigRealtimeHttpClient {
/** Flag to indicate whether or not the app is in the background or not. */
private boolean isInBackground;

// The HttpUrlConnection and auto-fetcher for this client. Only one of each exist at a time.
private HttpURLConnection httpURLConnection;
private ConfigAutoFetch configAutoFetch;

private final int ORIGINAL_RETRIES = 8;
private final ScheduledExecutorService scheduledExecutorService;
private final ConfigFetchHandler configFetchHandler;
Expand All @@ -111,6 +115,7 @@ public class ConfigRealtimeHttpClient {
private final Random random;
private final Clock clock;
private final ConfigSharedPrefsClient sharedPrefsClient;
private final Object backgroundLock;

public ConfigRealtimeHttpClient(
FirebaseApp firebaseApp,
Expand Down Expand Up @@ -145,6 +150,7 @@ public ConfigRealtimeHttpClient(
this.sharedPrefsClient = sharedPrefsClient;
this.isRealtimeDisabled = false;
this.isInBackground = false;
this.backgroundLock = new Object();
}

private static String extractProjectNumberFromAppId(String gmpAppId) {
Expand Down Expand Up @@ -391,14 +397,42 @@ public void run() {
}
}

void setRealtimeBackgroundState(boolean backgroundState) {
isInBackground = backgroundState;
public void setIsInBackground(boolean isInBackground) {
// Make changes in synchronized block so only one thread sets the background state and calls
// disconnect.
synchronized (backgroundLock) {
this.isInBackground = isInBackground;

// Propagate to ConfigAutoFetch as well.
if (configAutoFetch != null) {
configAutoFetch.setIsInBackground(isInBackground);
}
// Close the connection if the app is in the background and there is an active
// HttpUrlConnection.
if (isInBackground && httpURLConnection != null) {
httpURLConnection.disconnect();
}
}
}

private synchronized void resetRetryCount() {
httpRetriesRemaining = ORIGINAL_RETRIES;
}

/**
* The check and set http connection method are combined so that when canMakeHttpStreamConnection
* returns true, the same thread can mark isHttpConnectionIsRunning as true to prevent a race
* condition with another thread.
*/
private synchronized boolean checkAndSetHttpConnectionFlagIfNotRunning() {
boolean canMakeConnection = canMakeHttpStreamConnection();
if (canMakeConnection) {
setIsHttpConnectionRunning(true);
}

return canMakeConnection;
}

private synchronized void setIsHttpConnectionRunning(boolean connectionRunning) {
isHttpConnectionRunning = connectionRunning;
}
Expand Down Expand Up @@ -469,7 +503,7 @@ private String parseForbiddenErrorResponseMessage(InputStream inputStream) {
*/
@SuppressLint({"VisibleForTests", "DefaultLocale"})
public void beginRealtimeHttpStream() {
if (!canMakeHttpStreamConnection()) {
if (!checkAndSetHttpConnectionFlagIfNotRunning()) {
return;
}

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

try {
// If HTTP connection task failed throw exception to move to the catch block.
if (!httpURLConnectionTask.isSuccessful()) {
throw new IOException(httpURLConnectionTask.getException());
}
setIsHttpConnectionRunning(true);

// Get HTTP connection and check response code.
httpURLConnection = httpURLConnectionTask.getResult();
inputStream = httpURLConnection.getInputStream();
errorStream = httpURLConnection.getErrorStream();
responseCode = httpURLConnection.getResponseCode();

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

// Start listening for realtime notifications.
ConfigAutoFetch configAutoFetch = startAutoFetch(httpURLConnection);
configAutoFetch = startAutoFetch(httpURLConnection);
configAutoFetch.listenForNotifications();
}
} catch (IOException e) {
// Stream could not be open due to a transient issue and the system will retry the
// connection
// without user intervention.
Log.d(
TAG,
"Exception connecting to real-time RC backend. Retrying the connection...",
e);
if (isInBackground) {
// It's possible the app was backgrounded while the connection was open, which
// threw an exception trying to read the response. No real error here, so treat
// this as a success, even if we haven't read a 200 response code yet.
resetRetryCount();
} else {
// If it's not in the background, there might have been a transient error so the
// client will retry the connection.
Log.d(
TAG,
"Exception connecting to real-time RC backend. Retrying the connection...",
e);
}
} finally {
closeRealtimeHttpStream(httpURLConnection);
// Close HTTP connection and associated streams.
closeRealtimeHttpConnection(inputStream, errorStream);
setIsHttpConnectionRunning(false);

// Update backoff metadata if the connection failed in the foreground.
boolean connectionFailed =
responseCode == null || isStatusCodeRetryable(responseCode);
!isInBackground
&& (responseCode == null || isStatusCodeRetryable(responseCode));
if (connectionFailed) {
updateBackoffMetadataWithLastFailedStreamConnectionTime(
new Date(clock.currentTimeMillis()));
Expand Down Expand Up @@ -556,24 +603,34 @@ public void beginRealtimeHttpStream() {
}
}

// Reset parameters.
httpURLConnection = null;
configAutoFetch = null;

return Tasks.forResult(null);
});
}

// Pauses Http stream listening
public void closeRealtimeHttpStream(HttpURLConnection httpURLConnection) {
if (httpURLConnection != null) {
httpURLConnection.disconnect();

// Explicitly close the input stream due to a bug in the Android okhttp implementation.
// See github.com/firebase/firebase-android-sdk/pull/808.
private void closeHttpConnectionInputStream(InputStream inputStream) {
if (inputStream != null) {
try {
httpURLConnection.getInputStream().close();
if (httpURLConnection.getErrorStream() != null) {
httpURLConnection.getErrorStream().close();
}
} catch (IOException e) {
inputStream.close();
} catch (IOException ex) {
Log.d(TAG, "Error closing connection stream.", ex);
}
}
}

// Pauses Http stream listening by disconnecting the HttpUrlConnection and underlying InputStream
// and ErrorStream if they exist.
@VisibleForTesting
public void closeRealtimeHttpConnection(InputStream inputStream, InputStream errorStream) {
// Disconnect only if the connection is not null and in the foreground.
if (httpURLConnection != null && !isInBackground) {
httpURLConnection.disconnect();
}

closeHttpConnectionInputStream(inputStream);
closeHttpConnectionInputStream(errorStream);
}
}
Loading
Loading