Skip to content

Commit 9220e61

Browse files
authored
Refactor out cached ApkHashExtractor (#3767)
* Add ApkHashExtractor * Add copyrights * Address Kai's feedback * Fix lint
1 parent fd109a8 commit 9220e61

File tree

8 files changed

+259
-184
lines changed

8 files changed

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

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/LogWrapper.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,42 @@ void d(@NonNull String msg) {
3636
Log.d(LOG_TAG, msg);
3737
}
3838

39+
void d(@NonNull String additionalTag, @NonNull String msg) {
40+
Log.d(LOG_TAG, prependTag(additionalTag, msg));
41+
}
42+
3943
void v(@NonNull String msg) {
4044
Log.v(LOG_TAG, msg);
4145
}
4246

47+
void v(@NonNull String additionalTag, @NonNull String msg) {
48+
Log.v(LOG_TAG, prependTag(additionalTag, msg));
49+
}
50+
4351
void i(@NonNull String msg) {
4452
Log.i(LOG_TAG, msg);
4553
}
4654

55+
void i(@NonNull String additionalTag, @NonNull String msg) {
56+
Log.i(LOG_TAG, prependTag(additionalTag, msg));
57+
}
58+
4759
void w(@NonNull String msg) {
4860
Log.w(LOG_TAG, msg);
4961
}
5062

63+
void w(@NonNull String additionalTag, @NonNull String msg) {
64+
Log.w(LOG_TAG, prependTag(additionalTag, msg));
65+
}
66+
5167
void w(@NonNull String msg, @NonNull Throwable tr) {
5268
Log.w(LOG_TAG, msg, tr);
5369
}
5470

71+
void w(@NonNull String additionalTag, @NonNull String msg, @NonNull Throwable tr) {
72+
Log.w(LOG_TAG, prependTag(additionalTag, msg), tr);
73+
}
74+
5575
void e(@NonNull String msg) {
5676
Log.e(LOG_TAG, msg);
5777
}
@@ -60,5 +80,13 @@ void e(@NonNull String msg, @NonNull Throwable tr) {
6080
Log.e(LOG_TAG, msg, tr);
6181
}
6282

83+
void e(@NonNull String additionalTag, @NonNull String msg, @NonNull Throwable tr) {
84+
Log.e(LOG_TAG, prependTag(additionalTag, msg), tr);
85+
}
86+
87+
private String prependTag(String tag, String msg) {
88+
return String.format("%s: %s", tag, msg);
89+
}
90+
6391
private LogWrapper() {}
6492
}

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)