Skip to content

Commit 6788546

Browse files
authored
Always call foreground activity callbacks on the UI thread (#4531)
* Always call foreground activity callbacks on the UI thread This will make it easier to reason about what runs on what thread. * Refactor getActivityWithIgnoredClass to consolidate synchronized blocks
1 parent 8549013 commit 6788546

File tree

8 files changed

+71
-73
lines changed

8 files changed

+71
-73
lines changed

firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/AabUpdater.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,10 @@ class AabUpdater {
4747
private boolean hasBeenSentToPlayForCurrentTask = false;
4848

4949
AabUpdater(
50+
@NonNull FirebaseAppDistributionLifecycleNotifier lifecycleNotifier,
5051
@NonNull @Blocking Executor blockingExecutor,
5152
@NonNull @Lightweight Executor lightweightExecutor) {
52-
this(
53-
FirebaseAppDistributionLifecycleNotifier.getInstance(),
54-
new HttpsUrlConnectionFactory(),
55-
blockingExecutor,
56-
lightweightExecutor);
53+
this(lifecycleNotifier, new HttpsUrlConnectionFactory(), blockingExecutor, lightweightExecutor);
5754
}
5855

5956
AabUpdater(

firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/ApkInstaller.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,6 @@ class ApkInstaller {
4343
lifeCycleNotifier.addOnActivityDestroyedListener(this::onActivityDestroyed);
4444
}
4545

46-
ApkInstaller(@Lightweight Executor lightweightExecutor) {
47-
this(FirebaseAppDistributionLifecycleNotifier.getInstance(), lightweightExecutor);
48-
}
49-
5046
void onActivityDestroyed(@Nullable Activity activity) {
5147
if (activity instanceof InstallActivity) {
5248
// Since install activity is destroyed but app is still active, installation has failed /

firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/ApkUpdater.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,27 +58,28 @@ class ApkUpdater {
5858
public ApkUpdater(
5959
@NonNull FirebaseApp firebaseApp,
6060
@NonNull ApkInstaller apkInstaller,
61+
@NonNull FirebaseAppDistributionLifecycleNotifier lifeCycleNotifier,
6162
@NonNull @Blocking Executor blockingExecutor,
6263
@NonNull @Lightweight Executor lightweightExecutor) {
6364
this(
64-
blockingExecutor,
65-
lightweightExecutor,
6665
firebaseApp.getApplicationContext(),
6766
apkInstaller,
6867
new FirebaseAppDistributionNotificationsManager(firebaseApp.getApplicationContext()),
6968
new HttpsUrlConnectionFactory(),
70-
FirebaseAppDistributionLifecycleNotifier.getInstance());
69+
lifeCycleNotifier,
70+
blockingExecutor,
71+
lightweightExecutor);
7172
}
7273

7374
@VisibleForTesting
7475
public ApkUpdater(
75-
@NonNull @Blocking Executor blockingExecutor,
76-
@NonNull @Lightweight Executor lightweightExecutor,
7776
@NonNull Context context,
7877
@NonNull ApkInstaller apkInstaller,
7978
@NonNull FirebaseAppDistributionNotificationsManager appDistributionNotificationsManager,
8079
@NonNull HttpsUrlConnectionFactory httpsUrlConnectionFactory,
81-
@NonNull FirebaseAppDistributionLifecycleNotifier lifeCycleNotifier) {
80+
@NonNull FirebaseAppDistributionLifecycleNotifier lifeCycleNotifier,
81+
@NonNull @Blocking Executor blockingExecutor,
82+
@NonNull @Lightweight Executor lightweightExecutor) {
8283
this.blockingExecutor = blockingExecutor;
8384
this.lightweightExecutor = lightweightExecutor;
8485
this.context = context;

firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/FirebaseAppDistributionLifecycleNotifier.java

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@
1414

1515
package com.google.firebase.appdistribution.impl;
1616

17+
import static com.google.firebase.appdistribution.impl.TaskUtils.runAsyncInTask;
18+
1719
import android.app.Activity;
1820
import android.app.Application;
1921
import android.os.Bundle;
22+
import android.os.Looper;
2023
import androidx.annotation.GuardedBy;
2124
import androidx.annotation.NonNull;
2225
import androidx.annotation.Nullable;
@@ -25,6 +28,7 @@
2528
import com.google.android.gms.tasks.Task;
2629
import com.google.android.gms.tasks.TaskCompletionSource;
2730
import com.google.android.gms.tasks.Tasks;
31+
import com.google.firebase.annotations.concurrent.UiThread;
2832
import com.google.firebase.appdistribution.FirebaseAppDistributionException;
2933
import java.util.ArrayDeque;
3034
import java.util.Queue;
@@ -53,6 +57,7 @@ interface NullableActivityFunction<T> {
5357
}
5458

5559
private static FirebaseAppDistributionLifecycleNotifier instance;
60+
private final Executor uiThreadExecutor;
5661
private final Object lock = new Object();
5762

5863
@GuardedBy("lock")
@@ -82,11 +87,14 @@ interface NullableActivityFunction<T> {
8287
private final Queue<OnActivityDestroyedListener> onDestroyedListeners = new ArrayDeque<>();
8388

8489
@VisibleForTesting
85-
FirebaseAppDistributionLifecycleNotifier() {}
90+
FirebaseAppDistributionLifecycleNotifier(Executor uiThreadExecutor) {
91+
this.uiThreadExecutor = uiThreadExecutor;
92+
}
8693

87-
static synchronized FirebaseAppDistributionLifecycleNotifier getInstance() {
94+
static synchronized FirebaseAppDistributionLifecycleNotifier getInstance(
95+
@UiThread Executor uiThreadExecutor) {
8896
if (instance == null) {
89-
instance = new FirebaseAppDistributionLifecycleNotifier();
97+
instance = new FirebaseAppDistributionLifecycleNotifier(uiThreadExecutor);
9098
}
9199
return instance;
92100
}
@@ -115,9 +123,7 @@ interface OnActivityDestroyedListener {
115123
* Apply a function to a foreground activity, when one is available, returning a {@link Task} that
116124
* will complete immediately after the function is applied.
117125
*
118-
* <p>The function will be called immediately once the activity is available. This may be main
119-
* thread or the calling thread, depending on whether or not there is already a foreground
120-
* activity available when this method is called.
126+
* <p>The function will always be called on the UI thread.
121127
*/
122128
<T> Task<T> applyToForegroundActivity(ActivityFunction<T> function) {
123129
return getForegroundActivity(null)
@@ -141,9 +147,7 @@ <T> Task<T> applyToForegroundActivity(ActivityFunction<T> function) {
141147
* will be passed to the function, which may be null if there was no previously active activity or
142148
* the activity has been destroyed.
143149
*
144-
* <p>The function will be called immediately once the activity is available. This may be main
145-
* thread or the calling thread, depending on whether or not there is already a foreground
146-
* activity available when this method is called.
150+
* <p>The function will always be called on the UI thread.
147151
*/
148152
<T, A extends Activity> Task<T> applyToNullableForegroundActivity(
149153
Class<A> classToIgnore, NullableActivityFunction<T> function) {
@@ -168,9 +172,7 @@ <T, A extends Activity> Task<T> applyToNullableForegroundActivity(
168172
* will be passed to the function, which may be null if there was no previously active activity or
169173
* the activity has been destroyed.
170174
*
171-
* <p>The continuation function will be called immediately once the activity is available. This
172-
* may be on the main thread or the calling thread, depending on whether or not there is already a
173-
* foreground activity available when this method is called.
175+
* <p>The function will always be called on the UI thread.
174176
*/
175177
<T, A extends Activity> Task<T> applyToNullableForegroundActivityTask(
176178
Class<A> classToIgnore, SuccessContinuation<Activity, T> continuation) {
@@ -207,9 +209,7 @@ Task<Void> consumeForegroundActivity(ActivityConsumer consumer) {
207209
* Apply a function to a foreground activity, when one is available, returning a {@link Task} that
208210
* will complete with the result of the Task returned by that function.
209211
*
210-
* <p>The continuation function will be called immediately once the activity is available. This
211-
* may be on the main thread or the calling thread, depending on whether or not there is already a
212-
* foreground activity available when this method is called.
212+
* <p>The function will always be called on the UI thread.
213213
*/
214214
<T> Task<T> applyToForegroundActivityTask(SuccessContinuation<Activity, T> continuation) {
215215
return getForegroundActivity()
@@ -232,40 +232,40 @@ Task<Activity> getForegroundActivity() {
232232
<A extends Activity> Task<Activity> getForegroundActivity(@Nullable Class<A> classToIgnore) {
233233
synchronized (lock) {
234234
if (currentActivity != null) {
235-
return Tasks.forResult(
236-
getActivityWithIgnoredClass(currentActivity, previousActivity, classToIgnore));
235+
Activity foregroundActivity = getCurrentActivityWithIgnoredClass(classToIgnore);
236+
if (Looper.myLooper() == Looper.getMainLooper()) {
237+
// We're already on the UI thread, so just complete the task with the activity
238+
return Tasks.forResult(foregroundActivity);
239+
} else {
240+
// Run in UI thread so that returned Task will be completed on the UI thread
241+
return runAsyncInTask(
242+
uiThreadExecutor, () -> getCurrentActivityWithIgnoredClass(classToIgnore));
243+
}
237244
}
238245
}
239246

240247
TaskCompletionSource<Activity> task = new TaskCompletionSource<>();
241-
242248
addOnActivityResumedListener(
243249
new OnActivityResumedListener() {
244250
@Override
245251
public void onResumed(Activity activity) {
246-
task.setResult(
247-
getActivityWithIgnoredClass(activity, getPreviousActivity(), classToIgnore));
252+
// Since this method is run on the UI thread, the Task is completed on the UI thread
253+
task.setResult(getCurrentActivityWithIgnoredClass(classToIgnore));
248254
removeOnActivityResumedListener(this);
249255
}
250256
});
251-
252257
return task.getTask();
253258
}
254259

255260
@Nullable
256-
private Activity getPreviousActivity() {
261+
private <A extends Activity> Activity getCurrentActivityWithIgnoredClass(
262+
@Nullable Class<A> classToIgnore) {
257263
synchronized (lock) {
258-
return previousActivity;
259-
}
260-
}
261-
262-
@Nullable
263-
private static <A extends Activity> Activity getActivityWithIgnoredClass(
264-
Activity activity, @Nullable Activity fallbackActivity, @Nullable Class<A> classToIgnore) {
265-
if (classToIgnore != null && classToIgnore.isInstance(activity)) {
266-
return fallbackActivity;
267-
} else {
268-
return activity;
264+
if (classToIgnore != null && classToIgnore.isInstance(currentActivity)) {
265+
return previousActivity;
266+
} else {
267+
return currentActivity;
268+
}
269269
}
270270
}
271271

firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/FirebaseAppDistributionRegistrar.java

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.google.firebase.annotations.concurrent.Background;
2323
import com.google.firebase.annotations.concurrent.Blocking;
2424
import com.google.firebase.annotations.concurrent.Lightweight;
25+
import com.google.firebase.annotations.concurrent.UiThread;
2526
import com.google.firebase.appdistribution.FirebaseAppDistribution;
2627
import com.google.firebase.components.Component;
2728
import com.google.firebase.components.ComponentContainer;
@@ -52,6 +53,7 @@ public class FirebaseAppDistributionRegistrar implements ComponentRegistrar {
5253
Qualified<Executor> blockingExecutor = Qualified.qualified(Blocking.class, Executor.class);
5354
Qualified<Executor> lightweightExecutor =
5455
Qualified.qualified(Lightweight.class, Executor.class);
56+
Qualified<Executor> uiThreadExecutor = Qualified.qualified(UiThread.class, Executor.class);
5557
return Arrays.asList(
5658
Component.builder(FirebaseAppDistribution.class)
5759
.name(LIBRARY_NAME)
@@ -60,18 +62,22 @@ public class FirebaseAppDistributionRegistrar implements ComponentRegistrar {
6062
.add(Dependency.required(backgroundExecutor))
6163
.add(Dependency.required(blockingExecutor))
6264
.add(Dependency.required(lightweightExecutor))
65+
.add(Dependency.required(uiThreadExecutor))
6366
.factory(
6467
c ->
6568
buildFirebaseAppDistribution(
66-
c, backgroundExecutor, blockingExecutor, lightweightExecutor))
69+
c,
70+
backgroundExecutor,
71+
blockingExecutor,
72+
lightweightExecutor,
73+
uiThreadExecutor))
6774
// construct FirebaseAppDistribution instance on startup so we can register for
6875
// activity lifecycle callbacks before the API is called
6976
.alwaysEager()
7077
.build(),
7178
Component.builder(FeedbackSender.class)
7279
.add(Dependency.required(FirebaseApp.class))
7380
.add(Dependency.requiredProvider(FirebaseInstallationsApi.class))
74-
.add(Dependency.required(backgroundExecutor))
7581
.add(Dependency.required(blockingExecutor))
7682
.factory(c -> buildFeedbackSender(c, c.get(blockingExecutor)))
7783
.build(),
@@ -96,11 +102,13 @@ private FirebaseAppDistribution buildFirebaseAppDistribution(
96102
ComponentContainer container,
97103
Qualified<Executor> backgroundExecutorType,
98104
Qualified<Executor> blockingExecutorType,
99-
Qualified<Executor> lightweightExecutorType) {
105+
Qualified<Executor> lightweightExecutorType,
106+
Qualified<Executor> uiThreadExecutorType) {
100107
FirebaseApp firebaseApp = container.get(FirebaseApp.class);
101108
Executor backgroundExecutor = container.get(backgroundExecutorType);
102109
Executor blockingExecutor = container.get(blockingExecutorType);
103110
Executor lightweightExecutor = container.get(lightweightExecutorType);
111+
Executor uiThreadExecutor = container.get(uiThreadExecutorType);
104112
Context context = firebaseApp.getApplicationContext();
105113
Provider<FirebaseInstallationsApi> firebaseInstallationsApiProvider =
106114
container.getProvider(FirebaseInstallationsApi.class);
@@ -112,24 +120,29 @@ private FirebaseAppDistribution buildFirebaseAppDistribution(
112120
blockingExecutor);
113121
SignInStorage signInStorage = new SignInStorage(context, backgroundExecutor);
114122
FirebaseAppDistributionLifecycleNotifier lifecycleNotifier =
115-
FirebaseAppDistributionLifecycleNotifier.getInstance();
123+
FirebaseAppDistributionLifecycleNotifier.getInstance(uiThreadExecutor);
116124
ReleaseIdentifier releaseIdentifier = new ReleaseIdentifier(firebaseApp, testerApiClient);
117125
FirebaseAppDistribution appDistribution =
118126
new FirebaseAppDistributionImpl(
119127
firebaseApp,
120128
new TesterSignInManager(
121-
firebaseApp, firebaseInstallationsApiProvider, signInStorage, lightweightExecutor),
129+
firebaseApp,
130+
firebaseInstallationsApiProvider,
131+
signInStorage,
132+
lifecycleNotifier,
133+
lightweightExecutor),
122134
new NewReleaseFetcher(
123135
firebaseApp.getApplicationContext(),
124136
testerApiClient,
125137
releaseIdentifier,
126138
lightweightExecutor),
127139
new ApkUpdater(
128140
firebaseApp,
129-
new ApkInstaller(lightweightExecutor),
141+
new ApkInstaller(lifecycleNotifier, lightweightExecutor),
142+
lifecycleNotifier,
130143
blockingExecutor,
131144
lightweightExecutor),
132-
new AabUpdater(blockingExecutor, lightweightExecutor),
145+
new AabUpdater(lifecycleNotifier, blockingExecutor, lightweightExecutor),
133146
signInStorage,
134147
lifecycleNotifier,
135148
releaseIdentifier,

firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/impl/TesterSignInManager.java

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -56,20 +56,6 @@ class TesterSignInManager {
5656

5757
private boolean hasBeenSentToBrowserForCurrentTask = false;
5858

59-
TesterSignInManager(
60-
@NonNull FirebaseApp firebaseApp,
61-
@NonNull Provider<FirebaseInstallationsApi> firebaseInstallationsApiProvider,
62-
@NonNull final SignInStorage signInStorage,
63-
@Lightweight Executor lightweightExecutor) {
64-
this(
65-
firebaseApp,
66-
firebaseInstallationsApiProvider,
67-
signInStorage,
68-
FirebaseAppDistributionLifecycleNotifier.getInstance(),
69-
lightweightExecutor);
70-
}
71-
72-
@VisibleForTesting
7359
TesterSignInManager(
7460
@NonNull FirebaseApp firebaseApp,
7561
@NonNull Provider<FirebaseInstallationsApi> firebaseInstallationsApiProvider,

firebase-appdistribution/src/test/java/com/google/firebase/appdistribution/impl/ApkUpdaterTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,13 @@ public void setup() throws IOException, FirebaseAppDistributionException {
125125
apkUpdater =
126126
Mockito.spy(
127127
new ApkUpdater(
128-
blockingExecutor,
129-
lightweightExecutor,
130128
ApplicationProvider.getApplicationContext(),
131129
mockApkInstaller,
132130
mockNotificationsManager,
133131
mockHttpsUrlConnectionFactory,
134-
mockLifecycleNotifier));
132+
mockLifecycleNotifier,
133+
blockingExecutor,
134+
lightweightExecutor));
135135
}
136136

137137
@Test

firebase-appdistribution/src/test/java/com/google/firebase/appdistribution/impl/FirebaseAppDistributionLifecycleNotifierTest.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,14 @@
2626
import com.google.android.gms.tasks.SuccessContinuation;
2727
import com.google.android.gms.tasks.Task;
2828
import com.google.android.gms.tasks.Tasks;
29+
import com.google.firebase.annotations.concurrent.UiThread;
2930
import com.google.firebase.appdistribution.FirebaseAppDistributionException;
3031
import com.google.firebase.appdistribution.FirebaseAppDistributionException.Status;
3132
import com.google.firebase.appdistribution.impl.FirebaseAppDistributionLifecycleNotifier.ActivityConsumer;
3233
import com.google.firebase.appdistribution.impl.FirebaseAppDistributionLifecycleNotifier.ActivityFunction;
3334
import com.google.firebase.appdistribution.impl.FirebaseAppDistributionLifecycleNotifier.NullableActivityFunction;
35+
import com.google.firebase.concurrent.TestOnlyExecutors;
36+
import java.util.concurrent.Executor;
3437
import org.junit.Before;
3538
import org.junit.Test;
3639
import org.junit.runner.RunWith;
@@ -47,6 +50,8 @@ static class TestActivity extends Activity {}
4750

4851
static class OtherTestActivity extends Activity {}
4952

53+
private final @UiThread Executor uiThreadExecutor = TestOnlyExecutors.ui();
54+
5055
@Captor private ArgumentCaptor<Activity> activityArgCaptor;
5156
private TestActivity activity;
5257
private OtherTestActivity otherActivity;
@@ -57,7 +62,7 @@ public void setup() {
5762
MockitoAnnotations.initMocks(this);
5863
activity = Robolectric.buildActivity(TestActivity.class).create().get();
5964
otherActivity = Robolectric.buildActivity(OtherTestActivity.class).create().get();
60-
lifecycleNotifier = new FirebaseAppDistributionLifecycleNotifier();
65+
lifecycleNotifier = new FirebaseAppDistributionLifecycleNotifier(uiThreadExecutor);
6166
}
6267

6368
@Test

0 commit comments

Comments
 (0)