Skip to content

Commit

Permalink
Merge 2a4b6ee into 5c3f7ef
Browse files Browse the repository at this point in the history
  • Loading branch information
qdpham13 committed Apr 23, 2024
2 parents 5c3f7ef + 2a4b6ee commit d759291
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 56 deletions.
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 setBackgroundState(boolean backgroundState) {
isInBackground = backgroundState;
}

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

// Keep reference to InputStream so it can be closed when the connection is finished or threw 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 (!isInBackground) {
// 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);
}
} 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 @@ -184,9 +202,6 @@ private void handleNotifications(InputStream inputStream) throws IOException {
currentConfigUpdateMessage = "";
}
}

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 @@ -98,8 +98,10 @@ public class ConfigRealtimeHttpClient {
private boolean isRealtimeDisabled;

/** Flag to indicate whether or not the app is in the background or not. */
private boolean isInBackground;

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 Down Expand Up @@ -391,14 +393,42 @@ public void run() {
}
}

void setRealtimeBackgroundState(boolean backgroundState) {
isInBackground = backgroundState;
public void setRealtimeBackgroundState(boolean backgroundState) {
// Make changes in synchronized block so only one thread sets the background state and calls
// disconnect.
synchronized (isInBackground) {
isInBackground = backgroundState;
if (configAutoFetch != null) {
configAutoFetch.setBackgroundState(backgroundState);
}
// Close the connection if the app is in the background and their is an active
// HttpUrlConnection.
if (isInBackground) {
if (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 +499,7 @@ private String parseForbiddenErrorResponseMessage(InputStream inputStream) {
*/
@SuppressLint({"VisibleForTests", "DefaultLocale"})
public void beginRealtimeHttpStream() {
if (!canMakeHttpStreamConnection()) {
if (!checkAndSetHttpConnectionFlagIfNotRunning()) {
return;
}

Expand All @@ -489,17 +519,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 +543,31 @@ public void beginRealtimeHttpStream() {
metadataClient.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);

boolean connectionFailed =
responseCode == null || isStatusCodeRetryable(responseCode);
!isInBackground
&& (responseCode == null || isStatusCodeRetryable(responseCode));
if (connectionFailed) {
updateBackoffMetadataWithLastFailedStreamConnectionTime(
new Date(clock.currentTimeMillis()));
Expand All @@ -542,8 +584,7 @@ public void beginRealtimeHttpStream() {
"Unable to connect to the server. Try again in a few minutes. HTTP status code: %d",
responseCode);
// Return server message for when the Firebase Remote Config Realtime API is
// disabled and
// the server returns a 403
// disabled and the server returns a 403
if (responseCode == 403) {
errorMessage =
parseForbiddenErrorResponseMessage(httpURLConnection.getErrorStream());
Expand All @@ -556,24 +597,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();
private void closeHttpConnectionInputStream(InputStream inputStream) {
if (inputStream == null) {
return;
}

// Explicitly close the input stream due to a bug in the Android okhttp implementation.
// See github.com/firebase/firebase-android-sdk/pull/808.
try {
httpURLConnection.getInputStream().close();
if (httpURLConnection.getErrorStream() != null) {
httpURLConnection.getErrorStream().close();
}
} catch (IOException e) {
}
try {
inputStream.close();
} catch (IOException ex) {
Log.d(TAG, "Exception thrown when closing connection stream. Retrying connection...", 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) {
if (httpURLConnection != null && !isInBackground) {
httpURLConnection.disconnect();
}
closeHttpConnectionInputStream(inputStream);
closeHttpConnectionInputStream(errorStream);
}
}
Loading

0 comments on commit d759291

Please sign in to comment.