Skip to content

Commit 3bda15c

Browse files
committed
Add ApkHashExtractor
1 parent fd109a8 commit 3bda15c

File tree

7 files changed

+210
-184
lines changed

7 files changed

+210
-184
lines changed
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package com.google.firebase.appdistribution.impl;
2+
3+
import static com.google.firebase.appdistribution.impl.ReleaseIdentificationUtils.getPackageInfoWithMetadata;
4+
5+
import android.content.Context;
6+
import android.content.pm.PackageInfo;
7+
import androidx.annotation.NonNull;
8+
import androidx.annotation.Nullable;
9+
import androidx.annotation.VisibleForTesting;
10+
import com.google.firebase.appdistribution.FirebaseAppDistributionException;
11+
import com.google.firebase.appdistribution.FirebaseAppDistributionException.Status;
12+
import java.io.File;
13+
import java.io.IOException;
14+
import java.nio.ByteBuffer;
15+
import java.security.MessageDigest;
16+
import java.security.NoSuchAlgorithmException;
17+
import java.util.ArrayList;
18+
import java.util.Enumeration;
19+
import java.util.Locale;
20+
import java.util.concurrent.ConcurrentHashMap;
21+
import java.util.concurrent.ConcurrentMap;
22+
import java.util.zip.ZipEntry;
23+
import java.util.zip.ZipFile;
24+
25+
/** Extracts a hash of the installed APK. */
26+
class ApkHashExtractor {
27+
28+
private static final String TAG = "ApkHashExtractor:";
29+
private static final int BYTES_IN_LONG = 8;
30+
31+
private final ConcurrentMap<String, String> cachedApkHashes = new ConcurrentHashMap<>();
32+
private Context applicationContext;
33+
34+
ApkHashExtractor(Context applicationContext) {
35+
this.applicationContext = applicationContext;
36+
}
37+
38+
/**
39+
* Extract the SHA-256 hash of the installed APK.
40+
*
41+
* <p>The result is stored in an in-memory cache to avoid computing it repeatedly.
42+
*/
43+
String extractApkHash() throws FirebaseAppDistributionException {
44+
PackageInfo metadataPackageInfo = getPackageInfoWithMetadata(applicationContext);
45+
String installedReleaseApkHash = extractApkHash(metadataPackageInfo);
46+
if (installedReleaseApkHash == null || installedReleaseApkHash.isEmpty()) {
47+
throw new FirebaseAppDistributionException(
48+
"Could not calculate hash of installed APK", Status.UNKNOWN);
49+
}
50+
return installedReleaseApkHash;
51+
}
52+
53+
@VisibleForTesting
54+
String extractApkHash(PackageInfo packageInfo) {
55+
File sourceFile = new File(packageInfo.applicationInfo.sourceDir);
56+
57+
String key =
58+
String.format(
59+
Locale.ENGLISH, "%s.%d", sourceFile.getAbsolutePath(), sourceFile.lastModified());
60+
if (!cachedApkHashes.containsKey(key)) {
61+
cachedApkHashes.put(key, calculateApkHash(sourceFile));
62+
}
63+
return cachedApkHashes.get(key);
64+
}
65+
66+
@Nullable
67+
String calculateApkHash(@NonNull File file) {
68+
LogWrapper.getInstance().v(TAG + "Calculating release id for " + file.getPath());
69+
LogWrapper.getInstance().v(TAG + "File size: " + file.length());
70+
71+
long start = System.currentTimeMillis();
72+
long entries = 0;
73+
String zipFingerprint = null;
74+
try {
75+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
76+
ArrayList<Byte> checksums = new ArrayList<>();
77+
78+
// Since calculating the codeHash returned from the release backend is computationally
79+
// expensive, we has the existing checksum data from the ZipFile and compare it to
80+
// (1) the apk hash returned by the backend, or (2) look up a mapping from the apk zip hash to
81+
// the
82+
// full codehash, and compare that to the codehash to the backend
83+
ZipFile zis = new ZipFile(file);
84+
try {
85+
Enumeration<? extends ZipEntry> zipEntries = zis.entries();
86+
while (zipEntries.hasMoreElements()) {
87+
ZipEntry zip = zipEntries.nextElement();
88+
entries += 1;
89+
byte[] crcBytes = longToByteArray(zip.getCrc());
90+
for (byte b : crcBytes) {
91+
checksums.add(b);
92+
}
93+
}
94+
} finally {
95+
zis.close();
96+
}
97+
byte[] checksumByteArray = digest.digest(arrayListToByteArray(checksums));
98+
StringBuilder sb = new StringBuilder();
99+
for (byte b : checksumByteArray) {
100+
sb.append(String.format("%02x", b));
101+
}
102+
zipFingerprint = sb.toString();
103+
104+
} catch (IOException | NoSuchAlgorithmException e) {
105+
LogWrapper.getInstance().v(TAG + "id calculation failed for " + file.getPath());
106+
return null;
107+
} finally {
108+
long elapsed = System.currentTimeMillis() - start;
109+
if (elapsed > 2 * 1000) {
110+
LogWrapper.getInstance()
111+
.v(
112+
TAG
113+
+ String.format(
114+
"Long id calculation time %d ms and %d entries for %s",
115+
elapsed, entries, file.getPath()));
116+
}
117+
118+
LogWrapper.getInstance()
119+
.v(TAG + String.format("Finished calculating %d entries in %d ms", entries, elapsed));
120+
LogWrapper.getInstance()
121+
.v(TAG + String.format("%s hashes to %s", file.getPath(), zipFingerprint));
122+
}
123+
124+
return zipFingerprint;
125+
}
126+
127+
private static byte[] longToByteArray(long x) {
128+
ByteBuffer buffer = ByteBuffer.allocate(BYTES_IN_LONG);
129+
buffer.putLong(x);
130+
return buffer.array();
131+
}
132+
133+
private static byte[] arrayListToByteArray(ArrayList<Byte> list) {
134+
byte[] result = new byte[list.size()];
135+
for (int i = 0; i < list.size(); i++) {
136+
result[i] = list.get(i);
137+
}
138+
return result;
139+
}
140+
}

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

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@
4040
import com.google.firebase.appdistribution.UpdateProgress;
4141
import com.google.firebase.appdistribution.UpdateStatus;
4242
import com.google.firebase.appdistribution.UpdateTask;
43-
import com.google.firebase.inject.Provider;
44-
import com.google.firebase.installations.FirebaseInstallationsApi;
4543

4644
/**
4745
* This class is the "real" implementation of the Firebase App Distribution API which should only be
@@ -101,34 +99,6 @@ class FirebaseAppDistributionImpl implements FirebaseAppDistribution {
10199
lifecycleNotifier.addOnActivityResumedListener(this::onActivityResumed);
102100
}
103101

104-
FirebaseAppDistributionImpl(
105-
@NonNull FirebaseApp firebaseApp,
106-
@NonNull Provider<FirebaseInstallationsApi> firebaseInstallationsApiProvider,
107-
@NonNull SignInStorage signInStorage,
108-
@NonNull FirebaseAppDistributionLifecycleNotifier lifecycleNotifier) {
109-
this(
110-
firebaseApp,
111-
new TesterSignInManager(firebaseApp, firebaseInstallationsApiProvider, signInStorage),
112-
new NewReleaseFetcher(
113-
firebaseApp,
114-
new FirebaseAppDistributionTesterApiClient(),
115-
firebaseInstallationsApiProvider),
116-
new ApkUpdater(firebaseApp, new ApkInstaller()),
117-
new AabUpdater(),
118-
signInStorage,
119-
lifecycleNotifier);
120-
}
121-
122-
FirebaseAppDistributionImpl(
123-
@NonNull FirebaseApp firebaseApp,
124-
@NonNull Provider<FirebaseInstallationsApi> firebaseInstallationsApiProvider) {
125-
this(
126-
firebaseApp,
127-
firebaseInstallationsApiProvider,
128-
new SignInStorage(firebaseApp.getApplicationContext()),
129-
FirebaseAppDistributionLifecycleNotifier.getInstance());
130-
}
131-
132102
@Override
133103
@NonNull
134104
public UpdateTask updateIfNewReleaseAvailable() {

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.google.firebase.components.ComponentContainer;
2525
import com.google.firebase.components.ComponentRegistrar;
2626
import com.google.firebase.components.Dependency;
27+
import com.google.firebase.inject.Provider;
2728
import com.google.firebase.installations.FirebaseInstallationsApi;
2829
import com.google.firebase.platforminfo.LibraryVersionComponent;
2930
import java.util.Arrays;
@@ -55,13 +56,26 @@ public class FirebaseAppDistributionRegistrar implements ComponentRegistrar {
5556

5657
private FirebaseAppDistribution buildFirebaseAppDistribution(ComponentContainer container) {
5758
FirebaseApp firebaseApp = container.get(FirebaseApp.class);
58-
FirebaseAppDistribution appDistribution =
59-
new FirebaseAppDistributionImpl(
60-
firebaseApp, container.getProvider(FirebaseInstallationsApi.class));
59+
Context context = firebaseApp.getApplicationContext();
60+
Provider<FirebaseInstallationsApi> firebaseInstallationsApiProvider =
61+
container.getProvider(FirebaseInstallationsApi.class);
62+
SignInStorage signInStorage = new SignInStorage(context);
63+
FirebaseAppDistributionTesterApiClient testerApiClient =
64+
new FirebaseAppDistributionTesterApiClient();
6165
FirebaseAppDistributionLifecycleNotifier lifecycleNotifier =
6266
FirebaseAppDistributionLifecycleNotifier.getInstance();
67+
ApkHashExtractor apkHashExtractor = new ApkHashExtractor(firebaseApp.getApplicationContext());
68+
FirebaseAppDistribution appDistribution =
69+
new FirebaseAppDistributionImpl(
70+
firebaseApp,
71+
new TesterSignInManager(firebaseApp, firebaseInstallationsApiProvider, signInStorage),
72+
new NewReleaseFetcher(
73+
firebaseApp, testerApiClient, firebaseInstallationsApiProvider, apkHashExtractor),
74+
new ApkUpdater(firebaseApp, new ApkInstaller()),
75+
new AabUpdater(),
76+
signInStorage,
77+
lifecycleNotifier);
6378

64-
Context context = firebaseApp.getApplicationContext();
6579
if (context instanceof Application) {
6680
Application firebaseApplication = (Application) context;
6781
firebaseApplication.registerActivityLifecycleCallbacks(lifecycleNotifier);

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

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,10 @@
1414

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

17-
import static com.google.firebase.appdistribution.impl.ReleaseIdentificationUtils.calculateApkHash;
1817
import static com.google.firebase.appdistribution.impl.ReleaseIdentificationUtils.getPackageInfo;
19-
import static com.google.firebase.appdistribution.impl.ReleaseIdentificationUtils.getPackageInfoWithMetadata;
2018
import static com.google.firebase.appdistribution.impl.TaskUtils.runAsyncInTask;
2119

2220
import android.content.Context;
23-
import android.content.pm.PackageInfo;
2421
import androidx.annotation.NonNull;
2522
import androidx.annotation.Nullable;
2623
import androidx.annotation.VisibleForTesting;
@@ -34,8 +31,6 @@
3431
import com.google.firebase.inject.Provider;
3532
import com.google.firebase.installations.FirebaseInstallationsApi;
3633
import com.google.firebase.installations.InstallationTokenResult;
37-
import java.io.File;
38-
import java.util.Locale;
3934
import java.util.concurrent.ConcurrentHashMap;
4035
import java.util.concurrent.ConcurrentMap;
4136
import java.util.concurrent.Executor;
@@ -51,6 +46,7 @@ class NewReleaseFetcher {
5146
private final FirebaseApp firebaseApp;
5247
private final FirebaseAppDistributionTesterApiClient firebaseAppDistributionTesterApiClient;
5348
private final Provider<FirebaseInstallationsApi> firebaseInstallationsApiProvider;
49+
private final ApkHashExtractor apkHashExtractor;
5450
private final Context context;
5551
// Maintain an in-memory mapping from source file to APK hash to avoid re-calculating the hash
5652
private static final ConcurrentMap<String, String> cachedApkHashes = new ConcurrentHashMap<>();
@@ -61,22 +57,26 @@ class NewReleaseFetcher {
6157
NewReleaseFetcher(
6258
@NonNull FirebaseApp firebaseApp,
6359
@NonNull FirebaseAppDistributionTesterApiClient firebaseAppDistributionTesterApiClient,
64-
@NonNull Provider<FirebaseInstallationsApi> firebaseInstallationsApiProvider) {
60+
@NonNull Provider<FirebaseInstallationsApi> firebaseInstallationsApiProvider,
61+
ApkHashExtractor apkHashExtractor) {
6562
this(
6663
firebaseApp,
6764
firebaseAppDistributionTesterApiClient,
6865
firebaseInstallationsApiProvider,
66+
apkHashExtractor,
6967
Executors.newSingleThreadExecutor());
7068
}
7169

7270
NewReleaseFetcher(
7371
@NonNull FirebaseApp firebaseApp,
7472
@NonNull FirebaseAppDistributionTesterApiClient firebaseAppDistributionTesterApiClient,
7573
@NonNull Provider<FirebaseInstallationsApi> firebaseInstallationsApiProvider,
74+
ApkHashExtractor apkHashExtractor,
7675
@NonNull Executor executor) {
7776
this.firebaseApp = firebaseApp;
7877
this.firebaseAppDistributionTesterApiClient = firebaseAppDistributionTesterApiClient;
7978
this.firebaseInstallationsApiProvider = firebaseInstallationsApiProvider;
79+
this.apkHashExtractor = apkHashExtractor;
8080
this.taskExecutor = executor;
8181
this.context = firebaseApp.getApplicationContext();
8282
}
@@ -206,29 +206,10 @@ private String getInstalledAppVersionName(Context context)
206206
return getPackageInfo(context).versionName;
207207
}
208208

209-
@VisibleForTesting
210-
String extractApkHash(PackageInfo packageInfo) {
211-
File sourceFile = new File(packageInfo.applicationInfo.sourceDir);
212-
213-
String key =
214-
String.format(
215-
Locale.ENGLISH, "%s.%d", sourceFile.getAbsolutePath(), sourceFile.lastModified());
216-
if (!cachedApkHashes.containsKey(key)) {
217-
cachedApkHashes.put(key, calculateApkHash(sourceFile));
218-
}
219-
return cachedApkHashes.get(key);
220-
}
221-
222209
private boolean hasSameHashAsInstalledRelease(AppDistributionReleaseInternal newRelease)
223210
throws FirebaseAppDistributionException {
224-
Context context = firebaseApp.getApplicationContext();
225-
PackageInfo metadataPackageInfo = getPackageInfoWithMetadata(context);
226-
String installedReleaseApkHash = extractApkHash(metadataPackageInfo);
227-
228-
if (installedReleaseApkHash == null || installedReleaseApkHash.isEmpty()) {
229-
throw new FirebaseAppDistributionException(
230-
"Could not calculate hash of installed APK", Status.UNKNOWN);
231-
} else if (newRelease.getApkHash().isEmpty()) {
211+
String installedReleaseApkHash = apkHashExtractor.extractApkHash();
212+
if (newRelease.getApkHash().isEmpty()) {
232213
throw new FirebaseAppDistributionException(
233214
"Missing APK hash from new release", Status.UNKNOWN);
234215
}

0 commit comments

Comments
 (0)