Skip to content

Commit e452ad6

Browse files
committed
Remove synchronized blocks from ApkInstaller
1 parent 33a722b commit e452ad6

File tree

5 files changed

+163
-65
lines changed

5 files changed

+163
-65
lines changed

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

Lines changed: 20 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,46 +14,35 @@
1414

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

17-
import static com.google.firebase.appdistribution.impl.TaskUtils.safeSetTaskException;
18-
1917
import android.app.Activity;
2018
import android.content.Intent;
21-
import androidx.annotation.GuardedBy;
2219
import androidx.annotation.Nullable;
2320
import com.google.android.gms.tasks.Task;
2421
import com.google.android.gms.tasks.TaskCompletionSource;
22+
import com.google.firebase.annotations.concurrent.Lightweight;
2523
import com.google.firebase.appdistribution.FirebaseAppDistribution;
2624
import com.google.firebase.appdistribution.FirebaseAppDistributionException;
25+
import java.util.concurrent.Executor;
2726

2827
/** Class that handles installing APKs in {@link FirebaseAppDistribution}. */
2928
class ApkInstaller {
3029
private static final String TAG = "ApkInstaller";
3130

3231
private final FirebaseAppDistributionLifecycleNotifier lifeCycleNotifier;
32+
private final TaskCompletionSourceCache<Void> installTaskCompletionSourceCache;
33+
private final @Lightweight Executor lightweightExecutor;
3334

34-
@GuardedBy("installTaskLock")
35-
private TaskCompletionSource<Void> installTaskCompletionSource;
36-
37-
private final Object installTaskLock = new Object();
38-
39-
ApkInstaller(FirebaseAppDistributionLifecycleNotifier lifeCycleNotifier) {
35+
ApkInstaller(
36+
FirebaseAppDistributionLifecycleNotifier lifeCycleNotifier,
37+
@Lightweight Executor lightweightExecutor) {
4038
this.lifeCycleNotifier = lifeCycleNotifier;
41-
lifeCycleNotifier.addOnActivityStartedListener(this::onActivityStarted);
39+
this.installTaskCompletionSourceCache = new TaskCompletionSourceCache<>(lightweightExecutor);
40+
this.lightweightExecutor = lightweightExecutor;
4241
lifeCycleNotifier.addOnActivityDestroyedListener(this::onActivityDestroyed);
4342
}
4443

45-
ApkInstaller() {
46-
this(FirebaseAppDistributionLifecycleNotifier.getInstance());
47-
}
48-
49-
void onActivityStarted(@Nullable Activity activity) {
50-
synchronized (installTaskLock) {
51-
if (installTaskCompletionSource == null
52-
|| installTaskCompletionSource.getTask().isComplete()
53-
|| activity == null) {
54-
return;
55-
}
56-
}
44+
ApkInstaller(@Lightweight Executor lightweightExecutor) {
45+
this(FirebaseAppDistributionLifecycleNotifier.getInstance(), lightweightExecutor);
5746
}
5847

5948
void onActivityDestroyed(@Nullable Activity activity) {
@@ -65,15 +54,11 @@ void onActivityDestroyed(@Nullable Activity activity) {
6554
}
6655

6756
Task<Void> installApk(String path, Activity currentActivity) {
68-
synchronized (installTaskLock) {
69-
startInstallActivity(path, currentActivity);
70-
71-
if (this.installTaskCompletionSource == null
72-
|| this.installTaskCompletionSource.getTask().isComplete()) {
73-
this.installTaskCompletionSource = new TaskCompletionSource<>();
74-
}
75-
return installTaskCompletionSource.getTask();
76-
}
57+
return installTaskCompletionSourceCache.getOrCreateTaskFromCompletionSource(
58+
() -> {
59+
startInstallActivity(path, currentActivity);
60+
return new TaskCompletionSource<>();
61+
});
7762
}
7863

7964
private void startInstallActivity(String path, Activity currentActivity) {
@@ -84,12 +69,9 @@ private void startInstallActivity(String path, Activity currentActivity) {
8469
}
8570

8671
void trySetInstallTaskError() {
87-
synchronized (installTaskLock) {
88-
safeSetTaskException(
89-
installTaskCompletionSource,
90-
new FirebaseAppDistributionException(
91-
ErrorMessages.APK_INSTALLATION_FAILED,
92-
FirebaseAppDistributionException.Status.INSTALLATION_FAILURE));
93-
}
72+
installTaskCompletionSourceCache.setException(
73+
new FirebaseAppDistributionException(
74+
ErrorMessages.APK_INSTALLATION_FAILED,
75+
FirebaseAppDistributionException.Status.INSTALLATION_FAILURE));
9476
}
9577
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ private FirebaseAppDistribution buildFirebaseAppDistribution(
121121
firebaseApp, firebaseInstallationsApiProvider, signInStorage, blockingExecutor),
122122
new NewReleaseFetcher(
123123
firebaseApp.getApplicationContext(), testerApiClient, releaseIdentifier),
124-
new ApkUpdater(firebaseApp, new ApkInstaller(), blockingExecutor),
124+
new ApkUpdater(firebaseApp, new ApkInstaller(lightweightExecutor), blockingExecutor),
125125
new AabUpdater(blockingExecutor),
126126
signInStorage,
127127
lifecycleNotifier,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright 2022 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.impl;
16+
17+
import com.google.android.gms.tasks.Task;
18+
import com.google.android.gms.tasks.TaskCompletionSource;
19+
import com.google.firebase.annotations.concurrent.Lightweight;
20+
import com.google.firebase.concurrent.FirebaseExecutors;
21+
import java.util.concurrent.Executor;
22+
23+
/**
24+
* A cache for a {@link TaskCompletionSource}, for use in cases where we only ever want one active
25+
* task at a time for a particular operation.
26+
*
27+
* <p>This is equivalent to {@link UpdateTaskCache} but for Tasks.
28+
*/
29+
class TaskCompletionSourceCache<T> {
30+
31+
/** A functional interface for a producer of a new {@link TaskCompletionSource}. */
32+
@FunctionalInterface
33+
interface TaskCompletionSourceProducer<T> {
34+
35+
/** Produce a new {@link TaskCompletionSource}. */
36+
TaskCompletionSource<T> produce();
37+
}
38+
39+
private TaskCompletionSource<T> cachedTaskCompletionSource;
40+
private final Executor sequentialExecutor;
41+
42+
/**
43+
* Constructor for a {@link TaskCompletionSourceCache} that controls access using its own
44+
* sequential executor backed by the given base executor.
45+
*
46+
* @param baseExecutor Executor (typically {@link Lightweight}) to back the sequential executor.
47+
*/
48+
TaskCompletionSourceCache(Executor baseExecutor) {
49+
sequentialExecutor = FirebaseExecutors.newSequentialExecutor(baseExecutor);
50+
}
51+
52+
/**
53+
* Gets a cached {@link TaskCompletionSource}, if there is one and it is not completed, or else
54+
* calls the given {@code producer} and caches the return value.
55+
*/
56+
Task<T> getOrCreateTaskFromCompletionSource(TaskCompletionSourceProducer<T> producer) {
57+
TaskCompletionSource<T> taskCompletionSource = new TaskCompletionSource<>();
58+
sequentialExecutor.execute(
59+
() -> {
60+
if (!isOngoing(cachedTaskCompletionSource)) {
61+
cachedTaskCompletionSource = producer.produce();
62+
}
63+
TaskUtils.shadowTask(taskCompletionSource, cachedTaskCompletionSource.getTask());
64+
});
65+
return taskCompletionSource.getTask();
66+
}
67+
68+
/**
69+
* Sets the result on the cached {@link TaskCompletionSource}, if there is one and it is not
70+
* completed.
71+
*/
72+
Task<Void> setResult(T result) {
73+
TaskCompletionSource<Void> taskCompletionSource = new TaskCompletionSource<>();
74+
sequentialExecutor.execute(
75+
() -> {
76+
if (isOngoing(cachedTaskCompletionSource)) {
77+
cachedTaskCompletionSource.setResult(result);
78+
}
79+
});
80+
return taskCompletionSource.getTask();
81+
}
82+
83+
/**
84+
* Sets the exception on the cached {@link TaskCompletionSource}, if there is one and it is not
85+
* completed.
86+
*/
87+
Task<Void> setException(Exception e) {
88+
TaskCompletionSource<Void> taskCompletionSource = new TaskCompletionSource<>();
89+
sequentialExecutor.execute(
90+
() -> {
91+
if (isOngoing(cachedTaskCompletionSource)) {
92+
cachedTaskCompletionSource.setException(e);
93+
}
94+
});
95+
return taskCompletionSource.getTask();
96+
}
97+
98+
private static <T> boolean isOngoing(TaskCompletionSource<T> taskCompletionSource) {
99+
return taskCompletionSource != null && !taskCompletionSource.getTask().isComplete();
100+
}
101+
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.google.firebase.appdistribution.FirebaseAppDistributionException;
2222
import com.google.firebase.appdistribution.FirebaseAppDistributionException.Status;
2323
import com.google.firebase.appdistribution.UpdateTask;
24+
import com.google.firebase.concurrent.FirebaseExecutors;
2425
import java.util.concurrent.Executor;
2526

2627
class TaskUtils {
@@ -144,5 +145,12 @@ static <T> UpdateTask onSuccessUpdateTask(
144145
return updateTask;
145146
}
146147

148+
/** Set a {@link TaskCompletionSource} to be resolved with the result of another {@link Task}. */
149+
static <T> void shadowTask(TaskCompletionSource<T> taskCompletionSource, Task<T> task) {
150+
task.addOnSuccessListener(FirebaseExecutors.directExecutor(), taskCompletionSource::setResult)
151+
.addOnFailureListener(
152+
FirebaseExecutors.directExecutor(), taskCompletionSource::setException);
153+
}
154+
147155
private TaskUtils() {}
148156
}

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

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

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

17+
import static com.google.firebase.appdistribution.FirebaseAppDistributionException.Status.INSTALLATION_FAILURE;
18+
import static com.google.firebase.appdistribution.impl.TestUtils.awaitAsyncOperations;
19+
import static com.google.firebase.appdistribution.impl.TestUtils.awaitTaskFailure;
1720
import static org.junit.Assert.assertEquals;
1821
import static org.junit.Assert.assertFalse;
22+
import static org.mockito.ArgumentMatchers.any;
23+
import static org.mockito.Mockito.doNothing;
24+
import static org.mockito.Mockito.mock;
25+
import static org.mockito.Mockito.times;
26+
import static org.mockito.Mockito.verify;
1927
import static org.robolectric.Shadows.shadowOf;
2028

29+
import android.app.Activity;
2130
import android.content.Intent;
2231
import com.google.android.gms.tasks.Task;
32+
import com.google.firebase.annotations.concurrent.Lightweight;
2333
import com.google.firebase.appdistribution.FirebaseAppDistributionException;
2434
import com.google.firebase.appdistribution.impl.FirebaseAppDistributionServiceImplTest.TestActivity;
35+
import com.google.firebase.concurrent.TestOnlyExecutors;
36+
import java.util.concurrent.ExecutionException;
37+
import java.util.concurrent.ExecutorService;
2538
import org.junit.Before;
2639
import org.junit.Test;
2740
import org.junit.runner.RunWith;
@@ -37,58 +50,52 @@ public class ApkInstallerTests {
3750
private ShadowActivity shadowActivity;
3851
private ApkInstaller apkInstaller;
3952
@Mock private FirebaseAppDistributionLifecycleNotifier mockLifecycleNotifier;
53+
@Lightweight private ExecutorService lightweightExecutor;
4054

4155
@Before
4256
public void setUp() {
4357
MockitoAnnotations.initMocks(this);
4458

4559
activity = Robolectric.buildActivity(TestActivity.class).create().get();
4660
shadowActivity = shadowOf(activity);
47-
apkInstaller = new ApkInstaller(mockLifecycleNotifier);
61+
lightweightExecutor = TestOnlyExecutors.lite();
62+
apkInstaller = new ApkInstaller(mockLifecycleNotifier, lightweightExecutor);
4863
}
4964

5065
@Test
51-
public void installApk_currentActivityNotNull_InstallIntentOnCurrentActivity() {
66+
public void installApk_currentActivityNotNull_InstallIntentOnCurrentActivity()
67+
throws InterruptedException {
5268
String path = "path";
5369
Task<Void> installTask = apkInstaller.installApk(path, activity);
70+
awaitAsyncOperations(lightweightExecutor);
5471
Intent installIntent = shadowActivity.getNextStartedActivity();
5572

5673
assertFalse(installTask.isComplete());
5774
assertEquals(
5875
"com.google.firebase.appdistribution.impl.InstallActivity",
5976
installIntent.getComponent().getShortClassName());
6077
assertEquals(path, installIntent.getExtras().get("INSTALL_PATH"));
61-
}
62-
63-
@Test
64-
public void
65-
setCurrentActivity_appInForegroundAfterAnInstallAttempt_installIntentOnCurrentActivity() {
66-
apkInstaller.onActivityStarted(null);
67-
String path = "path123";
68-
Task<Void> installTask = apkInstaller.installApk(path, activity);
69-
apkInstaller.onActivityStarted(activity);
70-
71-
Intent installIntent = shadowActivity.getNextStartedActivity();
72-
73-
assertEquals(
74-
"com.google.firebase.appdistribution.impl.InstallActivity",
75-
installIntent.getComponent().getShortClassName());
76-
assertEquals(path, installIntent.getExtras().get("INSTALL_PATH"));
7778
assertFalse(installTask.isComplete());
7879
}
7980

8081
@Test
8182
public void installActivityDestroyed_setsInstallError() {
8283
Task<Void> installTask = apkInstaller.installApk("path", activity);
8384
apkInstaller.onActivityDestroyed(new InstallActivity());
84-
Exception ex = installTask.getException();
8585

86-
assert ex instanceof FirebaseAppDistributionException;
87-
assertEquals(
88-
ErrorMessages.APK_INSTALLATION_FAILED,
89-
((FirebaseAppDistributionException) ex).getMessage());
90-
assertEquals(
91-
FirebaseAppDistributionException.Status.INSTALLATION_FAILURE,
92-
((FirebaseAppDistributionException) ex).getErrorCode());
86+
awaitTaskFailure(installTask, INSTALLATION_FAILURE, ErrorMessages.APK_INSTALLATION_FAILED);
87+
}
88+
89+
@Test
90+
public void whenCalledMultipleTimes_onlyEmitsOneIntent()
91+
throws FirebaseAppDistributionException, ExecutionException, InterruptedException {
92+
Activity mockActivity = mock(Activity.class);
93+
doNothing().when(mockActivity).startActivity(any());
94+
95+
apkInstaller.installApk("path", mockActivity);
96+
apkInstaller.installApk("path", mockActivity);
97+
awaitAsyncOperations(lightweightExecutor);
98+
99+
verify(mockActivity, times(1)).startActivity(any());
93100
}
94101
}

0 commit comments

Comments
 (0)