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 implements OnActivityPausedListener ,
50
+ 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" ;
38
55
39
56
@ VisibleForTesting
40
57
static final String CHANNEL_GROUP_ID = prependPackage ("notification_channel_group_id" );
41
58
59
+
42
60
@ VisibleForTesting
43
- enum Notification {
61
+ enum NotificationType {
44
62
APP_UPDATE ("notification_channel_id" , "app_update_notification_tag" ),
45
63
FEEDBACK ("feedback_notification_channel_id" , "feedback_notification_tag" );
46
64
47
65
final String channelId ;
48
66
final String tag ;
49
67
final int id ;
50
68
51
- Notification (String channelId , String tag ) {
69
+ NotificationType (String channelId , String tag ) {
52
70
this .channelId = prependPackage (channelId );
53
71
this .tag = prependPackage (tag );
54
72
this .id = ordinal ();
@@ -58,12 +76,28 @@ enum Notification {
58
76
private final Context context ;
59
77
private final AppIconSource appIconSource ;
60
78
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 ;
61
86
62
87
@ 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 ) {
64
94
this .context = context ;
65
95
this .appIconSource = appIconSource ;
66
96
this .notificationManager = NotificationManagerCompat .from (context );
97
+ lifecycleNotifier .addOnActivityPausedListener (this );
98
+ lifecycleNotifier .addOnActivityResumedListener (this );
99
+ this .scheduledExecutorService = scheduledExecutorService ;
100
+ this .uiThreadExecutor = uiThreadExecutor ;
67
101
}
68
102
69
103
void showAppUpdateNotification (long totalBytes , long downloadedBytes , int stringResourceId ) {
@@ -72,7 +106,7 @@ void showAppUpdateNotification(long totalBytes, long downloadedBytes, int string
72
106
if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .O ) {
73
107
LogWrapper .i (TAG , "Creating app update notification channel group" );
74
108
createChannel (
75
- Notification . APP_UPDATE ,
109
+ APP_UPDATE ,
76
110
R .string .app_update_notification_channel_name ,
77
111
R .string .app_update_notification_channel_description ,
78
112
InterruptionLevel .DEFAULT );
@@ -85,7 +119,7 @@ void showAppUpdateNotification(long totalBytes, long downloadedBytes, int string
85
119
}
86
120
87
121
NotificationCompat .Builder notificationBuilder =
88
- new NotificationCompat .Builder (context , Notification . APP_UPDATE .channelId )
122
+ new NotificationCompat .Builder (context , APP_UPDATE .channelId )
89
123
.setOnlyAlertOnce (true )
90
124
.setSmallIcon (appIconSource .getNonAdaptiveIconOrDefault (context ))
91
125
.setContentTitle (context .getString (stringResourceId ))
@@ -97,8 +131,7 @@ void showAppUpdateNotification(long totalBytes, long downloadedBytes, int string
97
131
if (appLaunchIntent != null ) {
98
132
notificationBuilder .setContentIntent (appLaunchIntent );
99
133
}
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 ());
102
135
}
103
136
104
137
@ Nullable
@@ -128,7 +161,7 @@ public void showFeedbackNotification(
128
161
if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .O ) {
129
162
LogWrapper .i (TAG , "Creating feedback notification channel group" );
130
163
createChannel (
131
- Notification . FEEDBACK ,
164
+ FEEDBACK ,
132
165
R .string .feedback_notification_channel_name ,
133
166
R .string .feedback_notification_channel_description ,
134
167
interruptionLevel );
@@ -139,36 +172,58 @@ public void showFeedbackNotification(
139
172
return ;
140
173
}
141
174
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 ) {
142
191
Intent intent = new Intent (context , TakeScreenshotAndStartFeedbackActivity .class );
143
192
intent .addFlags (Intent .FLAG_ACTIVITY_NO_HISTORY );
144
193
intent .putExtra (TakeScreenshotAndStartFeedbackActivity .INFO_TEXT_EXTRA_KEY , infoText );
145
194
ApplicationInfo applicationInfo = context .getApplicationInfo ();
146
195
PackageManager packageManager = context .getPackageManager ();
147
196
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 ();
161
207
}
162
208
163
209
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 () {
164
219
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 );
167
221
}
168
222
169
223
@ RequiresApi (Build .VERSION_CODES .O )
170
224
private void createChannel (
171
- Notification notification , int name , int description , InterruptionLevel interruptionLevel ) {
225
+ NotificationType notification , int name , int description ,
226
+ InterruptionLevel interruptionLevel ) {
172
227
notificationManager .createNotificationChannelGroup (
173
228
new NotificationChannelGroup (
174
229
CHANNEL_GROUP_ID , context .getString (R .string .notifications_group_name )));
@@ -182,6 +237,37 @@ private void createChannel(
182
237
notificationManager .createNotificationChannel (channel );
183
238
}
184
239
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
+
185
271
private static String prependPackage (String id ) {
186
272
return String .format ("%s.%s" , PACKAGE_PREFIX , id );
187
273
}
0 commit comments