Skip to content

Commit 444edc1

Browse files
authored
Improve error handling in some more classes (#3278)
* Improve error handling around downloading and installing * Improve error handling around downloading and installing * Improve error handling in some more classes * Address Manny's feedback
1 parent 19130fb commit 444edc1

File tree

7 files changed

+176
-114
lines changed

7 files changed

+176
-114
lines changed

firebase-app-distribution/src/main/java/com/google/firebase/app/distribution/FirebaseAppDistributionNotificationsManager.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@
1414

1515
package com.google.firebase.app.distribution;
1616

17-
import android.app.Notification;
1817
import android.app.NotificationChannel;
1918
import android.app.NotificationManager;
2019
import android.app.PendingIntent;
2120
import android.content.Context;
2221
import android.content.Intent;
2322
import android.os.Build;
23+
import androidx.annotation.Nullable;
2424
import androidx.annotation.VisibleForTesting;
2525
import androidx.core.app.NotificationCompat;
2626
import com.google.firebase.app.distribution.internal.LogWrapper;
@@ -48,18 +48,20 @@ class FirebaseAppDistributionNotificationsManager {
4848

4949
void updateNotification(long totalBytes, long downloadedBytes, UpdateStatus status) {
5050
NotificationManager notificationManager = createNotificationManager(context);
51-
Notification notification =
51+
NotificationCompat.Builder notificationBuilder =
5252
new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
5353
.setOnlyAlertOnce(true)
5454
.setSmallIcon(appIconSource.getNonAdaptiveIconOrDefault(context))
55-
.setContentIntent(createPendingIntent())
5655
.setContentTitle(context.getString(getNotificationContentTitleId(status)))
5756
.setProgress(
5857
100,
5958
(int) (((float) downloadedBytes / (float) totalBytes) * 100),
60-
/*indeterminate = */ false)
61-
.build();
62-
notificationManager.notify(NOTIFICATION_TAG, /*id =*/ 0, notification);
59+
/*indeterminate = */ false);
60+
PendingIntent appLaunchIntent = createAppLaunchIntent();
61+
if (appLaunchIntent != null) {
62+
notificationBuilder.setContentIntent(appLaunchIntent);
63+
}
64+
notificationManager.notify(NOTIFICATION_TAG, /*id =*/ 0, notificationBuilder.build());
6365
}
6466

6567
int getNotificationContentTitleId(UpdateStatus status) {
@@ -100,11 +102,13 @@ private NotificationManager createNotificationManager(Context context) {
100102
}
101103
}
102104

103-
private PendingIntent createPendingIntent() {
105+
@Nullable
106+
private PendingIntent createAppLaunchIntent() {
104107
// Query the package manager for the best launch intent for the app
105108
Intent intent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
106109
if (intent == null) {
107110
LogWrapper.getInstance().w(TAG + "No activity found to launch app");
111+
return null;
108112
}
109113
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT);
110114
}

firebase-app-distribution/src/main/java/com/google/firebase/app/distribution/FirebaseAppDistributionRegistrar.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package com.google.firebase.app.distribution;
1616

1717
import android.app.Application;
18+
import android.content.Context;
1819
import androidx.annotation.Keep;
1920
import androidx.annotation.NonNull;
2021
import com.google.firebase.FirebaseApp;
@@ -57,14 +58,18 @@ private FirebaseAppDistribution buildFirebaseAppDistribution(ComponentContainer
5758
FirebaseAppDistributionLifecycleNotifier lifecycleNotifier =
5859
FirebaseAppDistributionLifecycleNotifier.getInstance();
5960

60-
if (firebaseApp.getApplicationContext() instanceof Application) {
61-
Application firebaseApplication = (Application) firebaseApp.getApplicationContext();
61+
Context context = firebaseApp.getApplicationContext();
62+
if (context instanceof Application) {
63+
Application firebaseApplication = (Application) context;
6264
firebaseApplication.registerActivityLifecycleCallbacks(lifecycleNotifier);
6365
} else {
6466
LogWrapper.getInstance()
6567
.e(
6668
TAG
67-
+ "Error registering app to ActivityLifecycleCallbacks. SDK might not function correctly.");
69+
+ "Context "
70+
+ context
71+
+ " was not an Application, can't register for lifecycle callbacks. SDK might not"
72+
+ " function correctly.");
6873
}
6974

7075
return appDistribution;

firebase-app-distribution/src/main/java/com/google/firebase/app/distribution/FirebaseAppDistributionTesterApiClient.java

Lines changed: 71 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import android.content.pm.PackageManager;
1919
import androidx.annotation.NonNull;
2020
import androidx.annotation.Nullable;
21+
import androidx.annotation.VisibleForTesting;
2122
import com.google.android.gms.common.util.AndroidUtilsLight;
2223
import com.google.android.gms.common.util.Hex;
2324
import com.google.firebase.app.distribution.Constants.ErrorMessages;
@@ -27,9 +28,6 @@
2728
import java.io.ByteArrayOutputStream;
2829
import java.io.IOException;
2930
import java.io.InputStream;
30-
import java.net.MalformedURLException;
31-
import java.net.ProtocolException;
32-
import java.net.URL;
3331
import javax.net.ssl.HttpsURLConnection;
3432
import org.json.JSONArray;
3533
import org.json.JSONException;
@@ -58,31 +56,69 @@ class FirebaseAppDistributionTesterApiClient {
5856

5957
public static final int DEFAULT_BUFFER_SIZE = 8192;
6058

59+
private final HttpsUrlConnectionFactory httpsUrlConnectionFactory;
60+
61+
FirebaseAppDistributionTesterApiClient() {
62+
this(new HttpsUrlConnectionFactory());
63+
}
64+
65+
@VisibleForTesting
66+
FirebaseAppDistributionTesterApiClient(
67+
@NonNull HttpsUrlConnectionFactory httpsUrlConnectionFactory) {
68+
this.httpsUrlConnectionFactory = httpsUrlConnectionFactory;
69+
}
70+
6171
/**
6272
* Fetches and returns the lastest release for the app that the tester has access to, or null if
6373
* the tester doesn't have access to any releases.
6474
*/
6575
@Nullable
66-
public AppDistributionReleaseInternal fetchNewRelease(
76+
AppDistributionReleaseInternal fetchNewRelease(
6777
@NonNull String fid,
6878
@NonNull String appId,
6979
@NonNull String apiKey,
7080
@NonNull String authToken,
7181
@NonNull Context context)
7282
throws FirebaseAppDistributionException {
73-
HttpsURLConnection connection = openHttpsUrlConnection(appId, fid, apiKey, authToken, context);
83+
HttpsURLConnection connection = null;
84+
int responseCode;
7485
String responseBody;
75-
try (BufferedInputStream inputStream = new BufferedInputStream(connection.getInputStream())) {
76-
responseBody = convertInputStreamToString(inputStream);
86+
try {
87+
connection = openHttpsUrlConnection(appId, fid, apiKey, authToken, context);
88+
responseCode = connection.getResponseCode();
89+
responseBody = readResponseBody(connection);
7790
} catch (IOException e) {
78-
throw getExceptionForHttpResponse(connection, e);
91+
throw new FirebaseAppDistributionException(
92+
ErrorMessages.NETWORK_ERROR, Status.NETWORK_FAILURE, e);
7993
} finally {
80-
connection.disconnect();
94+
if (connection != null) {
95+
connection.disconnect();
96+
}
8197
}
98+
99+
if (!isResponseSuccess(responseCode)) {
100+
throw getExceptionForHttpResponse(responseCode);
101+
}
102+
82103
return parseNewRelease(responseBody);
83104
}
84105

85-
AppDistributionReleaseInternal parseNewRelease(String responseBody)
106+
private String readResponseBody(HttpsURLConnection connection) throws IOException {
107+
boolean isSuccess = isResponseSuccess(connection.getResponseCode());
108+
InputStream inputStream = isSuccess ? connection.getInputStream() : connection.getErrorStream();
109+
if (inputStream == null && !isSuccess) {
110+
// If the server returns a response with an error code and no response body, getErrorStream
111+
// returns null. We return an empty string to reflect the empty body.
112+
return "";
113+
}
114+
return convertInputStreamToString(new BufferedInputStream(inputStream));
115+
}
116+
117+
private static boolean isResponseSuccess(int responseCode) {
118+
return responseCode >= 200 && responseCode < 300;
119+
}
120+
121+
private AppDistributionReleaseInternal parseNewRelease(String responseBody)
86122
throws FirebaseAppDistributionException {
87123
try {
88124
JSONObject responseJson = new JSONObject(responseBody);
@@ -128,40 +164,27 @@ AppDistributionReleaseInternal parseNewRelease(String responseBody)
128164
}
129165
}
130166

131-
private FirebaseAppDistributionException getExceptionForHttpResponse(
132-
HttpsURLConnection connection, Exception cause) {
133-
// TODO(lkellogg): this try-catch should be unnecessary because it will only throw an
134-
// IOException here if we couldn't connect to the server, in which case getInputStream() would
135-
// have already failed with the same exception. We also weirdly have to choose one of the two
136-
// thrown exceptions to set as the cause. We can avoid this by checking the response code
137-
// first, and then catching any unexpected exceptions when reading the input stream, essentially
138-
// combining the "default" case below with this try-catch.
139-
int responseCode;
140-
try {
141-
responseCode = connection.getResponseCode();
142-
} catch (IOException e) {
143-
return new FirebaseAppDistributionException(
144-
ErrorMessages.NETWORK_ERROR, Status.NETWORK_FAILURE, e);
145-
}
146-
LogWrapper.getInstance().e(TAG + "Failed due to " + responseCode);
167+
private FirebaseAppDistributionException getExceptionForHttpResponse(int responseCode) {
147168
switch (responseCode) {
169+
case 400:
170+
return new FirebaseAppDistributionException(
171+
"Bad request when fetching new release", Status.UNKNOWN);
148172
case 401:
149173
return new FirebaseAppDistributionException(
150-
ErrorMessages.AUTHENTICATION_ERROR, Status.AUTHENTICATION_FAILURE, cause);
174+
ErrorMessages.AUTHENTICATION_ERROR, Status.AUTHENTICATION_FAILURE);
151175
case 403:
152-
case 400:
153176
return new FirebaseAppDistributionException(
154-
ErrorMessages.AUTHORIZATION_ERROR, Status.AUTHENTICATION_FAILURE, cause);
177+
ErrorMessages.AUTHORIZATION_ERROR, Status.AUTHENTICATION_FAILURE);
155178
case 404:
156179
return new FirebaseAppDistributionException(
157-
ErrorMessages.NOT_FOUND_ERROR, Status.AUTHENTICATION_FAILURE, cause);
180+
"App or tester not found when fetching new release", Status.AUTHENTICATION_FAILURE);
158181
case 408:
159182
case 504:
160183
return new FirebaseAppDistributionException(
161-
ErrorMessages.TIMEOUT_ERROR, Status.NETWORK_FAILURE, cause);
184+
ErrorMessages.TIMEOUT_ERROR, Status.NETWORK_FAILURE);
162185
default:
163186
return new FirebaseAppDistributionException(
164-
ErrorMessages.UNKNOWN_ERROR, Status.UNKNOWN, cause);
187+
"Received error status when fetching new release: " + responseCode, Status.UNKNOWN);
165188
}
166189
}
167190

@@ -173,22 +196,13 @@ private String tryGetValue(JSONObject jsonObject, String key) {
173196
}
174197
}
175198

176-
HttpsURLConnection openHttpsUrlConnection(
199+
private HttpsURLConnection openHttpsUrlConnection(
177200
String appId, String fid, String apiKey, String authToken, Context context)
178-
throws FirebaseAppDistributionException {
201+
throws IOException {
179202
HttpsURLConnection httpsURLConnection;
180-
URL url = getReleasesEndpointUrl(appId, fid);
181-
try {
182-
httpsURLConnection = (HttpsURLConnection) url.openConnection();
183-
} catch (IOException e) {
184-
throw new FirebaseAppDistributionException(
185-
ErrorMessages.NETWORK_ERROR, Status.NETWORK_FAILURE, e);
186-
}
187-
try {
188-
httpsURLConnection.setRequestMethod(REQUEST_METHOD_GET);
189-
} catch (ProtocolException e) {
190-
throw new FirebaseAppDistributionException(ErrorMessages.UNKNOWN_ERROR, Status.UNKNOWN, e);
191-
}
203+
String url = String.format(RELEASE_ENDPOINT_URL_FORMAT, appId, fid);
204+
httpsURLConnection = httpsUrlConnectionFactory.openConnection(url);
205+
httpsURLConnection.setRequestMethod(REQUEST_METHOD_GET);
192206
httpsURLConnection.setRequestProperty(API_KEY_HEADER, apiKey);
193207
httpsURLConnection.setRequestProperty(INSTALLATION_AUTH_HEADER, authToken);
194208
httpsURLConnection.addRequestProperty(X_ANDROID_PACKAGE_HEADER_KEY, context.getPackageName());
@@ -197,16 +211,6 @@ HttpsURLConnection openHttpsUrlConnection(
197211
return httpsURLConnection;
198212
}
199213

200-
private URL getReleasesEndpointUrl(String appId, String fid)
201-
throws FirebaseAppDistributionException {
202-
try {
203-
return new URL(String.format(RELEASE_ENDPOINT_URL_FORMAT, appId, fid));
204-
} catch (MalformedURLException e) {
205-
throw new FirebaseAppDistributionException(
206-
ErrorMessages.UNKNOWN_ERROR, FirebaseAppDistributionException.Status.UNKNOWN, e);
207-
}
208-
}
209-
210214
private static String convertInputStreamToString(InputStream is) throws IOException {
211215
ByteArrayOutputStream result = new ByteArrayOutputStream();
212216
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
@@ -225,12 +229,22 @@ private String getFingerprintHashForPackage(Context context) {
225229
hash = AndroidUtilsLight.getPackageCertificateHashBytes(context, context.getPackageName());
226230

227231
if (hash == null) {
232+
LogWrapper.getInstance()
233+
.e(
234+
TAG
235+
+ "Could not get fingerprint hash for X-Android-Cert header. Package is not signed: "
236+
+ context.getPackageName());
228237
return null;
229238
} else {
230239
return Hex.bytesToStringUppercase(hash, /* zeroTerminated= */ false);
231240
}
232241
} catch (PackageManager.NameNotFoundException e) {
233-
LogWrapper.getInstance().e(TAG + "No such package: " + context.getPackageName(), e);
242+
LogWrapper.getInstance()
243+
.e(
244+
TAG
245+
+ "Could not get fingerprint hash for X-Android-Cert header. No such package: "
246+
+ context.getPackageName(),
247+
e);
234248
return null;
235249
}
236250
}

firebase-app-distribution/src/main/java/com/google/firebase/app/distribution/NewReleaseFetcher.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ boolean isSameAsInstalledRelease(AppDistributionReleaseInternal newRelease)
179179
return hasSameHashAsInstalledRelease(newRelease);
180180
}
181181

182+
// TODO(lkellogg): getIasArtifactId() will likely never be null since it's set to the empty
183+
// string if not present in the response
182184
if (newRelease.getIasArtifactId() == null) {
183185
return false;
184186
}

firebase-app-distribution/src/test/java/com/google/firebase/app/distribution/AabUpdaterTest.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public class AabUpdaterTest {
6666
static class TestActivity extends Activity {}
6767

6868
@Before
69-
public void setup() throws IOException {
69+
public void setup() throws IOException, FirebaseAppDistributionException {
7070
MockitoAnnotations.initMocks(this);
7171

7272
FirebaseApp.clearInstancesForTest();
@@ -113,7 +113,7 @@ public void updateAppTask_isNotRedirectResponse_setsDownloadFailure()
113113

114114
@Test
115115
public void updateAppTask_missingLocationHeader_setsDownloadFailure()
116-
throws IOException, InterruptedException {
116+
throws InterruptedException {
117117
when(mockHttpsUrlConnection.getHeaderField("Location")).thenReturn(null);
118118

119119
UpdateTask updateTask = aabUpdater.updateAab(TEST_RELEASE_NEWER_AAB_INTERNAL);
@@ -123,8 +123,7 @@ public void updateAppTask_missingLocationHeader_setsDownloadFailure()
123123
}
124124

125125
@Test
126-
public void updateAppTask_emptyLocationHeader_setsDownloadFailure()
127-
throws IOException, InterruptedException {
126+
public void updateAppTask_emptyLocationHeader_setsDownloadFailure() throws InterruptedException {
128127
when(mockHttpsUrlConnection.getHeaderField("Location")).thenReturn("");
129128

130129
UpdateTask updateTask = aabUpdater.updateAab(TEST_RELEASE_NEWER_AAB_INTERNAL);
@@ -154,7 +153,7 @@ public void updateAppTask_whenAabReleaseAvailable_redirectsToPlay() throws Inter
154153
}
155154

156155
@Test
157-
public void updateAppTask_onAppResume_setsUpdateCancelled() throws IOException {
156+
public void updateAppTask_onAppResume_setsUpdateCancelled() {
158157
TestOnCompleteListener<Void> onCompleteListener = new TestOnCompleteListener<>();
159158
UpdateTask updateTask = aabUpdater.updateAab(TEST_RELEASE_NEWER_AAB_INTERNAL);
160159
updateTask.addOnCompleteListener(testExecutor, onCompleteListener);

firebase-app-distribution/src/test/java/com/google/firebase/app/distribution/ApkUpdaterTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,10 @@ public class ApkUpdaterTest {
7373
@Mock private ApkInstaller mockApkInstaller;
7474
@Mock private FirebaseAppDistributionNotificationsManager mockNotificationsManager;
7575

76-
private Executor testExecutor = Executors.newSingleThreadExecutor();
76+
private final Executor testExecutor = Executors.newSingleThreadExecutor();
7777

7878
@Before
79-
public void setup() throws IOException {
79+
public void setup() throws IOException, FirebaseAppDistributionException {
8080
MockitoAnnotations.initMocks(this);
8181

8282
FirebaseApp.clearInstancesForTest();

0 commit comments

Comments
 (0)