Skip to content

Commit 14935e9

Browse files
andrewbibiloniRachel Prince
andauthored
FAD Update Notifications Manager to indicate download progress (#2903)
* Add terminal states in UpdateStatus, update APK tests * Remove Installed terminal state, remove use of class variables for totalbytes and bytesdownloaded * WIP * Only show progress for basic config * Fix icon tests for notificationmanager * google java format * Add comment to tests * Add copyright to manager test file * Rename basicConfig bool, use overload instead of flag, remove unused activity input in updateAppClient * change adaptive icon version check to use >= * make return in updateapp inline Co-authored-by: Rachel Prince <[email protected]>
1 parent 6c75d59 commit 14935e9

File tree

17 files changed

+441
-19
lines changed

17 files changed

+441
-19
lines changed

firebase-app-distribution/src/main/java/com/google/firebase/appdistribution/FirebaseAppDistribution.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,6 @@ public synchronized Task<Void> updateToLatestRelease() {
126126
if (release == null) {
127127
return Tasks.forResult(null);
128128
}
129-
130129
return showUpdateAlertDialog(release);
131130
});
132131

@@ -173,7 +172,14 @@ public synchronized Task<AppDistributionRelease> checkForUpdate() {
173172
*/
174173
@NonNull
175174
public synchronized UpdateTask updateApp() {
175+
return updateApp(false);
176+
}
176177

178+
/**
179+
* Overloaded updateApp with boolean input showDownloadInNotificationsManager. Set to true for
180+
* basic configuration and false for advanced configuration.
181+
*/
182+
private synchronized UpdateTask updateApp(boolean showDownloadInNotificationManager) {
177183
if (!isTesterSignedIn()) {
178184
UpdateTaskImpl updateTask = new UpdateTaskImpl();
179185
updateTask.setException(
@@ -182,7 +188,7 @@ public synchronized UpdateTask updateApp() {
182188
return updateTask;
183189
}
184190

185-
return this.updateAppClient.updateApp(cachedLatestRelease, currentActivity);
191+
return this.updateAppClient.updateApp(cachedLatestRelease, showDownloadInNotificationManager);
186192
}
187193

188194
/** Returns true if the App Distribution tester is signed in */
@@ -306,7 +312,8 @@ private Task<Void> showUpdateAlertDialog(AppDistributionRelease latestRelease) {
306312
AlertDialog.BUTTON_POSITIVE,
307313
context.getString(R.string.update_yes_button),
308314
(dialogInterface, i) ->
309-
updateApp()
315+
// show download progress in notification manager
316+
updateApp(true)
310317
.addOnSuccessListener(unused -> updateAlertDialogTask.setResult(null))
311318
.addOnFailureListener(updateAlertDialogTask::setException));
312319

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Copyright 2021 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;
16+
17+
import android.app.NotificationChannel;
18+
import android.app.NotificationManager;
19+
import android.app.PendingIntent;
20+
import android.content.Context;
21+
import android.content.Intent;
22+
import android.content.res.Resources;
23+
import android.graphics.drawable.AdaptiveIconDrawable;
24+
import android.graphics.drawable.Drawable;
25+
import android.os.Build;
26+
import android.os.Build.VERSION;
27+
import android.util.Log;
28+
import androidx.annotation.VisibleForTesting;
29+
import androidx.core.app.NotificationCompat;
30+
import androidx.core.content.ContextCompat;
31+
import com.google.firebase.FirebaseApp;
32+
33+
class FirebaseAppDistributionNotificationsManager {
34+
private static final String TAG = "FADNotificationsManager";
35+
private static final String NOTIFICATION_CHANNEL_ID =
36+
"com.google.firebase.app.distribution.notification_channel_id";
37+
38+
@VisibleForTesting
39+
static final String NOTIFICATION_TAG = "com.google.firebase.app.distribution.tag";
40+
41+
private final FirebaseApp firebaseApp;
42+
43+
FirebaseAppDistributionNotificationsManager(FirebaseApp firebaseApp) {
44+
this.firebaseApp = firebaseApp;
45+
}
46+
47+
void updateNotification(long totalBytes, long downloadedBytes, UpdateStatus status) {
48+
Context context = firebaseApp.getApplicationContext();
49+
NotificationManager notificationManager = createNotificationManager(context);
50+
NotificationCompat.Builder builder = createNotificationBuilder();
51+
if (isErrorState(status)) {
52+
builder.setContentTitle(context.getString(R.string.download_failed));
53+
} else if (status.equals(UpdateStatus.DOWNLOADED)) {
54+
builder.setContentTitle(context.getString(R.string.download_completed));
55+
} else {
56+
builder.setContentTitle(context.getString(R.string.downloading_app_update));
57+
}
58+
builder.setProgress(
59+
100,
60+
(int) (((float) downloadedBytes / (float) totalBytes) * 100),
61+
/*indeterminate = */ false);
62+
notificationManager.notify(NOTIFICATION_TAG, /*id =*/ 0, builder.build());
63+
}
64+
65+
private boolean isErrorState(UpdateStatus status) {
66+
return status.equals(UpdateStatus.DOWNLOAD_FAILED)
67+
|| status.equals(UpdateStatus.INSTALL_FAILED)
68+
|| status.equals(UpdateStatus.INSTALL_CANCELED);
69+
}
70+
71+
private NotificationManager createNotificationManager(Context context) {
72+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
73+
NotificationChannel channel =
74+
new NotificationChannel(
75+
NOTIFICATION_CHANNEL_ID,
76+
context.getString(R.string.notifications_channel_name),
77+
NotificationManager.IMPORTANCE_DEFAULT);
78+
channel.setDescription(context.getString(R.string.notifications_channel_description));
79+
// Register the channel with the system; you can't change the importance
80+
// or other notification behaviors after this
81+
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
82+
notificationManager.createNotificationChannel(channel);
83+
return notificationManager;
84+
} else {
85+
return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
86+
}
87+
}
88+
89+
private NotificationCompat.Builder createNotificationBuilder() {
90+
Context context = firebaseApp.getApplicationContext();
91+
return new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
92+
.setOnlyAlertOnce(true)
93+
.setSmallIcon(getSmallIcon())
94+
.setContentIntent(createPendingIntent());
95+
}
96+
97+
private PendingIntent createPendingIntent() {
98+
// Query the package manager for the best launch intent for the app
99+
Context context = firebaseApp.getApplicationContext();
100+
Intent intent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
101+
if (intent == null) {
102+
Log.w(TAG, "No activity found to launch app");
103+
}
104+
return PendingIntent.getActivity(
105+
firebaseApp.getApplicationContext(), 0, intent, PendingIntent.FLAG_ONE_SHOT);
106+
}
107+
108+
@VisibleForTesting
109+
int getSmallIcon() {
110+
Context context = firebaseApp.getApplicationContext();
111+
int iconId = context.getApplicationInfo().icon;
112+
113+
if (iconId == 0 || isAdaptiveIcon(iconId)) {
114+
// fallback to default icon
115+
return android.R.drawable.sym_def_app_icon;
116+
}
117+
118+
return iconId;
119+
}
120+
121+
/** Adaptive icons cause a crash loop in the Notifications Manager. See b/69969749. */
122+
private boolean isAdaptiveIcon(int iconId) {
123+
try {
124+
Drawable icon = ContextCompat.getDrawable(firebaseApp.getApplicationContext(), iconId);
125+
if (VERSION.SDK_INT >= Build.VERSION_CODES.O && icon instanceof AdaptiveIconDrawable) {
126+
Log.e(TAG, "Adaptive icons cannot be used in notifications. Ignoring icon id: " + iconId);
127+
return true;
128+
} else {
129+
// AdaptiveIcons were introduced in API 26
130+
return false;
131+
}
132+
} catch (Resources.NotFoundException ex) {
133+
return true;
134+
}
135+
}
136+
}

firebase-app-distribution/src/main/java/com/google/firebase/appdistribution/UpdateApkClient.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,14 @@ class UpdateApkClient {
4747
private final int UPDATE_INTERVAL_MS = 250;
4848
private static final String TAG = "FADUpdateAppClient";
4949
private static final String REQUEST_METHOD = "GET";
50+
private final FirebaseAppDistributionNotificationsManager appDistributionNotificationsManager;
51+
5052
private TaskCompletionSource<File> downloadTaskCompletionSource;
5153
private final Executor downloadExecutor;
5254
private TaskCompletionSource<Void> installTaskCompletionSource;
5355
private final FirebaseApp firebaseApp;
5456
private UpdateTaskImpl cachedUpdateTask;
57+
private boolean showDownloadInNotificationManager = false;
5558

5659
@GuardedBy("activityLock")
5760
private Activity currentActivity;
@@ -61,10 +64,15 @@ class UpdateApkClient {
6164
public UpdateApkClient(@NonNull FirebaseApp firebaseApp) {
6265
this.downloadExecutor = Executors.newSingleThreadExecutor();
6366
this.firebaseApp = firebaseApp;
67+
this.appDistributionNotificationsManager =
68+
new FirebaseAppDistributionNotificationsManager(firebaseApp);
6469
}
6570

66-
public void updateApk(@NonNull UpdateTaskImpl updateTask, @NonNull String downloadUrl) {
67-
71+
public void updateApk(
72+
@NonNull UpdateTaskImpl updateTask,
73+
@NonNull String downloadUrl,
74+
@NonNull boolean showDownloadInNotificationManager) {
75+
this.showDownloadInNotificationManager = showDownloadInNotificationManager;
6876
this.cachedUpdateTask = updateTask;
6977
downloadApk(downloadUrl)
7078
.addOnSuccessListener(
@@ -278,13 +286,17 @@ private void setTaskCompletionErrorWithDefault(
278286
}
279287
}
280288

281-
private void postUpdateProgress(long totalBytes, long downloadedBytes, UpdateStatus status) {
289+
@VisibleForTesting
290+
void postUpdateProgress(long totalBytes, long downloadedBytes, UpdateStatus status) {
282291
cachedUpdateTask.updateProgress(
283292
UpdateProgress.builder()
284293
.setApkFileTotalBytes(totalBytes)
285294
.setApkBytesDownloaded(downloadedBytes)
286295
.setUpdateStatus(status)
287296
.build());
297+
if (showDownloadInNotificationManager) {
298+
appDistributionNotificationsManager.updateNotification(totalBytes, downloadedBytes, status);
299+
}
288300
}
289301

290302
private void postInstallationFailure(Exception e, long fileLength) {

firebase-app-distribution/src/main/java/com/google/firebase/appdistribution/UpdateAppClient.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import androidx.annotation.GuardedBy;
2323
import androidx.annotation.NonNull;
2424
import androidx.annotation.Nullable;
25-
import com.google.android.gms.tasks.TaskCompletionSource;
2625
import com.google.firebase.FirebaseApp;
2726
import com.google.firebase.appdistribution.internal.AppDistributionReleaseInternal;
2827

@@ -35,7 +34,6 @@ public class UpdateAppClient {
3534
private Activity currentActivity;
3635

3736
private final Object activityLock = new Object();
38-
private TaskCompletionSource<Void> updateAppTaskCompletionSource = null;
3937
private UpdateTaskImpl cachedUpdateAppTask;
4038

4139
public UpdateAppClient(@NonNull FirebaseApp firebaseApp) {
@@ -44,7 +42,8 @@ public UpdateAppClient(@NonNull FirebaseApp firebaseApp) {
4442

4543
@NonNull
4644
synchronized UpdateTask updateApp(
47-
@NonNull AppDistributionReleaseInternal latestRelease, @NonNull Activity currentActivity) {
45+
@NonNull AppDistributionReleaseInternal latestRelease,
46+
@NonNull boolean showDownloadInNotificationManager) {
4847

4948
if (cachedUpdateAppTask != null && !cachedUpdateAppTask.isComplete()) {
5049
return cachedUpdateAppTask;
@@ -70,7 +69,8 @@ synchronized UpdateTask updateApp(
7069
if (latestRelease.getBinaryType() == BinaryType.AAB) {
7170
redirectToPlayForAabUpdate(cachedUpdateAppTask, latestRelease.getDownloadUrl());
7271
} else {
73-
this.updateApkClient.updateApk(cachedUpdateAppTask, latestRelease.getDownloadUrl());
72+
this.updateApkClient.updateApk(
73+
cachedUpdateAppTask, latestRelease.getDownloadUrl(), showDownloadInNotificationManager);
7474
}
7575
return cachedUpdateAppTask;
7676
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="24"
5+
android:viewportHeight="24"
6+
android:tint="#FFFFFF">
7+
<group android:scaleX="0.92"
8+
android:scaleY="0.92"
9+
android:translateX="0.96"
10+
android:translateY="0.96">
11+
<path
12+
android:pathData="M17.6,11.48 L19.44,8.3a0.63,0.63 0,0 0,-1.09 -0.63l-1.88,3.24a11.43,11.43 0,0 0,-8.94 0L5.65,7.67a0.63,0.63 0,0 0,-1.09 0.63L6.4,11.48A10.81,10.81 0,0 0,1 20L23,20A10.81,10.81 0,0 0,17.6 11.48ZM7,17.25A1.25,1.25 0,1 1,8.25 16,1.25 1.25,0 0,1 7,17.25ZM17,17.25A1.25,1.25 0,1 1,18.25 16,1.25 1.25,0 0,1 17,17.25Z"
13+
android:fillColor="#FF000000"/>
14+
</group>
15+
</vector>
373 Bytes
Loading
265 Bytes
Loading
478 Bytes
Loading
673 Bytes
Loading
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<vector
3+
android:height="108dp"
4+
android:width="108dp"
5+
android:viewportHeight="108"
6+
android:viewportWidth="108"
7+
xmlns:android="http://schemas.android.com/apk/res/android">
8+
<path android:fillColor="#3DDC84"
9+
android:pathData="M0,0h108v108h-108z"/>
10+
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
11+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
12+
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
13+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
14+
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
15+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
16+
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
17+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
18+
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
19+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
20+
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
21+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
22+
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
23+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
24+
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
25+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
26+
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
27+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
28+
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
29+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
30+
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
31+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
32+
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
33+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
34+
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
35+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
36+
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
37+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
38+
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
39+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
40+
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
41+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
42+
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
43+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
44+
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
45+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
46+
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
47+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
48+
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
49+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
50+
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
51+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
52+
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
53+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
54+
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
55+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
56+
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
57+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
58+
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
59+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
60+
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
61+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
62+
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
63+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
64+
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
65+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
66+
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
67+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
68+
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
69+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
70+
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
71+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
72+
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
73+
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
74+
</vector>

0 commit comments

Comments
 (0)