Skip to content

Commit e585921

Browse files
committed
Add getForegroundActivity overload with consumer
1 parent aafced0 commit e585921

File tree

8 files changed

+190
-101
lines changed

8 files changed

+190
-101
lines changed

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
import static com.google.firebase.appdistribution.FirebaseAppDistributionException.Status.DOWNLOAD_FAILURE;
1818
import static com.google.firebase.appdistribution.FirebaseAppDistributionException.Status.NETWORK_FAILURE;
19-
import static com.google.firebase.appdistribution.TaskUtils.combineWithResultOf;
2019
import static com.google.firebase.appdistribution.TaskUtils.runAsyncInTask;
2120
import static com.google.firebase.appdistribution.TaskUtils.safeSetTaskException;
2221

@@ -97,10 +96,10 @@ UpdateTaskImpl updateAab(@NonNull AppDistributionReleaseInternal newRelease) {
9796

9897
// On a background thread, fetch the redirect URL and open it in the Play app
9998
runAsyncInTask(executor, () -> fetchDownloadRedirectUrl(newRelease.getDownloadUrl()))
100-
.onSuccessTask(combineWithResultOf(() -> lifecycleNotifier.getForegroundActivity()))
101-
.addOnSuccessListener(
102-
urlAndActivity ->
103-
openRedirectUrlInPlay(urlAndActivity.first(), urlAndActivity.second()))
99+
.onSuccessTask(
100+
redirectUrl ->
101+
lifecycleNotifier.getForegroundActivity(
102+
activity -> openRedirectUrlInPlay(redirectUrl, activity)))
104103
.addOnFailureListener(this::setUpdateTaskCompletionError);
105104

106105
return cachedUpdateTask;

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

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,19 @@
2020
import androidx.annotation.GuardedBy;
2121
import androidx.annotation.NonNull;
2222
import androidx.annotation.Nullable;
23+
import androidx.annotation.VisibleForTesting;
2324
import com.google.android.gms.tasks.Task;
2425
import com.google.android.gms.tasks.TaskCompletionSource;
25-
import com.google.android.gms.tasks.Tasks;
2626
import java.util.ArrayDeque;
2727
import java.util.Queue;
2828

2929
class FirebaseAppDistributionLifecycleNotifier implements Application.ActivityLifecycleCallbacks {
3030

31+
/** A functional interface for a function that takes an activity and does something with it. */
32+
interface ActivityConsumer {
33+
void consume(Activity activity) throws FirebaseAppDistributionException;
34+
}
35+
3136
private static FirebaseAppDistributionLifecycleNotifier instance;
3237
private final Object lock = new Object();
3338

@@ -54,7 +59,8 @@ class FirebaseAppDistributionLifecycleNotifier implements Application.ActivityLi
5459
@GuardedBy("lock")
5560
private final Queue<OnActivityDestroyedListener> onDestroyedListeners = new ArrayDeque<>();
5661

57-
private FirebaseAppDistributionLifecycleNotifier() {}
62+
@VisibleForTesting
63+
FirebaseAppDistributionLifecycleNotifier() {}
5864

5965
static synchronized FirebaseAppDistributionLifecycleNotifier getInstance() {
6066
if (instance == null) {
@@ -91,25 +97,47 @@ interface OnActivityDestroyedListener {
9197
* activity comes to the foreground.
9298
*/
9399
Task<Activity> getForegroundActivity() {
100+
return getForegroundActivity(activity -> {});
101+
}
102+
103+
/**
104+
* Get a {@link Task} that will succeed with a result of the app's foregrounded {@link Activity},
105+
* when one is available, after passing the activity to an {@link ActivityConsumer}.
106+
*
107+
* <p>The returned task will fail with a {@link FirebaseAppDistributionException} if the consumer
108+
* throws. Otherwise it will never fail, and will wait indefinitely for a foreground activity
109+
* before applying the consumer.
110+
*/
111+
Task<Activity> getForegroundActivity(ActivityConsumer consumer) {
94112
synchronized (lock) {
113+
TaskCompletionSource<Activity> task = new TaskCompletionSource<>();
95114
if (currentActivity != null) {
96-
return Tasks.forResult(currentActivity);
115+
consumeActivityAndCompleteTask(task, currentActivity, consumer);
116+
} else {
117+
addOnActivityResumedListener(
118+
new OnActivityResumedListener() {
119+
@Override
120+
public void onResumed(Activity activity) {
121+
consumeActivityAndCompleteTask(task, activity, consumer);
122+
removeOnActivityResumedListener(this);
123+
}
124+
});
97125
}
98-
TaskCompletionSource<Activity> task = new TaskCompletionSource<>();
99-
100-
addOnActivityResumedListener(
101-
new OnActivityResumedListener() {
102-
@Override
103-
public void onResumed(Activity activity) {
104-
task.setResult(activity);
105-
removeOnActivityResumedListener(this);
106-
}
107-
});
108126

109127
return task.getTask();
110128
}
111129
}
112130

131+
void consumeActivityAndCompleteTask(
132+
TaskCompletionSource task, Activity activity, ActivityConsumer consumer) {
133+
try {
134+
consumer.consume(activity);
135+
task.setResult(activity);
136+
} catch (Throwable t) {
137+
task.setException(FirebaseAppDistributionException.wrap(t));
138+
}
139+
}
140+
113141
void addOnActivityCreatedListener(@NonNull OnActivityCreatedListener listener) {
114142
synchronized (lock) {
115143
this.onActivityCreatedListeners.add(listener);

firebase-appdistribution/src/main/java/com/google/firebase/appdistribution/TaskUtils.java

Lines changed: 0 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,9 @@
1414

1515
package com.google.firebase.appdistribution;
1616

17-
import com.google.android.gms.tasks.SuccessContinuation;
1817
import com.google.android.gms.tasks.Task;
1918
import com.google.android.gms.tasks.TaskCompletionSource;
2019
import com.google.android.gms.tasks.Tasks;
21-
import com.google.auto.value.AutoValue;
2220
import com.google.firebase.appdistribution.FirebaseAppDistributionException.Status;
2321
import com.google.firebase.appdistribution.internal.LogWrapper;
2422
import java.util.concurrent.Executor;
@@ -34,11 +32,6 @@ interface Operation<TResult> {
3432
TResult run() throws FirebaseAppDistributionException;
3533
}
3634

37-
/** A functional interface to wrap a function that produces a {@link Task}. */
38-
interface TaskSource<TResult> {
39-
Task<TResult> get();
40-
}
41-
4235
/**
4336
* Runs a long running operation inside a {@link Task}, wrapping any errors in {@link
4437
* FirebaseAppDistributionException}.
@@ -92,58 +85,6 @@ static <TResult> Task<TResult> handleTaskFailure(Task<TResult> task) {
9285
return task;
9386
}
9487

95-
/**
96-
* An @{link AutoValue} class to hold the result of two Tasks, combined using {@link
97-
* #combineWithResultOf}.
98-
*
99-
* @param <T1> The result type of the first task
100-
* @param <T2> The result type of the second task
101-
*/
102-
@AutoValue
103-
abstract static class CombinedTaskResults<T1, T2> {
104-
abstract T1 first();
105-
106-
abstract T2 second();
107-
108-
static <T1, T2> CombinedTaskResults<T1, T2> create(T1 first, T2 second) {
109-
return new AutoValue_TaskUtils_CombinedTaskResults(first, second);
110-
}
111-
}
112-
113-
/**
114-
* Returns a {@link SuccessContinuation} to be chained off of a {@link Task}, that will run
115-
* another task in sequence and combine both results together.
116-
*
117-
* <p>This is useful when you want to run two tasks and use the results of each, but those tasks
118-
* need to be run sequentially. If they can be run in parallel, use {@link Tasks#whenAll} or one
119-
* of its variations.
120-
*
121-
* <p>Usage:
122-
*
123-
* <pre>{@code
124-
* runFirstAsyncTask()
125-
* .onSuccessTask(combineWithResultOf(executor, () -> startSecondAsyncTask())
126-
* .addOnSuccessListener(
127-
* results ->
128-
* doSomethingWithBothResults(results.result1(), results.result2()));
129-
* }</pre>
130-
*
131-
* @param secondTaskSource A {@link TaskSource} providing the next task to run
132-
* @param <T1> The result type of the first task
133-
* @param <T2> The result type of the second task
134-
* @return A {@link SuccessContinuation} that will return a new task with result type {@link
135-
* CombinedTaskResults}, combining the results of both tasks
136-
*/
137-
static <T1, T2> SuccessContinuation<T1, CombinedTaskResults<T1, T2>> combineWithResultOf(
138-
TaskSource<T2> secondTaskSource) {
139-
return firstResult ->
140-
secondTaskSource
141-
.get()
142-
.onSuccessTask(
143-
secondResult ->
144-
Tasks.forResult(CombinedTaskResults.create(firstResult, secondResult)));
145-
}
146-
14788
static void safeSetTaskException(TaskCompletionSource taskCompletionSource, Exception e) {
14889
if (taskCompletionSource != null && !taskCompletionSource.getTask().isComplete()) {
14990
taskCompletionSource.setException(e);

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

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
package com.google.firebase.appdistribution;
1616

1717
import static com.google.firebase.appdistribution.FirebaseAppDistributionException.Status.AUTHENTICATION_CANCELED;
18-
import static com.google.firebase.appdistribution.TaskUtils.combineWithResultOf;
1918
import static com.google.firebase.appdistribution.TaskUtils.safeSetTaskException;
2019
import static com.google.firebase.appdistribution.TaskUtils.safeSetTaskResult;
2120

@@ -141,28 +140,30 @@ public Task<Void> signInTester() {
141140
.getId()
142141
.addOnFailureListener(
143142
handleTaskFailure(ErrorMessages.AUTHENTICATION_ERROR, Status.AUTHENTICATION_FAILURE))
144-
.onSuccessTask(combineWithResultOf(() -> lifecycleNotifier.getForegroundActivity()))
145-
.addOnSuccessListener(
146-
fidAndActivity -> {
147-
// Launch the intent outside of the synchronized block because we don't need to wait
148-
// for the lock, and we don't want to risk the activity leaving the foreground in
149-
// the meantime.
150-
openSignInFlowInBrowser(fidAndActivity.first(), fidAndActivity.second());
151-
// This synchronized block is required by the @GuardedBy annotation, but is not
152-
// practically required in this case because the only reads of this variable are on
153-
// the main thread, which this callback is also running on.
154-
synchronized (signInTaskLock) {
155-
hasBeenSentToBrowserForCurrentTask = true;
156-
}
157-
})
158-
// No failures expected here, since getForegroundActivity() will wait indefinitely for a
159-
// foreground activity, but catch any unexpected failures to be safe.
143+
.onSuccessTask(this::getForegroundActivityAndOpenSignInFlow)
144+
// Catch any unexpected failures to be safe.
160145
.addOnFailureListener(handleTaskFailure(ErrorMessages.UNKNOWN_ERROR, Status.UNKNOWN));
161146

162147
return signInTaskCompletionSource.getTask();
163148
}
164149
}
165150

151+
private Task<Activity> getForegroundActivityAndOpenSignInFlow(String fid) {
152+
return lifecycleNotifier.getForegroundActivity(
153+
activity -> {
154+
// Launch the intent outside of the synchronized block because we don't need to wait
155+
// for the lock, and we don't want to risk the activity leaving the foreground in
156+
// the meantime.
157+
openSignInFlowInBrowser(fid, activity);
158+
// This synchronized block is required by the @GuardedBy annotation, but is not
159+
// practically required in this case because the only reads of this variable are on
160+
// the main thread, which this callback is also running on.
161+
synchronized (signInTaskLock) {
162+
hasBeenSentToBrowserForCurrentTask = true;
163+
}
164+
});
165+
}
166+
166167
private OnFailureListener handleTaskFailure(String message, Status status) {
167168
return e -> {
168169
LogWrapper.getInstance().e(TAG + message, e);

firebase-appdistribution/src/test/java/com/google/firebase/appdistribution/AabUpdaterTest.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@
1616
import static com.google.common.truth.Truth.assertThat;
1717
import static com.google.firebase.appdistribution.TestUtils.assertTaskFailure;
1818
import static com.google.firebase.appdistribution.TestUtils.awaitAsyncOperations;
19+
import static com.google.firebase.appdistribution.TestUtils.getForegroundActivityAnswer;
1920
import static org.junit.Assert.assertEquals;
21+
import static org.mockito.ArgumentMatchers.any;
2022
import static org.mockito.Mockito.when;
2123
import static org.robolectric.Shadows.shadowOf;
2224
import static org.robolectric.annotation.LooperMode.Mode.PAUSED;
2325

2426
import android.app.Activity;
2527
import android.net.Uri;
26-
import com.google.android.gms.tasks.Tasks;
2728
import com.google.firebase.FirebaseApp;
2829
import com.google.firebase.appdistribution.Constants.ErrorMessages;
2930
import com.google.firebase.appdistribution.FirebaseAppDistributionException.Status;
@@ -88,7 +89,9 @@ public void setup() throws IOException, FirebaseAppDistributionException {
8889
aabUpdater =
8990
Mockito.spy(
9091
new AabUpdater(mockLifecycleNotifier, mockHttpsUrlConnectionFactory, testExecutor));
91-
when(mockLifecycleNotifier.getForegroundActivity()).thenReturn(Tasks.forResult(activity));
92+
93+
when(mockLifecycleNotifier.getForegroundActivity(any()))
94+
.thenAnswer(getForegroundActivityAnswer(activity));
9295
}
9396

9497
@Test
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.appdistribution;
16+
17+
import static com.google.common.truth.Truth.assertThat;
18+
import static org.mockito.Mockito.spy;
19+
import static org.mockito.Mockito.verify;
20+
21+
import android.app.Activity;
22+
import com.google.android.gms.tasks.Task;
23+
import com.google.firebase.appdistribution.FirebaseAppDistributionLifecycleNotifier.ActivityConsumer;
24+
import org.junit.Before;
25+
import org.junit.Test;
26+
import org.junit.runner.RunWith;
27+
import org.robolectric.Robolectric;
28+
import org.robolectric.RobolectricTestRunner;
29+
30+
@RunWith(RobolectricTestRunner.class)
31+
public class FirebaseAppDistributionLifecycleNotifierTest {
32+
private TestActivity activity;
33+
private FirebaseAppDistributionLifecycleNotifier lifecycleNotifier;
34+
35+
static class TestActivity extends Activity {}
36+
37+
@Before
38+
public void setup() {
39+
activity = Robolectric.buildActivity(TestActivity.class).create().get();
40+
lifecycleNotifier = new FirebaseAppDistributionLifecycleNotifier();
41+
}
42+
43+
@Test
44+
public void getForegroundActivity_whenActivityResumes_succeeds() {
45+
Task<Activity> task = lifecycleNotifier.getForegroundActivity();
46+
assertThat(task.isComplete()).isFalse();
47+
48+
// Simulate an activity resuming
49+
lifecycleNotifier.onActivityResumed(activity);
50+
51+
assertThat(task.isComplete()).isTrue();
52+
assertThat(task.isSuccessful()).isTrue();
53+
assertThat(task.getResult()).isEqualTo(activity);
54+
}
55+
56+
@Test
57+
public void getForegroundActivity_withCurrentActivity_succeeds() {
58+
// Resume an activity so there is a current foreground activity already when
59+
// getForegroundActivity is called
60+
lifecycleNotifier.onActivityResumed(activity);
61+
62+
Task<Activity> task = lifecycleNotifier.getForegroundActivity();
63+
64+
assertThat(task.isComplete()).isTrue();
65+
assertThat(task.isSuccessful()).isTrue();
66+
assertThat(task.getResult()).isEqualTo(activity);
67+
}
68+
69+
@Test
70+
public void getForegroundActivity_withConsumer_succeedsAndCallsConsumer()
71+
throws FirebaseAppDistributionException {
72+
ActivityConsumer consumer = spy(ActivityConsumer.class);
73+
Task<Activity> task = lifecycleNotifier.getForegroundActivity(consumer);
74+
75+
// Simulate an activity resuming
76+
lifecycleNotifier.onActivityResumed(activity);
77+
78+
assertThat(task.isComplete()).isTrue();
79+
assertThat(task.isSuccessful()).isTrue();
80+
assertThat(task.getResult()).isEqualTo(activity);
81+
verify(consumer).consume(activity);
82+
}
83+
84+
@Test
85+
public void getForegroundActivity_withConsumerAndCurrentActivity_succeedsAndCallsConsumer()
86+
throws FirebaseAppDistributionException {
87+
// Resume an activity so there is a current foreground activity already when
88+
// getForegroundActivity is called
89+
lifecycleNotifier.onActivityResumed(activity);
90+
91+
ActivityConsumer consumer = spy(ActivityConsumer.class);
92+
Task<Activity> task = lifecycleNotifier.getForegroundActivity(consumer);
93+
94+
assertThat(task.isComplete()).isTrue();
95+
assertThat(task.isSuccessful()).isTrue();
96+
assertThat(task.getResult()).isEqualTo(activity);
97+
verify(consumer).consume(activity);
98+
}
99+
}

0 commit comments

Comments
 (0)