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 ;
26
32
import androidx .annotation .Nullable ;
27
33
import androidx .annotation .RequiresApi ;
28
34
import androidx .annotation .VisibleForTesting ;
35
+ import androidx .annotation .WorkerThread ;
29
36
import androidx .core .app .NotificationCompat ;
30
37
import androidx .core .app .NotificationManagerCompat ;
38
+ import com .google .firebase .annotations .concurrent .Lightweight ;
39
+ import com .google .firebase .annotations .concurrent .UiThread ;
31
40
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 ;
32
46
import javax .inject .Inject ;
47
+ import javax .inject .Singleton ;
48
+
49
+ @ Singleton
50
+ class FirebaseAppDistributionNotificationsManager implements OnActivityPausedListener ,
51
+ OnActivityResumedListener {
33
52
34
- class FirebaseAppDistributionNotificationsManager {
35
53
private static final String TAG = "NotificationsManager" ;
36
54
37
55
private static final String PACKAGE_PREFIX = "com.google.firebase.appdistribution" ;
38
56
39
57
@ VisibleForTesting
40
58
static final String CHANNEL_GROUP_ID = prependPackage ("notification_channel_group_id" );
41
59
60
+
42
61
@ VisibleForTesting
43
- enum Notification {
62
+ enum NotificationType {
44
63
APP_UPDATE ("notification_channel_id" , "app_update_notification_tag" ),
45
64
FEEDBACK ("feedback_notification_channel_id" , "feedback_notification_tag" );
46
65
47
66
final String channelId ;
48
67
final String tag ;
49
68
final int id ;
50
69
51
- Notification (String channelId , String tag ) {
70
+ NotificationType (String channelId , String tag ) {
52
71
this .channelId = prependPackage (channelId );
53
72
this .tag = prependPackage (tag );
54
73
this .id = ordinal ();
@@ -58,12 +77,28 @@ enum Notification {
58
77
private final Context context ;
59
78
private final AppIconSource appIconSource ;
60
79
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 ;
61
87
62
88
@ 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 ) {
64
95
this .context = context ;
65
96
this .appIconSource = appIconSource ;
66
97
this .notificationManager = NotificationManagerCompat .from (context );
98
+ lifecycleNotifier .addOnActivityPausedListener (this );
99
+ lifecycleNotifier .addOnActivityResumedListener (this );
100
+ this .scheduledExecutorService = scheduledExecutorService ;
101
+ this .uiThreadExecutor = uiThreadExecutor ;
67
102
}
68
103
69
104
void showAppUpdateNotification (long totalBytes , long downloadedBytes , int stringResourceId ) {
@@ -72,7 +107,7 @@ void showAppUpdateNotification(long totalBytes, long downloadedBytes, int string
72
107
if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .O ) {
73
108
LogWrapper .i (TAG , "Creating app update notification channel group" );
74
109
createChannel (
75
- Notification . APP_UPDATE ,
110
+ APP_UPDATE ,
76
111
R .string .app_update_notification_channel_name ,
77
112
R .string .app_update_notification_channel_description ,
78
113
InterruptionLevel .DEFAULT );
@@ -85,7 +120,7 @@ void showAppUpdateNotification(long totalBytes, long downloadedBytes, int string
85
120
}
86
121
87
122
NotificationCompat .Builder notificationBuilder =
88
- new NotificationCompat .Builder (context , Notification . APP_UPDATE .channelId )
123
+ new NotificationCompat .Builder (context , APP_UPDATE .channelId )
89
124
.setOnlyAlertOnce (true )
90
125
.setSmallIcon (appIconSource .getNonAdaptiveIconOrDefault (context ))
91
126
.setContentTitle (context .getString (stringResourceId ))
@@ -97,8 +132,7 @@ void showAppUpdateNotification(long totalBytes, long downloadedBytes, int string
97
132
if (appLaunchIntent != null ) {
98
133
notificationBuilder .setContentIntent (appLaunchIntent );
99
134
}
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 ());
102
136
}
103
137
104
138
@ Nullable
@@ -128,7 +162,7 @@ public void showFeedbackNotification(
128
162
if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .O ) {
129
163
LogWrapper .i (TAG , "Creating feedback notification channel group" );
130
164
createChannel (
131
- Notification . FEEDBACK ,
165
+ FEEDBACK ,
132
166
R .string .feedback_notification_channel_name ,
133
167
R .string .feedback_notification_channel_description ,
134
168
interruptionLevel );
@@ -139,36 +173,58 @@ public void showFeedbackNotification(
139
173
return ;
140
174
}
141
175
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 ) {
142
192
Intent intent = new Intent (context , TakeScreenshotAndStartFeedbackActivity .class );
143
193
intent .addFlags (Intent .FLAG_ACTIVITY_NO_HISTORY );
144
194
intent .putExtra (TakeScreenshotAndStartFeedbackActivity .INFO_TEXT_EXTRA_KEY , infoText );
145
195
ApplicationInfo applicationInfo = context .getApplicationInfo ();
146
196
PackageManager packageManager = context .getPackageManager ();
147
197
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 ();
161
208
}
162
209
163
210
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 () {
164
220
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 );
167
222
}
168
223
169
224
@ RequiresApi (Build .VERSION_CODES .O )
170
225
private void createChannel (
171
- Notification notification , int name , int description , InterruptionLevel interruptionLevel ) {
226
+ NotificationType notification , int name , 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