@@ -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