Skip to content

Commit ca88589

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

File tree

6 files changed

+275
-59
lines changed

6 files changed

+275
-59
lines changed

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

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
import dagger.Component;
3030
import dagger.Module;
3131
import java.util.concurrent.Executor;
32+
import java.util.concurrent.ExecutorService;
33+
import java.util.concurrent.ScheduledExecutorService;
3234
import javax.inject.Singleton;
3335

3436
@Component(modules = AppDistroComponent.MainModule.class)
@@ -63,13 +65,13 @@ interface Builder {
6365
Builder setFis(Provider<FirebaseInstallationsApi> fis);
6466

6567
@BindsInstance
66-
Builder setBackgroundExecutor(@Background Executor executor);
68+
Builder setBackgroundExecutor(@Background ScheduledExecutorService executor);
6769

6870
@BindsInstance
69-
Builder setBlockingExecutor(@Blocking Executor executor);
71+
Builder setBlockingExecutor(@Blocking ScheduledExecutorService executor);
7072

7173
@BindsInstance
72-
Builder setLightweightExecutor(@Lightweight Executor executor);
74+
Builder setLightweightExecutor(@Lightweight ScheduledExecutorService executor);
7375

7476
@BindsInstance
7577
Builder setUiThreadExecutor(@UiThread Executor executor);
@@ -86,5 +88,23 @@ interface Builder {
8688
interface MainModule {
8789
@Binds
8890
FirebaseAppDistribution bindAppDistro(FirebaseAppDistributionImpl impl);
91+
92+
@Binds @Background
93+
ExecutorService bindBackgroundExecutorService(@Background ScheduledExecutorService ses);
94+
95+
@Binds @Background
96+
Executor bindBackgroundExecutor(@Background ExecutorService es);
97+
98+
@Binds @Lightweight
99+
ExecutorService bindLightweightExecutorService(@Lightweight ScheduledExecutorService ses);
100+
101+
@Binds @Lightweight
102+
Executor bindLightweightExecutor(@Lightweight ExecutorService es);
103+
104+
@Binds @Blocking
105+
ExecutorService bindBlockingExecutorService(@Blocking ScheduledExecutorService ses);
106+
107+
@Binds @Blocking
108+
Executor bindBlockingExecutor(@Blocking ExecutorService es);
89109
}
90110
}

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

Lines changed: 111 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;
@@ -28,27 +34,39 @@
2834
import androidx.annotation.VisibleForTesting;
2935
import androidx.core.app.NotificationCompat;
3036
import androidx.core.app.NotificationManagerCompat;
37+
import com.google.firebase.annotations.concurrent.Lightweight;
38+
import com.google.firebase.annotations.concurrent.UiThread;
3139
import com.google.firebase.appdistribution.InterruptionLevel;
40+
import com.google.firebase.appdistribution.impl.FirebaseAppDistributionLifecycleNotifier.OnActivityPausedListener;
41+
import com.google.firebase.appdistribution.impl.FirebaseAppDistributionLifecycleNotifier.OnActivityResumedListener;
42+
import java.util.concurrent.Executor;
43+
import java.util.concurrent.ScheduledExecutorService;
44+
import java.util.concurrent.ScheduledFuture;
3245
import javax.inject.Inject;
46+
import javax.inject.Singleton;
47+
48+
@Singleton
49+
class FirebaseAppDistributionNotificationsManager implements OnActivityPausedListener,
50+
OnActivityResumedListener {
3351

34-
class FirebaseAppDistributionNotificationsManager {
3552
private static final String TAG = "NotificationsManager";
3653

3754
private static final String PACKAGE_PREFIX = "com.google.firebase.appdistribution";
3855

3956
@VisibleForTesting
4057
static final String CHANNEL_GROUP_ID = prependPackage("notification_channel_group_id");
4158

59+
4260
@VisibleForTesting
43-
enum Notification {
61+
enum NotificationType {
4462
APP_UPDATE("notification_channel_id", "app_update_notification_tag"),
4563
FEEDBACK("feedback_notification_channel_id", "feedback_notification_tag");
4664

4765
final String channelId;
4866
final String tag;
4967
final int id;
5068

51-
Notification(String channelId, String tag) {
69+
NotificationType(String channelId, String tag) {
5270
this.channelId = prependPackage(channelId);
5371
this.tag = prependPackage(tag);
5472
this.id = ordinal();
@@ -58,12 +76,28 @@ enum Notification {
5876
private final Context context;
5977
private final AppIconSource appIconSource;
6078
private final NotificationManagerCompat notificationManager;
79+
@Lightweight
80+
private final ScheduledExecutorService scheduledExecutorService;
81+
@UiThread
82+
private final Executor uiThreadExecutor;
83+
84+
private Notification feedbackNotificationToBeShown;
85+
private ScheduledFuture<?> feedbackNotificationCancellationFuture;
6186

6287
@Inject
63-
FirebaseAppDistributionNotificationsManager(Context context, AppIconSource appIconSource) {
88+
FirebaseAppDistributionNotificationsManager(
89+
Context context,
90+
AppIconSource appIconSource,
91+
FirebaseAppDistributionLifecycleNotifier lifecycleNotifier,
92+
@Lightweight ScheduledExecutorService scheduledExecutorService,
93+
@UiThread Executor uiThreadExecutor) {
6494
this.context = context;
6595
this.appIconSource = appIconSource;
6696
this.notificationManager = NotificationManagerCompat.from(context);
97+
lifecycleNotifier.addOnActivityPausedListener(this);
98+
lifecycleNotifier.addOnActivityResumedListener(this);
99+
this.scheduledExecutorService = scheduledExecutorService;
100+
this.uiThreadExecutor = uiThreadExecutor;
67101
}
68102

69103
void showAppUpdateNotification(long totalBytes, long downloadedBytes, int stringResourceId) {
@@ -72,7 +106,7 @@ void showAppUpdateNotification(long totalBytes, long downloadedBytes, int string
72106
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
73107
LogWrapper.i(TAG, "Creating app update notification channel group");
74108
createChannel(
75-
Notification.APP_UPDATE,
109+
APP_UPDATE,
76110
R.string.app_update_notification_channel_name,
77111
R.string.app_update_notification_channel_description,
78112
InterruptionLevel.DEFAULT);
@@ -85,7 +119,7 @@ void showAppUpdateNotification(long totalBytes, long downloadedBytes, int string
85119
}
86120

87121
NotificationCompat.Builder notificationBuilder =
88-
new NotificationCompat.Builder(context, Notification.APP_UPDATE.channelId)
122+
new NotificationCompat.Builder(context, APP_UPDATE.channelId)
89123
.setOnlyAlertOnce(true)
90124
.setSmallIcon(appIconSource.getNonAdaptiveIconOrDefault(context))
91125
.setContentTitle(context.getString(stringResourceId))
@@ -97,8 +131,7 @@ void showAppUpdateNotification(long totalBytes, long downloadedBytes, int string
97131
if (appLaunchIntent != null) {
98132
notificationBuilder.setContentIntent(appLaunchIntent);
99133
}
100-
notificationManager.notify(
101-
Notification.APP_UPDATE.tag, Notification.APP_UPDATE.id, notificationBuilder.build());
134+
notificationManager.notify(APP_UPDATE.tag, APP_UPDATE.id, notificationBuilder.build());
102135
}
103136

104137
@Nullable
@@ -128,7 +161,7 @@ public void showFeedbackNotification(
128161
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
129162
LogWrapper.i(TAG, "Creating feedback notification channel group");
130163
createChannel(
131-
Notification.FEEDBACK,
164+
FEEDBACK,
132165
R.string.feedback_notification_channel_name,
133166
R.string.feedback_notification_channel_description,
134167
interruptionLevel);
@@ -139,36 +172,58 @@ public void showFeedbackNotification(
139172
return;
140173
}
141174

175+
uiThreadExecutor.execute(() -> {
176+
// ensure that class state is managed on same thread as lifecycle callbacks
177+
cancelFeedbackCancellationFuture();
178+
feedbackNotificationToBeShown = buildFeedbackNotification(infoText, interruptionLevel);
179+
doShowFeedbackNotification();
180+
});
181+
}
182+
183+
// this must be run on the main (UI) thread
184+
private void doShowFeedbackNotification() {
185+
LogWrapper.i(TAG, "Showing feedback notification");
186+
notificationManager.notify(FEEDBACK.tag, FEEDBACK.id, feedbackNotificationToBeShown);
187+
}
188+
189+
private Notification buildFeedbackNotification(
190+
@NonNull CharSequence infoText, @NonNull InterruptionLevel interruptionLevel) {
142191
Intent intent = new Intent(context, TakeScreenshotAndStartFeedbackActivity.class);
143192
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
144193
intent.putExtra(TakeScreenshotAndStartFeedbackActivity.INFO_TEXT_EXTRA_KEY, infoText);
145194
ApplicationInfo applicationInfo = context.getApplicationInfo();
146195
PackageManager packageManager = context.getPackageManager();
147196
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());
197+
return new NotificationCompat.Builder(context, FEEDBACK.channelId)
198+
.setSmallIcon(R.drawable.ic_baseline_rate_review_24)
199+
.setContentTitle(context.getString(R.string.feedback_notification_title))
200+
.setContentText(context.getString(R.string.feedback_notification_text, appLabel))
201+
.setPriority(interruptionLevel.notificationPriority)
202+
.setOngoing(true)
203+
.setOnlyAlertOnce(true)
204+
.setAutoCancel(false)
205+
.setContentIntent(getPendingIntent(intent, /* extraFlags= */ 0))
206+
.build();
161207
}
162208

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

169223
@RequiresApi(Build.VERSION_CODES.O)
170224
private void createChannel(
171-
Notification notification, int name, int description, InterruptionLevel interruptionLevel) {
225+
NotificationType notification, int name, int description,
226+
InterruptionLevel interruptionLevel) {
172227
notificationManager.createNotificationChannelGroup(
173228
new NotificationChannelGroup(
174229
CHANNEL_GROUP_ID, context.getString(R.string.notifications_group_name)));
@@ -182,6 +237,37 @@ private void createChannel(
182237
notificationManager.createNotificationChannel(channel);
183238
}
184239

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

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

Lines changed: 7 additions & 4 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.
@@ -48,10 +49,12 @@ public class FirebaseAppDistributionRegistrar implements ComponentRegistrar {
4849

4950
@Override
5051
public @NonNull List<Component<?>> getComponents() {
51-
Qualified<Executor> backgroundExecutor = Qualified.qualified(Background.class, Executor.class);
52-
Qualified<Executor> blockingExecutor = Qualified.qualified(Blocking.class, Executor.class);
53-
Qualified<Executor> lightweightExecutor =
54-
Qualified.qualified(Lightweight.class, Executor.class);
52+
Qualified<ScheduledExecutorService> backgroundExecutor =
53+
Qualified.qualified(Background.class, ScheduledExecutorService.class);
54+
Qualified<ScheduledExecutorService> blockingExecutor =
55+
Qualified.qualified(Blocking.class, ScheduledExecutorService.class);
56+
Qualified<ScheduledExecutorService> lightweightExecutor =
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)

0 commit comments

Comments
 (0)