@@ -100,6 +100,10 @@ public class ConfigRealtimeHttpClient {
100
100
/** Flag to indicate whether or not the app is in the background or not. */
101
101
private boolean isInBackground ;
102
102
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
+
103
107
private final int ORIGINAL_RETRIES = 8 ;
104
108
private final ScheduledExecutorService scheduledExecutorService ;
105
109
private final ConfigFetchHandler configFetchHandler ;
@@ -111,6 +115,7 @@ public class ConfigRealtimeHttpClient {
111
115
private final Random random ;
112
116
private final Clock clock ;
113
117
private final ConfigSharedPrefsClient sharedPrefsClient ;
118
+ private final Object backgroundLock ;
114
119
115
120
public ConfigRealtimeHttpClient (
116
121
FirebaseApp firebaseApp ,
@@ -145,6 +150,7 @@ public ConfigRealtimeHttpClient(
145
150
this .sharedPrefsClient = sharedPrefsClient ;
146
151
this .isRealtimeDisabled = false ;
147
152
this .isInBackground = false ;
153
+ this .backgroundLock = new Object ();
148
154
}
149
155
150
156
private static String extractProjectNumberFromAppId (String gmpAppId ) {
@@ -391,14 +397,42 @@ public void run() {
391
397
}
392
398
}
393
399
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
+ }
396
416
}
397
417
398
418
private synchronized void resetRetryCount () {
399
419
httpRetriesRemaining = ORIGINAL_RETRIES ;
400
420
}
401
421
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
+
402
436
private synchronized void setIsHttpConnectionRunning (boolean connectionRunning ) {
403
437
isHttpConnectionRunning = connectionRunning ;
404
438
}
@@ -469,7 +503,7 @@ private String parseForbiddenErrorResponseMessage(InputStream inputStream) {
469
503
*/
470
504
@ SuppressLint ({"VisibleForTests" , "DefaultLocale" })
471
505
public void beginRealtimeHttpStream () {
472
- if (!canMakeHttpStreamConnection ()) {
506
+ if (!checkAndSetHttpConnectionFlagIfNotRunning ()) {
473
507
return ;
474
508
}
475
509
@@ -489,17 +523,21 @@ public void beginRealtimeHttpStream() {
489
523
this .scheduledExecutorService ,
490
524
(completedHttpUrlConnectionTask ) -> {
491
525
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 ;
493
530
494
531
try {
495
532
// If HTTP connection task failed throw exception to move to the catch block.
496
533
if (!httpURLConnectionTask .isSuccessful ()) {
497
534
throw new IOException (httpURLConnectionTask .getException ());
498
535
}
499
- setIsHttpConnectionRunning (true );
500
536
501
537
// Get HTTP connection and check response code.
502
538
httpURLConnection = httpURLConnectionTask .getResult ();
539
+ inputStream = httpURLConnection .getInputStream ();
540
+ errorStream = httpURLConnection .getErrorStream ();
503
541
responseCode = httpURLConnection .getResponseCode ();
504
542
505
543
// If the connection returned a 200 response code, start listening for messages.
@@ -509,23 +547,32 @@ public void beginRealtimeHttpStream() {
509
547
sharedPrefsClient .resetRealtimeBackoff ();
510
548
511
549
// Start listening for realtime notifications.
512
- ConfigAutoFetch configAutoFetch = startAutoFetch (httpURLConnection );
550
+ configAutoFetch = startAutoFetch (httpURLConnection );
513
551
configAutoFetch .listenForNotifications ();
514
552
}
515
553
} 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
+ }
523
567
} finally {
524
- closeRealtimeHttpStream (httpURLConnection );
568
+ // Close HTTP connection and associated streams.
569
+ closeRealtimeHttpConnection (inputStream , errorStream );
525
570
setIsHttpConnectionRunning (false );
526
571
572
+ // Update backoff metadata if the connection failed in the foreground.
527
573
boolean connectionFailed =
528
- responseCode == null || isStatusCodeRetryable (responseCode );
574
+ !isInBackground
575
+ && (responseCode == null || isStatusCodeRetryable (responseCode ));
529
576
if (connectionFailed ) {
530
577
updateBackoffMetadataWithLastFailedStreamConnectionTime (
531
578
new Date (clock .currentTimeMillis ()));
@@ -556,24 +603,34 @@ public void beginRealtimeHttpStream() {
556
603
}
557
604
}
558
605
606
+ // Reset parameters.
607
+ httpURLConnection = null ;
608
+ configAutoFetch = null ;
609
+
559
610
return Tasks .forResult (null );
560
611
});
561
612
}
562
613
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 ) {
570
616
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 );
576
620
}
577
621
}
578
622
}
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
+ }
579
636
}
0 commit comments