Skip to content

Extract APK hash on the background thread #4534

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,9 @@ private FirebaseAppDistribution buildFirebaseAppDistribution(
SignInStorage signInStorage = new SignInStorage(context, backgroundExecutor);
FirebaseAppDistributionLifecycleNotifier lifecycleNotifier =
FirebaseAppDistributionLifecycleNotifier.getInstance(uiThreadExecutor);
ReleaseIdentifier releaseIdentifier = new ReleaseIdentifier(firebaseApp, testerApiClient);
ReleaseIdentifier releaseIdentifier =
new ReleaseIdentifier(
firebaseApp, testerApiClient, backgroundExecutor, lightweightExecutor);
FirebaseAppDistribution appDistribution =
new FirebaseAppDistributionImpl(
firebaseApp,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,31 +62,44 @@ public Task<AppDistributionReleaseInternal> checkForNewRelease() {
.fetchNewRelease()
.onSuccessTask(
lightweightExecutor,
release -> Tasks.forResult(isNewerRelease(release) ? release : null)));
release ->
isNewerRelease(release)
.onSuccessTask(
lightweightExecutor,
isNewer -> Tasks.forResult(isNewer ? release : null))));
}

private boolean isNewerRelease(AppDistributionReleaseInternal retrievedNewRelease)
private Task<Boolean> isNewerRelease(AppDistributionReleaseInternal retrievedNewRelease)
throws FirebaseAppDistributionException {
if (retrievedNewRelease == null) {
LogWrapper.v(TAG, "Tester does not have access to any releases");
return false;
return Tasks.forResult(false);
}

long newReleaseBuildVersion = parseBuildVersion(retrievedNewRelease.getBuildVersion());

if (isOlderBuildVersion(newReleaseBuildVersion)) {
LogWrapper.v(TAG, "New release has lower version code than current release");
return false;
return Tasks.forResult(false);
}

if (isNewerBuildVersion(newReleaseBuildVersion)
|| !isSameAsInstalledRelease(retrievedNewRelease)
|| hasDifferentAppVersionName(retrievedNewRelease)) {
return true;
} else {
LogWrapper.v(TAG, "New release is older or is currently installed");
return false;
return Tasks.forResult(true);
}

return hasSameBinaryIdentifier(retrievedNewRelease)
.onSuccessTask(
lightweightExecutor,
hasSameBinaryIdentifier -> {
if (hasSameBinaryIdentifier) {
// The release has the same app version name and binary identifier, and has either
// an equal or older build version
LogWrapper.v(TAG, "New release is older or is currently installed");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is "older" always correct? Could be "newer" as well, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah sorry I meant to clarify this because I knew it was misleading. It can be "older" in that if we can return false here only if the release has the same app version name, and the same APK hash, and has either an equal or older build version. If the build version was newer it would have returned true in the condition above.

I've added a comment and renamed some methods to make it clearer what's going on here.

return Tasks.forResult(false);
}
return Tasks.forResult(true);
});
}

private long parseBuildVersion(String buildVersion) throws FirebaseAppDistributionException {
Expand Down Expand Up @@ -114,12 +127,15 @@ private boolean hasDifferentAppVersionName(AppDistributionReleaseInternal newRel
}

@VisibleForTesting
boolean isSameAsInstalledRelease(AppDistributionReleaseInternal newRelease)
Task<Boolean> hasSameBinaryIdentifier(AppDistributionReleaseInternal newRelease)
throws FirebaseAppDistributionException {
if (newRelease.getBinaryType().equals(BinaryType.APK)) {
return hasSameHashAsInstalledRelease(newRelease);
}
return Tasks.forResult(hasSameIasArtifactId(newRelease));
}

boolean hasSameIasArtifactId(AppDistributionReleaseInternal newRelease) {
if (newRelease.getIasArtifactId() == null || newRelease.getIasArtifactId().isEmpty()) {
LogWrapper.w(TAG, "AAB release missing IAS Artifact ID. Assuming new release is different.");
return false;
Expand All @@ -146,12 +162,17 @@ private String getInstalledAppVersionName(Context context)
return getPackageInfo(context).versionName;
}

private boolean hasSameHashAsInstalledRelease(AppDistributionReleaseInternal newRelease)
private Task<Boolean> hasSameHashAsInstalledRelease(AppDistributionReleaseInternal newRelease)
throws FirebaseAppDistributionException {
if (newRelease.getApkHash().isEmpty()) {
throw new FirebaseAppDistributionException(
"Missing APK hash from new release", Status.UNKNOWN);
return Tasks.forException(
new FirebaseAppDistributionException(
"Missing APK hash from new release", Status.UNKNOWN));
}
return releaseIdentifier.extractApkHash().equals(newRelease.getApkHash());
return releaseIdentifier
.extractApkHash()
.onSuccessTask(
lightweightExecutor,
apkHash -> Tasks.forResult(apkHash.equals(newRelease.getApkHash())));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package com.google.firebase.appdistribution.impl;

import static com.google.firebase.appdistribution.impl.PackageInfoUtils.getPackageInfoWithMetadata;
import static com.google.firebase.appdistribution.impl.TaskUtils.runAsyncInTask;

import android.content.pm.PackageInfo;
import androidx.annotation.NonNull;
Expand All @@ -23,6 +24,8 @@
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.FirebaseApp;
import com.google.firebase.annotations.concurrent.Background;
import com.google.firebase.annotations.concurrent.Lightweight;
import com.google.firebase.appdistribution.FirebaseAppDistributionException;
import com.google.firebase.appdistribution.FirebaseAppDistributionException.Status;
import java.io.File;
Expand All @@ -36,6 +39,7 @@
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executor;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

Expand All @@ -51,10 +55,18 @@ class ReleaseIdentifier {
private final FirebaseApp firebaseApp;
private final FirebaseAppDistributionTesterApiClient testerApiClient;

@Background private final Executor backgroundExecutor;
@Lightweight private final Executor lightweightExecutor;

ReleaseIdentifier(
FirebaseApp firebaseApp, FirebaseAppDistributionTesterApiClient testerApiClient) {
FirebaseApp firebaseApp,
FirebaseAppDistributionTesterApiClient testerApiClient,
@Background Executor backgroundExecutor,
@Lightweight Executor lightweightExecutor) {
this.firebaseApp = firebaseApp;
this.testerApiClient = testerApiClient;
this.backgroundExecutor = backgroundExecutor;
this.lightweightExecutor = lightweightExecutor;
}

/**
Expand Down Expand Up @@ -82,13 +94,8 @@ Task<String> identifyRelease() {
}

// If we can't find an IAS artifact ID, we assume the installed release is an APK
String apkHash;
try {
apkHash = extractApkHash();
} catch (FirebaseAppDistributionException e) {
return Tasks.forException(e);
}
return testerApiClient.findReleaseUsingApkHash(apkHash);
return extractApkHash()
.onSuccessTask(lightweightExecutor, testerApiClient::findReleaseUsingApkHash);
}

/**
Expand All @@ -111,15 +118,19 @@ String extractInternalAppSharingArtifactId() throws FirebaseAppDistributionExcep
*
* <p>The result is stored in an in-memory cache to avoid computing it repeatedly.
*/
String extractApkHash() throws FirebaseAppDistributionException {
PackageInfo metadataPackageInfo =
getPackageInfoWithMetadata(firebaseApp.getApplicationContext());
String installedReleaseApkHash = extractApkHash(metadataPackageInfo);
if (installedReleaseApkHash == null || installedReleaseApkHash.isEmpty()) {
throw new FirebaseAppDistributionException(
"Could not calculate hash of installed APK", Status.UNKNOWN);
}
return installedReleaseApkHash;
Task<String> extractApkHash() {
return runAsyncInTask(
backgroundExecutor,
() -> {
PackageInfo metadataPackageInfo =
getPackageInfoWithMetadata(firebaseApp.getApplicationContext());
String installedReleaseApkHash = extractApkHash(metadataPackageInfo);
if (installedReleaseApkHash == null || installedReleaseApkHash.isEmpty()) {
throw new FirebaseAppDistributionException(
"Could not calculate hash of installed APK", Status.UNKNOWN);
}
return installedReleaseApkHash;
});
}

private String extractApkHash(PackageInfo packageInfo) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import static android.os.Looper.getMainLooper;
import static com.google.common.truth.Truth.assertThat;
import static com.google.firebase.appdistribution.BinaryType.APK;
import static com.google.firebase.appdistribution.impl.TestUtils.awaitAsyncOperations;
import static com.google.firebase.appdistribution.impl.TestUtils.awaitTask;
import static com.google.firebase.appdistribution.impl.TestUtils.awaitTaskFailure;
import static org.junit.Assert.assertEquals;
Expand All @@ -38,7 +39,7 @@
import com.google.firebase.appdistribution.FirebaseAppDistributionException.Status;
import com.google.firebase.concurrent.TestOnlyExecutors;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
Expand All @@ -64,7 +65,7 @@ public class NewReleaseFetcherTest {
@Mock private FirebaseAppDistributionTesterApiClient mockFirebaseAppDistributionTesterApiClient;
@Mock private ReleaseIdentifier mockReleaseIdentifier;

@Lightweight Executor lightweightExecutor = TestOnlyExecutors.lite();
@Lightweight ExecutorService lightweightExecutor = TestOnlyExecutors.lite();

@Before
public void setup() throws FirebaseAppDistributionException {
Expand All @@ -81,7 +82,7 @@ public void setup() throws FirebaseAppDistributionException {
packageInfo.versionName = "1.0";
shadowPackageManager.installPackage(packageInfo);

when(mockReleaseIdentifier.extractApkHash()).thenReturn(CURRENT_APK_HASH);
when(mockReleaseIdentifier.extractApkHash()).thenReturn(Tasks.forResult(CURRENT_APK_HASH));
when(mockReleaseIdentifier.extractInternalAppSharingArtifactId())
.thenReturn(TEST_IAS_ARTIFACT_ID);

Expand Down Expand Up @@ -147,6 +148,7 @@ public void checkForNewRelease_whenNewReleaseIsSameRelease_returnsNull()
.thenReturn(Tasks.forResult(getTestInstalledRelease().build()));

Task<AppDistributionReleaseInternal> releaseTask = newReleaseFetcher.checkForNewRelease();
awaitAsyncOperations(lightweightExecutor);

assertThat(awaitTask(releaseTask)).isNull();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package com.google.firebase.appdistribution.impl;

import static com.google.common.truth.Truth.assertThat;
import static com.google.firebase.appdistribution.impl.TestUtils.awaitTask;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
Expand All @@ -33,7 +34,12 @@
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.annotations.concurrent.Background;
import com.google.firebase.annotations.concurrent.Lightweight;
import com.google.firebase.appdistribution.FirebaseAppDistributionException;
import com.google.firebase.concurrent.TestOnlyExecutors;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
Expand All @@ -56,6 +62,9 @@ public class ReleaseIdentifierTest {
private static final String CURRENT_APK_HASH = "currentApkHash";
private static final long INSTALLED_VERSION_CODE = 1;

@Background private final Executor backgroundExecutor = TestOnlyExecutors.background();
@Lightweight private final Executor lightweightExecutor = TestOnlyExecutors.lite();

private FirebaseApp firebaseApp;
private ShadowPackageManager shadowPackageManager;
@Mock private FirebaseAppDistributionTesterApiClient mockTesterApiClient;
Expand All @@ -79,44 +88,52 @@ public void setup() {

shadowPackageManager =
shadowOf(ApplicationProvider.getApplicationContext().getPackageManager());
releaseIdentifier = Mockito.spy(new ReleaseIdentifier(firebaseApp, mockTesterApiClient));
releaseIdentifier =
Mockito.spy(
new ReleaseIdentifier(
firebaseApp, mockTesterApiClient, backgroundExecutor, lightweightExecutor));
}

@Test
public void identifyRelease_apk_returnsReleaseName() {
public void identifyRelease_apk_returnsReleaseName()
throws FirebaseAppDistributionException, ExecutionException, InterruptedException {
installTestApk();
doReturn(CURRENT_APK_HASH).when(releaseIdentifier).calculateApkHash(any());
when(mockTesterApiClient.findReleaseUsingApkHash(CURRENT_APK_HASH))
.thenReturn(Tasks.forResult(TEST_RELEASE_NAME));

Task<String> task = releaseIdentifier.identifyRelease();

assertThat(task.isSuccessful()).isTrue();
assertThat(task.getResult()).isEqualTo(TEST_RELEASE_NAME);
assertThat(awaitTask(task)).isEqualTo(TEST_RELEASE_NAME);
}

@Test
public void identifyRelease_aab_returnsReleaseName() {
public void identifyRelease_aab_returnsReleaseName()
throws FirebaseAppDistributionException, ExecutionException, InterruptedException {
installTestAab();
when(mockTesterApiClient.findReleaseUsingIasArtifactId(TEST_IAS_ARTIFACT_ID))
.thenReturn(Tasks.forResult(TEST_RELEASE_NAME));

Task<String> task = releaseIdentifier.identifyRelease();

assertThat(task.isSuccessful()).isTrue();
assertThat(task.getResult()).isEqualTo(TEST_RELEASE_NAME);
assertThat(awaitTask(task)).isEqualTo(TEST_RELEASE_NAME);
}

@Test
public void extractApkHash_returnsAHash() throws FirebaseAppDistributionException {
assertThat(releaseIdentifier.extractApkHash().matches("^[0-9a-fA-F]+$")).isTrue();
public void extractApkHash_returnsAHash()
throws FirebaseAppDistributionException, ExecutionException, InterruptedException {
Task<String> task = releaseIdentifier.extractApkHash();

assertThat(awaitTask(task).matches("^[0-9a-fA-F]+$")).isTrue();
}

@Test
public void extractApkHash_ifKeyInCachedApkHashes_doesNotRecalculateZipHash()
throws FirebaseAppDistributionException {
releaseIdentifier.extractApkHash();
releaseIdentifier.extractApkHash();
throws FirebaseAppDistributionException, ExecutionException, InterruptedException {
Task<String> task1 = releaseIdentifier.extractApkHash();
awaitTask(task1);
Task<String> task2 = releaseIdentifier.extractApkHash();
awaitTask(task2);

// asserts that that calculateApkHash is only called once
verify(releaseIdentifier).calculateApkHash(any());
Expand Down