14
14
15
15
package com .google .firebase .appdistribution .impl ;
16
16
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 ;
17
23
import android .app .NotificationChannel ;
18
24
import android .app .NotificationChannelGroup ;
19
25
import android .app .PendingIntent ;
28
34
import androidx .annotation .VisibleForTesting ;
29
35
import androidx .core .app .NotificationCompat ;
30
36
import androidx .core .app .NotificationManagerCompat ;
37
+ import com .google .firebase .annotations .concurrent .Lightweight ;
38
+ import com .google .firebase .annotations .concurrent .UiThread ;
31
39
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 ;
32
45
import javax .inject .Inject ;
46
+ import javax .inject .Singleton ;
47
+
48
+ @ Singleton
49
+ class FirebaseAppDistributionNotificationsManager
50
+ implements OnActivityPausedListener , OnActivityResumedListener {
33
51
34
- class FirebaseAppDistributionNotificationsManager {
35
52
private static final String TAG = "NotificationsManager" ;
36
53
37
54
private static final String PACKAGE_PREFIX = "com.google.firebase.appdistribution" ;
@@ -40,15 +57,15 @@ class FirebaseAppDistributionNotificationsManager {
40
57
static final String CHANNEL_GROUP_ID = prependPackage ("notification_channel_group_id" );
41
58
42
59
@ VisibleForTesting
43
- enum Notification {
60
+ enum NotificationType {
44
61
APP_UPDATE ("notification_channel_id" , "app_update_notification_tag" ),
45
62
FEEDBACK ("feedback_notification_channel_id" , "feedback_notification_tag" );
46
63
47
64
final String channelId ;
48
65
final String tag ;
49
66
final int id ;
50
67
51
- Notification (String channelId , String tag ) {
68
+ NotificationType (String channelId , String tag ) {
52
69
this .channelId = prependPackage (channelId );
53
70
this .tag = prependPackage (tag );
54
71
this .id = ordinal ();
@@ -58,12 +75,26 @@ enum Notification {
58
75
private final Context context ;
59
76
private final AppIconSource appIconSource ;
60
77
private final NotificationManagerCompat notificationManager ;
78
+ @ Lightweight private final ScheduledExecutorService scheduledExecutorService ;
79
+ @ UiThread private final Executor uiThreadExecutor ;
80
+
81
+ private Notification feedbackNotificationToBeShown ;
82
+ private ScheduledFuture <?> feedbackNotificationCancellationFuture ;
61
83
62
84
@ Inject
63
- FirebaseAppDistributionNotificationsManager (Context context , AppIconSource appIconSource ) {
85
+ FirebaseAppDistributionNotificationsManager (
86
+ Context context ,
87
+ AppIconSource appIconSource ,
88
+ FirebaseAppDistributionLifecycleNotifier lifecycleNotifier ,
89
+ @ Lightweight ScheduledExecutorService scheduledExecutorService ,
90
+ @ UiThread Executor uiThreadExecutor ) {
64
91
this .context = context ;
65
92
this .appIconSource = appIconSource ;
66
93
this .notificationManager = NotificationManagerCompat .from (context );
94
+ lifecycleNotifier .addOnActivityPausedListener (this );
95
+ lifecycleNotifier .addOnActivityResumedListener (this );
96
+ this .scheduledExecutorService = scheduledExecutorService ;
97
+ this .uiThreadExecutor = uiThreadExecutor ;
67
98
}
68
99
69
100
void showAppUpdateNotification (long totalBytes , long downloadedBytes , int stringResourceId ) {
@@ -72,7 +103,7 @@ void showAppUpdateNotification(long totalBytes, long downloadedBytes, int string
72
103
if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .O ) {
73
104
LogWrapper .i (TAG , "Creating app update notification channel group" );
74
105
createChannel (
75
- Notification . APP_UPDATE ,
106
+ APP_UPDATE ,
76
107
R .string .app_update_notification_channel_name ,
77
108
R .string .app_update_notification_channel_description ,
78
109
InterruptionLevel .DEFAULT );
@@ -85,7 +116,7 @@ void showAppUpdateNotification(long totalBytes, long downloadedBytes, int string
85
116
}
86
117
87
118
NotificationCompat .Builder notificationBuilder =
88
- new NotificationCompat .Builder (context , Notification . APP_UPDATE .channelId )
119
+ new NotificationCompat .Builder (context , APP_UPDATE .channelId )
89
120
.setOnlyAlertOnce (true )
90
121
.setSmallIcon (appIconSource .getNonAdaptiveIconOrDefault (context ))
91
122
.setContentTitle (context .getString (stringResourceId ))
@@ -97,8 +128,7 @@ void showAppUpdateNotification(long totalBytes, long downloadedBytes, int string
97
128
if (appLaunchIntent != null ) {
98
129
notificationBuilder .setContentIntent (appLaunchIntent );
99
130
}
100
- notificationManager .notify (
101
- Notification .APP_UPDATE .tag , Notification .APP_UPDATE .id , notificationBuilder .build ());
131
+ notificationManager .notify (APP_UPDATE .tag , APP_UPDATE .id , notificationBuilder .build ());
102
132
}
103
133
104
134
@ Nullable
@@ -128,7 +158,7 @@ public void showFeedbackNotification(
128
158
if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .O ) {
129
159
LogWrapper .i (TAG , "Creating feedback notification channel group" );
130
160
createChannel (
131
- Notification . FEEDBACK ,
161
+ FEEDBACK ,
132
162
R .string .feedback_notification_channel_name ,
133
163
R .string .feedback_notification_channel_description ,
134
164
interruptionLevel );
@@ -139,36 +169,62 @@ public void showFeedbackNotification(
139
169
return ;
140
170
}
141
171
172
+ uiThreadExecutor .execute (
173
+ () -> {
174
+ // ensure that class state is managed on same thread as lifecycle callbacks
175
+ cancelFeedbackCancellationFuture ();
176
+ feedbackNotificationToBeShown = buildFeedbackNotification (infoText , interruptionLevel );
177
+ doShowFeedbackNotification ();
178
+ });
179
+ }
180
+
181
+ // this must be run on the main (UI) thread
182
+ private void doShowFeedbackNotification () {
183
+ LogWrapper .i (TAG , "Showing feedback notification" );
184
+ notificationManager .notify (FEEDBACK .tag , FEEDBACK .id , feedbackNotificationToBeShown );
185
+ }
186
+
187
+ private Notification buildFeedbackNotification (
188
+ @ NonNull CharSequence infoText , @ NonNull InterruptionLevel interruptionLevel ) {
142
189
Intent intent = new Intent (context , TakeScreenshotAndStartFeedbackActivity .class );
143
190
intent .addFlags (Intent .FLAG_ACTIVITY_NO_HISTORY );
144
191
intent .putExtra (TakeScreenshotAndStartFeedbackActivity .INFO_TEXT_EXTRA_KEY , infoText );
145
192
ApplicationInfo applicationInfo = context .getApplicationInfo ();
146
193
PackageManager packageManager = context .getPackageManager ();
147
194
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 ());
195
+ return new NotificationCompat .Builder (context , FEEDBACK .channelId )
196
+ .setSmallIcon (R .drawable .ic_baseline_rate_review_24 )
197
+ .setContentTitle (context .getString (R .string .feedback_notification_title ))
198
+ .setContentText (context .getString (R .string .feedback_notification_text , appLabel ))
199
+ .setPriority (interruptionLevel .notificationPriority )
200
+ .setOngoing (true )
201
+ .setOnlyAlertOnce (true )
202
+ .setAutoCancel (false )
203
+ .setContentIntent (getPendingIntent (intent , /* extraFlags= */ 0 ))
204
+ .build ();
161
205
}
162
206
163
207
public void cancelFeedbackNotification () {
208
+ uiThreadExecutor .execute (
209
+ () -> {
210
+ // ensure that class state is managed on same thread as lifecycle callbacks
211
+ feedbackNotificationToBeShown = null ;
212
+ cancelFeedbackCancellationFuture ();
213
+ doCancelFeedbackNotification ();
214
+ });
215
+ }
216
+
217
+ public void doCancelFeedbackNotification () {
164
218
LogWrapper .i (TAG , "Cancelling feedback notification" );
165
- NotificationManagerCompat .from (context )
166
- .cancel (Notification .FEEDBACK .tag , Notification .FEEDBACK .id );
219
+ NotificationManagerCompat .from (context ).cancel (FEEDBACK .tag , FEEDBACK .id );
167
220
}
168
221
169
222
@ RequiresApi (Build .VERSION_CODES .O )
170
223
private void createChannel (
171
- Notification notification , int name , int description , InterruptionLevel interruptionLevel ) {
224
+ NotificationType notification ,
225
+ int name ,
226
+ int description ,
227
+ InterruptionLevel interruptionLevel ) {
172
228
notificationManager .createNotificationChannelGroup (
173
229
new NotificationChannelGroup (
174
230
CHANNEL_GROUP_ID , context .getString (R .string .notifications_group_name )));
@@ -182,6 +238,37 @@ private void createChannel(
182
238
notificationManager .createNotificationChannel (channel );
183
239
}
184
240
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
+
185
272
private static String prependPackage (String id ) {
186
273
return String .format ("%s.%s" , PACKAGE_PREFIX , id );
187
274
}
0 commit comments