Skip to content

Commit 5c73af0

Browse files
committed
Use ScheduleExecutorService to cancel feedback notification if app is in background.
1 parent 712ed5a commit 5c73af0

File tree

6 files changed

+174
-52
lines changed

6 files changed

+174
-52
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import dagger.Component;
3030
import dagger.Module;
3131
import java.util.concurrent.Executor;
32+
import java.util.concurrent.ScheduledExecutorService;
3233
import javax.inject.Singleton;
3334

3435
@Component(modules = AppDistroComponent.MainModule.class)
@@ -71,6 +72,9 @@ interface Builder {
7172
@BindsInstance
7273
Builder setLightweightExecutor(@Lightweight Executor executor);
7374

75+
@BindsInstance
76+
Builder setLightweightScheduledExecutorService(@Lightweight ScheduledExecutorService executor);
77+
7478
@BindsInstance
7579
Builder setUiThreadExecutor(@UiThread Executor executor);
7680

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

Lines changed: 112 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414

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

17+
import static com.google.firebase.appdistribution.impl.FirebaseAppDistributionNotificationsManager.NotificationType.APP_UPDATE;
18+
import static com.google.firebase.appdistribution.impl.FirebaseAppDistributionNotificationsManager.NotificationType.FEEDBACK;
19+
import static java.util.concurrent.TimeUnit.SECONDS;
20+
21+
import android.app.Activity;
22+
import android.app.Notification;
1723
import android.app.NotificationChannel;
1824
import android.app.NotificationChannelGroup;
1925
import android.app.PendingIntent;
@@ -26,29 +32,42 @@
2632
import androidx.annotation.Nullable;
2733
import androidx.annotation.RequiresApi;
2834
import androidx.annotation.VisibleForTesting;
35+
import androidx.annotation.WorkerThread;
2936
import androidx.core.app.NotificationCompat;
3037
import androidx.core.app.NotificationManagerCompat;
38+
import com.google.firebase.annotations.concurrent.Lightweight;
39+
import com.google.firebase.annotations.concurrent.UiThread;
3140
import com.google.firebase.appdistribution.InterruptionLevel;
41+
import com.google.firebase.appdistribution.impl.FirebaseAppDistributionLifecycleNotifier.OnActivityPausedListener;
42+
import com.google.firebase.appdistribution.impl.FirebaseAppDistributionLifecycleNotifier.OnActivityResumedListener;
43+
import java.util.concurrent.Executor;
44+
import java.util.concurrent.ScheduledExecutorService;
45+
import java.util.concurrent.ScheduledFuture;
3246
import javax.inject.Inject;
47+
import javax.inject.Singleton;
48+
49+
@Singleton
50+
class FirebaseAppDistributionNotificationsManager implements OnActivityPausedListener,
51+
OnActivityResumedListener {
3352

34-
class FirebaseAppDistributionNotificationsManager {
3553
private static final String TAG = "NotificationsManager";
3654

3755
private static final String PACKAGE_PREFIX = "com.google.firebase.appdistribution";
3856

3957
@VisibleForTesting
4058
static final String CHANNEL_GROUP_ID = prependPackage("notification_channel_group_id");
4159

60+
4261
@VisibleForTesting
43-
enum Notification {
62+
enum NotificationType {
4463
APP_UPDATE("notification_channel_id", "app_update_notification_tag"),
4564
FEEDBACK("feedback_notification_channel_id", "feedback_notification_tag");
4665

4766
final String channelId;
4867
final String tag;
4968
final int id;
5069

51-
Notification(String channelId, String tag) {
70+
NotificationType(String channelId, String tag) {
5271
this.channelId = prependPackage(channelId);
5372
this.tag = prependPackage(tag);
5473
this.id = ordinal();
@@ -58,12 +77,28 @@ enum Notification {
5877
private final Context context;
5978
private final AppIconSource appIconSource;
6079
private final NotificationManagerCompat notificationManager;
80+
@Lightweight
81+
private final ScheduledExecutorService scheduledExecutorService;
82+
@UiThread
83+
private final Executor uiThreadExecutor;
84+
85+
private Notification feedbackNotificationToBeShown;
86+
private ScheduledFuture<?> feedbackNotificationCancellationFuture;
6187

6288
@Inject
63-
FirebaseAppDistributionNotificationsManager(Context context, AppIconSource appIconSource) {
89+
FirebaseAppDistributionNotificationsManager(
90+
Context context,
91+
AppIconSource appIconSource,
92+
FirebaseAppDistributionLifecycleNotifier lifecycleNotifier,
93+
@Lightweight ScheduledExecutorService scheduledExecutorService,
94+
@UiThread Executor uiThreadExecutor) {
6495
this.context = context;
6596
this.appIconSource = appIconSource;
6697
this.notificationManager = NotificationManagerCompat.from(context);
98+
lifecycleNotifier.addOnActivityPausedListener(this);
99+
lifecycleNotifier.addOnActivityResumedListener(this);
100+
this.scheduledExecutorService = scheduledExecutorService;
101+
this.uiThreadExecutor = uiThreadExecutor;
67102
}
68103

69104
void showAppUpdateNotification(long totalBytes, long downloadedBytes, int stringResourceId) {
@@ -72,7 +107,7 @@ void showAppUpdateNotification(long totalBytes, long downloadedBytes, int string
72107
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
73108
LogWrapper.i(TAG, "Creating app update notification channel group");
74109
createChannel(
75-
Notification.APP_UPDATE,
110+
APP_UPDATE,
76111
R.string.app_update_notification_channel_name,
77112
R.string.app_update_notification_channel_description,
78113
InterruptionLevel.DEFAULT);
@@ -85,7 +120,7 @@ void showAppUpdateNotification(long totalBytes, long downloadedBytes, int string
85120
}
86121

87122
NotificationCompat.Builder notificationBuilder =
88-
new NotificationCompat.Builder(context, Notification.APP_UPDATE.channelId)
123+
new NotificationCompat.Builder(context, APP_UPDATE.channelId)
89124
.setOnlyAlertOnce(true)
90125
.setSmallIcon(appIconSource.getNonAdaptiveIconOrDefault(context))
91126
.setContentTitle(context.getString(stringResourceId))
@@ -97,8 +132,7 @@ void showAppUpdateNotification(long totalBytes, long downloadedBytes, int string
97132
if (appLaunchIntent != null) {
98133
notificationBuilder.setContentIntent(appLaunchIntent);
99134
}
100-
notificationManager.notify(
101-
Notification.APP_UPDATE.tag, Notification.APP_UPDATE.id, notificationBuilder.build());
135+
notificationManager.notify(APP_UPDATE.tag, APP_UPDATE.id, notificationBuilder.build());
102136
}
103137

104138
@Nullable
@@ -128,7 +162,7 @@ public void showFeedbackNotification(
128162
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
129163
LogWrapper.i(TAG, "Creating feedback notification channel group");
130164
createChannel(
131-
Notification.FEEDBACK,
165+
FEEDBACK,
132166
R.string.feedback_notification_channel_name,
133167
R.string.feedback_notification_channel_description,
134168
interruptionLevel);
@@ -139,36 +173,58 @@ public void showFeedbackNotification(
139173
return;
140174
}
141175

176+
uiThreadExecutor.execute(() -> {
177+
// ensure that class state is managed on same thread as lifecycle callbacks
178+
cancelFeedbackCancellationFuture();
179+
feedbackNotificationToBeShown = buildFeedbackNotification(infoText, interruptionLevel);
180+
doShowFeedbackNotification();
181+
});
182+
}
183+
184+
// this must be run on the main (UI) thread
185+
private void doShowFeedbackNotification() {
186+
LogWrapper.i(TAG, "Showing feedback notification");
187+
notificationManager.notify(FEEDBACK.tag, FEEDBACK.id, feedbackNotificationToBeShown);
188+
}
189+
190+
private Notification buildFeedbackNotification(
191+
@NonNull CharSequence infoText, @NonNull InterruptionLevel interruptionLevel) {
142192
Intent intent = new Intent(context, TakeScreenshotAndStartFeedbackActivity.class);
143193
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
144194
intent.putExtra(TakeScreenshotAndStartFeedbackActivity.INFO_TEXT_EXTRA_KEY, infoText);
145195
ApplicationInfo applicationInfo = context.getApplicationInfo();
146196
PackageManager packageManager = context.getPackageManager();
147197
CharSequence appLabel = packageManager.getApplicationLabel(applicationInfo);
148-
NotificationCompat.Builder builder =
149-
new NotificationCompat.Builder(context, Notification.FEEDBACK.channelId)
150-
.setSmallIcon(R.drawable.ic_baseline_rate_review_24)
151-
.setContentTitle(context.getString(R.string.feedback_notification_title))
152-
.setContentText(context.getString(R.string.feedback_notification_text, appLabel))
153-
.setPriority(interruptionLevel.notificationPriority)
154-
.setOngoing(true)
155-
.setOnlyAlertOnce(true)
156-
.setAutoCancel(false)
157-
.setContentIntent(getPendingIntent(intent, /* extraFlags= */ 0));
158-
LogWrapper.i(TAG, "Showing feedback notification");
159-
notificationManager.notify(
160-
Notification.FEEDBACK.tag, Notification.FEEDBACK.id, builder.build());
198+
return new NotificationCompat.Builder(context, FEEDBACK.channelId)
199+
.setSmallIcon(R.drawable.ic_baseline_rate_review_24)
200+
.setContentTitle(context.getString(R.string.feedback_notification_title))
201+
.setContentText(context.getString(R.string.feedback_notification_text, appLabel))
202+
.setPriority(interruptionLevel.notificationPriority)
203+
.setOngoing(true)
204+
.setOnlyAlertOnce(true)
205+
.setAutoCancel(false)
206+
.setContentIntent(getPendingIntent(intent, /* extraFlags= */ 0))
207+
.build();
161208
}
162209

163210
public void cancelFeedbackNotification() {
211+
uiThreadExecutor.execute(() -> {
212+
// ensure that class state is managed on same thread as lifecycle callbacks
213+
feedbackNotificationToBeShown = null;
214+
cancelFeedbackCancellationFuture();
215+
doCancelFeedbackNotification();
216+
});
217+
}
218+
219+
public void doCancelFeedbackNotification() {
164220
LogWrapper.i(TAG, "Cancelling feedback notification");
165-
NotificationManagerCompat.from(context)
166-
.cancel(Notification.FEEDBACK.tag, Notification.FEEDBACK.id);
221+
NotificationManagerCompat.from(context).cancel(FEEDBACK.tag, FEEDBACK.id);
167222
}
168223

169224
@RequiresApi(Build.VERSION_CODES.O)
170225
private void createChannel(
171-
Notification notification, int name, int description, InterruptionLevel interruptionLevel) {
226+
NotificationType notification, int name, int description,
227+
InterruptionLevel interruptionLevel) {
172228
notificationManager.createNotificationChannelGroup(
173229
new NotificationChannelGroup(
174230
CHANNEL_GROUP_ID, context.getString(R.string.notifications_group_name)));
@@ -182,6 +238,37 @@ private void createChannel(
182238
notificationManager.createNotificationChannel(channel);
183239
}
184240

241+
// this runs on the main (UI) thread
242+
@Override
243+
public void onPaused(Activity activity) {
244+
LogWrapper.d(TAG, "Activity paused");
245+
if (feedbackNotificationToBeShown != null) {
246+
LogWrapper.d(TAG, "Scheduling cancelFeedbackNotification");
247+
cancelFeedbackCancellationFuture();
248+
feedbackNotificationCancellationFuture =
249+
scheduledExecutorService.schedule(this::doCancelFeedbackNotification, 1, SECONDS);
250+
}
251+
}
252+
253+
// this runs on the main (UI) thread
254+
@Override
255+
public void onResumed(Activity activity) {
256+
LogWrapper.d(TAG, "Activity resumed");
257+
if (feedbackNotificationToBeShown != null) {
258+
cancelFeedbackCancellationFuture();
259+
doShowFeedbackNotification();
260+
}
261+
}
262+
263+
// this must be run on the main (UI) thread
264+
private void cancelFeedbackCancellationFuture() {
265+
if (feedbackNotificationCancellationFuture != null) {
266+
LogWrapper.d(TAG, "Canceling feedbackNotificationCancellationFuture");
267+
feedbackNotificationCancellationFuture.cancel(false);
268+
feedbackNotificationCancellationFuture = null;
269+
}
270+
}
271+
185272
private static String prependPackage(String id) {
186273
return String.format("%s.%s", PACKAGE_PREFIX, id);
187274
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import java.util.Arrays;
3535
import java.util.List;
3636
import java.util.concurrent.Executor;
37+
import java.util.concurrent.ScheduledExecutorService;
3738

3839
/**
3940
* Registers FirebaseAppDistribution and related components.
@@ -52,6 +53,8 @@ public class FirebaseAppDistributionRegistrar implements ComponentRegistrar {
5253
Qualified<Executor> blockingExecutor = Qualified.qualified(Blocking.class, Executor.class);
5354
Qualified<Executor> lightweightExecutor =
5455
Qualified.qualified(Lightweight.class, Executor.class);
56+
Qualified<ScheduledExecutorService> lightweightScheduledExecutorService =
57+
Qualified.qualified(Lightweight.class, ScheduledExecutorService.class);
5558
Qualified<Executor> uiThreadExecutor = Qualified.qualified(UiThread.class, Executor.class);
5659
return Arrays.asList(
5760
Component.builder(FirebaseAppDistribution.class)
@@ -74,6 +77,7 @@ public class FirebaseAppDistributionRegistrar implements ComponentRegistrar {
7477
.add(Dependency.required(backgroundExecutor))
7578
.add(Dependency.required(blockingExecutor))
7679
.add(Dependency.required(lightweightExecutor))
80+
.add(Dependency.required(lightweightScheduledExecutorService))
7781
.add(Dependency.required(uiThreadExecutor))
7882
.factory(
7983
container ->
@@ -85,6 +89,8 @@ public class FirebaseAppDistributionRegistrar implements ComponentRegistrar {
8589
.setBackgroundExecutor(container.get(backgroundExecutor))
8690
.setBlockingExecutor(container.get(blockingExecutor))
8791
.setLightweightExecutor(container.get(lightweightExecutor))
92+
.setLightweightScheduledExecutorService(
93+
container.get(lightweightScheduledExecutorService))
8894
.setUiThreadExecutor(container.get(uiThreadExecutor))
8995
.build())
9096
.build(),

firebase-appdistribution/src/test/java/com/google/firebase/appdistribution/impl/FirebaseAppDistributionNotificationsManagerTest.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
import static android.content.Context.NOTIFICATION_SERVICE;
1919
import static com.google.common.truth.Truth.assertThat;
2020
import static com.google.firebase.appdistribution.impl.FirebaseAppDistributionNotificationsManager.CHANNEL_GROUP_ID;
21-
import static com.google.firebase.appdistribution.impl.FirebaseAppDistributionNotificationsManager.Notification.APP_UPDATE;
22-
import static com.google.firebase.appdistribution.impl.FirebaseAppDistributionNotificationsManager.Notification.FEEDBACK;
21+
import static com.google.firebase.appdistribution.impl.FirebaseAppDistributionNotificationsManager.NotificationType.APP_UPDATE;
22+
import static com.google.firebase.appdistribution.impl.FirebaseAppDistributionNotificationsManager.NotificationType.FEEDBACK;
2323
import static org.robolectric.Shadows.shadowOf;
2424

2525
import android.app.Notification;
@@ -28,18 +28,30 @@
2828
import android.content.Intent;
2929
import androidx.test.core.app.ApplicationProvider;
3030
import com.google.firebase.FirebaseApp;
31+
import com.google.firebase.annotations.concurrent.Lightweight;
32+
import com.google.firebase.annotations.concurrent.UiThread;
3133
import com.google.firebase.appdistribution.InterruptionLevel;
34+
import com.google.firebase.concurrent.TestOnlyExecutors;
35+
import java.util.concurrent.Executor;
36+
import java.util.concurrent.ScheduledExecutorService;
3237
import org.junit.Before;
3338
import org.junit.Test;
3439
import org.junit.runner.RunWith;
40+
import org.mockito.Mock;
3541
import org.mockito.MockitoAnnotations;
3642
import org.robolectric.RobolectricTestRunner;
3743

3844
@RunWith(RobolectricTestRunner.class)
3945
public class FirebaseAppDistributionNotificationsManagerTest {
46+
@Lightweight
47+
private final ScheduledExecutorService lightweightExecutor = TestOnlyExecutors.lite();
48+
@UiThread
49+
private final Executor uiThreadExecutor = TestOnlyExecutors.ui();
4050

4151
private FirebaseAppDistributionNotificationsManager firebaseAppDistributionNotificationsManager;
4252
private NotificationManager notificationManager;
53+
@Mock
54+
private FirebaseAppDistributionLifecycleNotifier mockLifecycleNotifier;
4355

4456
@Before
4557
public void setup() {
@@ -50,7 +62,8 @@ public void setup() {
5062
ApplicationProvider.getApplicationContext().getSystemService(NOTIFICATION_SERVICE);
5163
firebaseAppDistributionNotificationsManager =
5264
new FirebaseAppDistributionNotificationsManager(
53-
ApplicationProvider.getApplicationContext(), new AppIconSource());
65+
ApplicationProvider.getApplicationContext(), new AppIconSource(), mockLifecycleNotifier,
66+
lightweightExecutor, uiThreadExecutor);
5467
}
5568

5669
@Test
@@ -135,7 +148,7 @@ public void showFeedbackNotification_setsIntentToScreenshotActivity() {
135148
Intent actualIntent = shadowOf(notification.contentIntent).getSavedIntent();
136149
assertThat(actualIntent.getComponent()).isEqualTo(expectedIntent.getComponent());
137150
assertThat(
138-
actualIntent.getStringExtra(TakeScreenshotAndStartFeedbackActivity.INFO_TEXT_EXTRA_KEY))
151+
actualIntent.getStringExtra(TakeScreenshotAndStartFeedbackActivity.INFO_TEXT_EXTRA_KEY))
139152
.isEqualTo("Terms and conditions");
140153
}
141154

0 commit comments

Comments
 (0)