1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.deskclock.alarms;
17 
18 import android.annotation.TargetApi;
19 import android.app.Notification;
20 import android.app.NotificationChannel;
21 import android.app.NotificationManager;
22 import android.app.PendingIntent;
23 import android.app.Service;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.res.Resources;
27 import android.os.Build;
28 import android.service.notification.StatusBarNotification;
29 import androidx.core.app.NotificationCompat;
30 import androidx.core.app.NotificationManagerCompat;
31 import androidx.core.content.ContextCompat;
32 
33 import com.android.deskclock.AlarmClockFragment;
34 import com.android.deskclock.AlarmUtils;
35 import com.android.deskclock.DeskClock;
36 import com.android.deskclock.LogUtils;
37 import com.android.deskclock.R;
38 import com.android.deskclock.Utils;
39 import com.android.deskclock.provider.Alarm;
40 import com.android.deskclock.provider.AlarmInstance;
41 
42 import java.text.DateFormat;
43 import java.text.SimpleDateFormat;
44 import java.util.Locale;
45 import java.util.Objects;
46 
47 final class AlarmNotifications {
48     static final String EXTRA_NOTIFICATION_ID = "extra_notification_id";
49 
50     /**
51      * Notification channel containing all low priority notifications.
52      */
53     private static final String ALARM_LOW_PRIORITY_NOTIFICATION_CHANNEL_ID =
54             "alarmLowPriorityNotification";
55 
56     /**
57      * Notification channel containing all high priority notifications.
58      */
59     private static final String ALARM_HIGH_PRIORITY_NOTIFICATION_CHANNEL_ID =
60             "alarmHighPriorityNotification";
61 
62     /**
63      * Notification channel containing all snooze notifications.
64      */
65     private static final String ALARM_SNOOZE_NOTIFICATION_CHANNEL_ID =
66             "alarmSnoozeNotification";
67 
68     /**
69      * Notification channel containing all missed notifications.
70      */
71     private static final String ALARM_MISSED_NOTIFICATION_CHANNEL_ID =
72             "alarmMissedNotification";
73 
74     /**
75      * Notification channel containing all alarm notifications.
76      */
77     private static final String ALARM_NOTIFICATION_CHANNEL_ID = "alarmNotification";
78 
79     /**
80      * Formats times such that chronological order and lexicographical order agree.
81      */
82     private static final DateFormat SORT_KEY_FORMAT =
83             new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US);
84 
85     /**
86      * This value is coordinated with group ids from
87      * {@link com.android.deskclock.data.NotificationModel}
88      */
89     private static final String UPCOMING_GROUP_KEY = "1";
90 
91     /**
92      * This value is coordinated with group ids from
93      * {@link com.android.deskclock.data.NotificationModel}
94      */
95     private static final String MISSED_GROUP_KEY = "4";
96 
97     /**
98      * This value is coordinated with notification ids from
99      * {@link com.android.deskclock.data.NotificationModel}
100      */
101     private static final int ALARM_GROUP_NOTIFICATION_ID = Integer.MAX_VALUE - 4;
102 
103     /**
104      * This value is coordinated with notification ids from
105      * {@link com.android.deskclock.data.NotificationModel}
106      */
107     private static final int ALARM_GROUP_MISSED_NOTIFICATION_ID = Integer.MAX_VALUE - 5;
108 
109     /**
110      * This value is coordinated with notification ids from
111      * {@link com.android.deskclock.data.NotificationModel}
112      */
113     private static final int ALARM_FIRING_NOTIFICATION_ID = Integer.MAX_VALUE - 7;
114 
showLowPriorityNotification(Context context, AlarmInstance instance)115     static synchronized void showLowPriorityNotification(Context context,
116             AlarmInstance instance) {
117         LogUtils.v("Displaying low priority notification for alarm instance: " + instance.mId);
118 
119         NotificationCompat.Builder builder = new NotificationCompat.Builder(
120                  context, ALARM_LOW_PRIORITY_NOTIFICATION_CHANNEL_ID)
121                          .setShowWhen(false)
122                         .setContentTitle(context.getString(
123                                 R.string.alarm_alert_predismiss_title))
124                         .setContentText(AlarmUtils.getAlarmText(
125                                 context, instance, true /* includeLabel */))
126                         .setColor(ContextCompat.getColor(context, R.color.default_background))
127                         .setSmallIcon(R.drawable.stat_notify_alarm)
128                         .setAutoCancel(false)
129                         .setSortKey(createSortKey(instance))
130                         .setPriority(NotificationCompat.PRIORITY_DEFAULT)
131                         .setCategory(NotificationCompat.CATEGORY_ALARM)
132                         .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
133                         .setLocalOnly(true);
134 
135         if (Utils.isNOrLater()) {
136             builder.setGroup(UPCOMING_GROUP_KEY);
137         }
138 
139         // Setup up hide notification
140         Intent hideIntent = AlarmStateManager.createStateChangeIntent(context,
141                 AlarmStateManager.ALARM_DELETE_TAG, instance,
142                 AlarmInstance.HIDE_NOTIFICATION_STATE);
143         final int id = instance.hashCode();
144         builder.setDeleteIntent(PendingIntent.getService(context, id,
145                 hideIntent, PendingIntent.FLAG_UPDATE_CURRENT));
146 
147         // Setup up dismiss action
148         Intent dismissIntent = AlarmStateManager.createStateChangeIntent(context,
149                 AlarmStateManager.ALARM_DISMISS_TAG, instance, AlarmInstance.PREDISMISSED_STATE);
150         builder.addAction(R.drawable.ic_alarm_off_24dp,
151                 context.getString(R.string.alarm_alert_dismiss_text),
152                 PendingIntent.getService(context, id,
153                         dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT));
154 
155         // Setup content action if instance is owned by alarm
156         Intent viewAlarmIntent = createViewAlarmIntent(context, instance);
157         builder.setContentIntent(PendingIntent.getActivity(context, id,
158                 viewAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT));
159 
160         NotificationManagerCompat nm = NotificationManagerCompat.from(context);
161         if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
162             NotificationChannel channel = new NotificationChannel(
163                     ALARM_LOW_PRIORITY_NOTIFICATION_CHANNEL_ID,
164                     context.getString(R.string.default_label),
165                     NotificationManagerCompat.IMPORTANCE_DEFAULT);
166             nm.createNotificationChannel(channel);
167         }
168         final Notification notification = builder.build();
169         nm.notify(id, notification);
170         updateUpcomingAlarmGroupNotification(context, -1, notification);
171     }
172 
showHighPriorityNotification(Context context, AlarmInstance instance)173     static synchronized void showHighPriorityNotification(Context context,
174             AlarmInstance instance) {
175         LogUtils.v("Displaying high priority notification for alarm instance: " + instance.mId);
176 
177         NotificationCompat.Builder builder = new NotificationCompat.Builder(
178                 context, ALARM_HIGH_PRIORITY_NOTIFICATION_CHANNEL_ID)
179                         .setShowWhen(false)
180                         .setContentTitle(context.getString(
181                                 R.string.alarm_alert_predismiss_title))
182                         .setContentText(AlarmUtils.getAlarmText(
183                                 context, instance, true /* includeLabel */))
184                         .setColor(ContextCompat.getColor(context, R.color.default_background))
185                         .setSmallIcon(R.drawable.stat_notify_alarm)
186                         .setAutoCancel(false)
187                         .setSortKey(createSortKey(instance))
188                         .setPriority(NotificationCompat.PRIORITY_HIGH)
189                         .setCategory(NotificationCompat.CATEGORY_ALARM)
190                         .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
191                         .setLocalOnly(true);
192 
193         if (Utils.isNOrLater()) {
194             builder.setGroup(UPCOMING_GROUP_KEY);
195         }
196 
197         // Setup up dismiss action
198         Intent dismissIntent = AlarmStateManager.createStateChangeIntent(context,
199                 AlarmStateManager.ALARM_DISMISS_TAG, instance, AlarmInstance.PREDISMISSED_STATE);
200         final int id = instance.hashCode();
201         builder.addAction(R.drawable.ic_alarm_off_24dp,
202                 context.getString(R.string.alarm_alert_dismiss_text),
203                 PendingIntent.getService(context, id,
204                         dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT));
205 
206         // Setup content action if instance is owned by alarm
207         Intent viewAlarmIntent = createViewAlarmIntent(context, instance);
208         builder.setContentIntent(PendingIntent.getActivity(context, id,
209                 viewAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT));
210 
211         NotificationManagerCompat nm = NotificationManagerCompat.from(context);
212         if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
213             NotificationChannel channel = new NotificationChannel(
214                     ALARM_HIGH_PRIORITY_NOTIFICATION_CHANNEL_ID,
215                     context.getString(R.string.default_label),
216                     NotificationManagerCompat.IMPORTANCE_DEFAULT);
217             nm.createNotificationChannel(channel);
218         }
219         final Notification notification = builder.build();
220         nm.notify(id, notification);
221         updateUpcomingAlarmGroupNotification(context, -1, notification);
222     }
223 
224     @TargetApi(Build.VERSION_CODES.N)
isGroupSummary(Notification n)225     private static boolean isGroupSummary(Notification n) {
226         return (n.flags & Notification.FLAG_GROUP_SUMMARY) == Notification.FLAG_GROUP_SUMMARY;
227     }
228 
229     /**
230      * Method which returns the first active notification for a given group. If a notification was
231      * just posted, provide it to make sure it is included as a potential result. If a notification
232      * was just canceled, provide the id so that it is not included as a potential result. These
233      * extra parameters are needed due to a race condition which exists in
234      * {@link NotificationManager#getActiveNotifications()}.
235      *
236      * @param context Context from which to grab the NotificationManager
237      * @param group The group key to query for notifications
238      * @param canceledNotificationId The id of the just-canceled notification (-1 if none)
239      * @param postedNotification The notification that was just posted
240      * @return The first active notification for the group
241      */
242     @TargetApi(Build.VERSION_CODES.N)
getFirstActiveNotification(Context context, String group, int canceledNotificationId, Notification postedNotification)243     private static Notification getFirstActiveNotification(Context context, String group,
244             int canceledNotificationId, Notification postedNotification) {
245         final NotificationManager nm =
246                 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
247         final StatusBarNotification[] notifications = nm.getActiveNotifications();
248         Notification firstActiveNotification = postedNotification;
249         for (StatusBarNotification statusBarNotification : notifications) {
250             final Notification n = statusBarNotification.getNotification();
251             if (!isGroupSummary(n)
252                     && group.equals(n.getGroup())
253                     && statusBarNotification.getId() != canceledNotificationId) {
254                 if (firstActiveNotification == null
255                         || n.getSortKey().compareTo(firstActiveNotification.getSortKey()) < 0) {
256                     firstActiveNotification = n;
257                 }
258             }
259         }
260         return firstActiveNotification;
261     }
262 
263     @TargetApi(Build.VERSION_CODES.N)
getActiveGroupSummaryNotification(Context context, String group)264     private static Notification getActiveGroupSummaryNotification(Context context, String group) {
265         final NotificationManager nm =
266                 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
267         final StatusBarNotification[] notifications = nm.getActiveNotifications();
268         for (StatusBarNotification statusBarNotification : notifications) {
269             final Notification n = statusBarNotification.getNotification();
270             if (isGroupSummary(n) && group.equals(n.getGroup())) {
271                 return n;
272             }
273         }
274         return null;
275     }
276 
updateUpcomingAlarmGroupNotification(Context context, int canceledNotificationId, Notification postedNotification)277     private static void updateUpcomingAlarmGroupNotification(Context context,
278             int canceledNotificationId, Notification postedNotification) {
279         if (!Utils.isNOrLater()) {
280             return;
281         }
282 
283         final NotificationManagerCompat nm = NotificationManagerCompat.from(context);
284         if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
285             NotificationChannel channel = new NotificationChannel(
286                     ALARM_NOTIFICATION_CHANNEL_ID,
287                     context.getString(R.string.default_label),
288                     NotificationManagerCompat.IMPORTANCE_DEFAULT);
289             nm.createNotificationChannel(channel);
290         }
291 
292         final Notification firstUpcoming = getFirstActiveNotification(context, UPCOMING_GROUP_KEY,
293                 canceledNotificationId, postedNotification);
294         if (firstUpcoming == null) {
295             nm.cancel(ALARM_GROUP_NOTIFICATION_ID);
296             return;
297         }
298 
299         Notification summary = getActiveGroupSummaryNotification(context, UPCOMING_GROUP_KEY);
300         if (summary == null
301                 || !Objects.equals(summary.contentIntent, firstUpcoming.contentIntent)) {
302             summary = new NotificationCompat.Builder(context, ALARM_NOTIFICATION_CHANNEL_ID)
303                     .setShowWhen(false)
304                     .setContentIntent(firstUpcoming.contentIntent)
305                     .setColor(ContextCompat.getColor(context, R.color.default_background))
306                     .setSmallIcon(R.drawable.stat_notify_alarm)
307                     .setGroup(UPCOMING_GROUP_KEY)
308                     .setGroupSummary(true)
309                     .setPriority(NotificationCompat.PRIORITY_HIGH)
310                     .setCategory(NotificationCompat.CATEGORY_ALARM)
311                     .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
312                     .setLocalOnly(true)
313                     .build();
314             nm.notify(ALARM_GROUP_NOTIFICATION_ID, summary);
315         }
316     }
317 
updateMissedAlarmGroupNotification(Context context, int canceledNotificationId, Notification postedNotification)318     private static void updateMissedAlarmGroupNotification(Context context,
319             int canceledNotificationId, Notification postedNotification) {
320         if (!Utils.isNOrLater()) {
321             return;
322         }
323 
324         final NotificationManagerCompat nm = NotificationManagerCompat.from(context);
325         if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
326             NotificationChannel channel = new NotificationChannel(
327                     ALARM_NOTIFICATION_CHANNEL_ID,
328                     context.getString(R.string.default_label),
329                     NotificationManagerCompat.IMPORTANCE_DEFAULT);
330             nm.createNotificationChannel(channel);
331         }
332 
333         final Notification firstMissed = getFirstActiveNotification(context, MISSED_GROUP_KEY,
334                 canceledNotificationId, postedNotification);
335         if (firstMissed == null) {
336             nm.cancel(ALARM_GROUP_MISSED_NOTIFICATION_ID);
337             return;
338         }
339 
340         Notification summary = getActiveGroupSummaryNotification(context, MISSED_GROUP_KEY);
341         if (summary == null
342                 || !Objects.equals(summary.contentIntent, firstMissed.contentIntent)) {
343             if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
344                 NotificationChannel channel = new NotificationChannel(
345                         ALARM_MISSED_NOTIFICATION_CHANNEL_ID,
346                         context.getString(R.string.default_label),
347                         NotificationManagerCompat.IMPORTANCE_DEFAULT);
348                 nm.createNotificationChannel(channel);
349             }
350             summary = new NotificationCompat.Builder(context, ALARM_NOTIFICATION_CHANNEL_ID)
351                     .setShowWhen(false)
352                     .setContentIntent(firstMissed.contentIntent)
353                     .setColor(ContextCompat.getColor(context, R.color.default_background))
354                     .setSmallIcon(R.drawable.stat_notify_alarm)
355                     .setGroup(MISSED_GROUP_KEY)
356                     .setGroupSummary(true)
357                     .setPriority(NotificationCompat.PRIORITY_HIGH)
358                     .setCategory(NotificationCompat.CATEGORY_ALARM)
359                     .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
360                     .setLocalOnly(true)
361                     .build();
362             nm.notify(ALARM_GROUP_MISSED_NOTIFICATION_ID, summary);
363         }
364     }
365 
showSnoozeNotification(Context context, AlarmInstance instance)366     static synchronized void showSnoozeNotification(Context context,
367             AlarmInstance instance) {
368         LogUtils.v("Displaying snoozed notification for alarm instance: " + instance.mId);
369 
370         NotificationCompat.Builder builder = new NotificationCompat.Builder(
371                 context, ALARM_SNOOZE_NOTIFICATION_CHANNEL_ID)
372                         .setShowWhen(false)
373                         .setContentTitle(instance.getLabelOrDefault(context))
374                         .setContentText(context.getString(R.string.alarm_alert_snooze_until,
375                                 AlarmUtils.getFormattedTime(context, instance.getAlarmTime())))
376                         .setColor(ContextCompat.getColor(context, R.color.default_background))
377                         .setSmallIcon(R.drawable.stat_notify_alarm)
378                         .setAutoCancel(false)
379                         .setSortKey(createSortKey(instance))
380                         .setPriority(NotificationCompat.PRIORITY_MAX)
381                         .setCategory(NotificationCompat.CATEGORY_ALARM)
382                         .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
383                         .setLocalOnly(true);
384 
385         if (Utils.isNOrLater()) {
386             builder.setGroup(UPCOMING_GROUP_KEY);
387         }
388 
389         // Setup up dismiss action
390         Intent dismissIntent = AlarmStateManager.createStateChangeIntent(context,
391                 AlarmStateManager.ALARM_DISMISS_TAG, instance, AlarmInstance.DISMISSED_STATE);
392         final int id = instance.hashCode();
393         builder.addAction(R.drawable.ic_alarm_off_24dp,
394                 context.getString(R.string.alarm_alert_dismiss_text),
395                 PendingIntent.getService(context, id,
396                         dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT));
397 
398         // Setup content action if instance is owned by alarm
399         Intent viewAlarmIntent = createViewAlarmIntent(context, instance);
400         builder.setContentIntent(PendingIntent.getActivity(context, id,
401                 viewAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT));
402 
403         NotificationManagerCompat nm = NotificationManagerCompat.from(context);
404         if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
405             NotificationChannel channel = new NotificationChannel(
406                     ALARM_SNOOZE_NOTIFICATION_CHANNEL_ID,
407                     context.getString(R.string.default_label),
408                     NotificationManagerCompat.IMPORTANCE_DEFAULT);
409             nm.createNotificationChannel(channel);
410         }
411         final Notification notification = builder.build();
412         nm.notify(id, notification);
413         updateUpcomingAlarmGroupNotification(context, -1, notification);
414     }
415 
showMissedNotification(Context context, AlarmInstance instance)416     static synchronized void showMissedNotification(Context context,
417             AlarmInstance instance) {
418         LogUtils.v("Displaying missed notification for alarm instance: " + instance.mId);
419 
420         String label = instance.mLabel;
421         String alarmTime = AlarmUtils.getFormattedTime(context, instance.getAlarmTime());
422         NotificationCompat.Builder builder = new NotificationCompat.Builder(
423                 context, ALARM_MISSED_NOTIFICATION_CHANNEL_ID)
424                         .setShowWhen(false)
425                         .setContentTitle(context.getString(R.string.alarm_missed_title))
426                         .setContentText(instance.mLabel.isEmpty() ? alarmTime :
427                                 context.getString(R.string.alarm_missed_text, alarmTime, label))
428                         .setColor(ContextCompat.getColor(context, R.color.default_background))
429                         .setSortKey(createSortKey(instance))
430                         .setSmallIcon(R.drawable.stat_notify_alarm)
431                         .setPriority(NotificationCompat.PRIORITY_HIGH)
432                         .setCategory(NotificationCompat.CATEGORY_ALARM)
433                         .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
434                         .setLocalOnly(true);
435 
436         if (Utils.isNOrLater()) {
437             builder.setGroup(MISSED_GROUP_KEY);
438         }
439 
440         final int id = instance.hashCode();
441 
442         // Setup dismiss intent
443         Intent dismissIntent = AlarmStateManager.createStateChangeIntent(context,
444                 AlarmStateManager.ALARM_DISMISS_TAG, instance, AlarmInstance.DISMISSED_STATE);
445         builder.setDeleteIntent(PendingIntent.getService(context, id,
446                 dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT));
447 
448         // Setup content intent
449         Intent showAndDismiss = AlarmInstance.createIntent(context, AlarmStateManager.class,
450                 instance.mId);
451         showAndDismiss.putExtra(EXTRA_NOTIFICATION_ID, id);
452         showAndDismiss.setAction(AlarmStateManager.SHOW_AND_DISMISS_ALARM_ACTION);
453         builder.setContentIntent(PendingIntent.getBroadcast(context, id,
454                 showAndDismiss, PendingIntent.FLAG_UPDATE_CURRENT));
455 
456         NotificationManagerCompat nm = NotificationManagerCompat.from(context);
457         if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
458             NotificationChannel channel = new NotificationChannel(
459                     ALARM_MISSED_NOTIFICATION_CHANNEL_ID,
460                     context.getString(R.string.default_label),
461                     NotificationManagerCompat.IMPORTANCE_DEFAULT);
462             nm.createNotificationChannel(channel);
463         }
464         final Notification notification = builder.build();
465         nm.notify(id, notification);
466         updateMissedAlarmGroupNotification(context, -1, notification);
467     }
468 
showAlarmNotification(Service service, AlarmInstance instance)469     static synchronized void showAlarmNotification(Service service, AlarmInstance instance) {
470         LogUtils.v("Displaying alarm notification for alarm instance: " + instance.mId);
471 
472         Resources resources = service.getResources();
473         NotificationCompat.Builder notification = new NotificationCompat.Builder(
474                 service, ALARM_NOTIFICATION_CHANNEL_ID)
475                         .setContentTitle(instance.getLabelOrDefault(service))
476                         .setContentText(AlarmUtils.getFormattedTime(
477                                 service, instance.getAlarmTime()))
478                         .setColor(ContextCompat.getColor(service, R.color.default_background))
479                         .setSmallIcon(R.drawable.stat_notify_alarm)
480                         .setOngoing(true)
481                         .setAutoCancel(false)
482                         .setDefaults(NotificationCompat.DEFAULT_LIGHTS)
483                         .setWhen(0)
484                         .setCategory(NotificationCompat.CATEGORY_ALARM)
485                         .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
486                         .setLocalOnly(true);
487 
488         // Setup Snooze Action
489         Intent snoozeIntent = AlarmStateManager.createStateChangeIntent(service,
490                 AlarmStateManager.ALARM_SNOOZE_TAG, instance, AlarmInstance.SNOOZE_STATE);
491         snoozeIntent.putExtra(AlarmStateManager.FROM_NOTIFICATION_EXTRA, true);
492         PendingIntent snoozePendingIntent = PendingIntent.getService(service,
493                 ALARM_FIRING_NOTIFICATION_ID, snoozeIntent, PendingIntent.FLAG_UPDATE_CURRENT);
494         notification.addAction(R.drawable.ic_snooze_24dp,
495                 resources.getString(R.string.alarm_alert_snooze_text), snoozePendingIntent);
496 
497         // Setup Dismiss Action
498         Intent dismissIntent = AlarmStateManager.createStateChangeIntent(service,
499                 AlarmStateManager.ALARM_DISMISS_TAG, instance, AlarmInstance.DISMISSED_STATE);
500         dismissIntent.putExtra(AlarmStateManager.FROM_NOTIFICATION_EXTRA, true);
501         PendingIntent dismissPendingIntent = PendingIntent.getService(service,
502                 ALARM_FIRING_NOTIFICATION_ID, dismissIntent, PendingIntent.FLAG_UPDATE_CURRENT);
503         notification.addAction(R.drawable.ic_alarm_off_24dp,
504                 resources.getString(R.string.alarm_alert_dismiss_text),
505                 dismissPendingIntent);
506 
507         // Setup Content Action
508         Intent contentIntent = AlarmInstance.createIntent(service, AlarmActivity.class,
509                 instance.mId);
510         notification.setContentIntent(PendingIntent.getActivity(service,
511                 ALARM_FIRING_NOTIFICATION_ID, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT));
512 
513         // Setup fullscreen intent
514         Intent fullScreenIntent = AlarmInstance.createIntent(service, AlarmActivity.class,
515                 instance.mId);
516         // set action, so we can be different then content pending intent
517         fullScreenIntent.setAction("fullscreen_activity");
518         fullScreenIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
519                 Intent.FLAG_ACTIVITY_NO_USER_ACTION);
520         notification.setFullScreenIntent(PendingIntent.getActivity(service,
521                 ALARM_FIRING_NOTIFICATION_ID, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT),
522                 true);
523         notification.setPriority(NotificationCompat.PRIORITY_MAX);
524 
525         clearNotification(service, instance);
526         service.startForeground(ALARM_FIRING_NOTIFICATION_ID, notification.build());
527     }
528 
clearNotification(Context context, AlarmInstance instance)529     static synchronized void clearNotification(Context context, AlarmInstance instance) {
530         LogUtils.v("Clearing notifications for alarm instance: " + instance.mId);
531         NotificationManagerCompat nm = NotificationManagerCompat.from(context);
532         final int id = instance.hashCode();
533         nm.cancel(id);
534         updateUpcomingAlarmGroupNotification(context, id, null);
535         updateMissedAlarmGroupNotification(context, id, null);
536     }
537 
538     /**
539      * Updates the notification for an existing alarm. Use if the label has changed.
540      */
updateNotification(Context context, AlarmInstance instance)541     static void updateNotification(Context context, AlarmInstance instance) {
542         switch (instance.mAlarmState) {
543             case AlarmInstance.LOW_NOTIFICATION_STATE:
544                 showLowPriorityNotification(context, instance);
545                 break;
546             case AlarmInstance.HIGH_NOTIFICATION_STATE:
547                 showHighPriorityNotification(context, instance);
548                 break;
549             case AlarmInstance.SNOOZE_STATE:
550                 showSnoozeNotification(context, instance);
551                 break;
552             case AlarmInstance.MISSED_STATE:
553                 showMissedNotification(context, instance);
554                 break;
555             default:
556                 LogUtils.d("No notification to update");
557         }
558     }
559 
createViewAlarmIntent(Context context, AlarmInstance instance)560     static Intent createViewAlarmIntent(Context context, AlarmInstance instance) {
561         final long alarmId = instance.mAlarmId == null ? Alarm.INVALID_ID : instance.mAlarmId;
562         return Alarm.createIntent(context, DeskClock.class, alarmId)
563                 .putExtra(AlarmClockFragment.SCROLL_TO_ALARM_INTENT_EXTRA, alarmId)
564                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
565     }
566 
567     /**
568      * Alarm notifications are sorted chronologically. Missed alarms are sorted chronologically
569      * <strong>after</strong> all upcoming/snoozed alarms by including the "MISSED" prefix on the
570      * sort key.
571      *
572      * @param instance the alarm instance for which the notification is generated
573      * @return the sort key that specifies the order of this alarm notification
574      */
createSortKey(AlarmInstance instance)575     private static String createSortKey(AlarmInstance instance) {
576         final String timeKey = SORT_KEY_FORMAT.format(instance.getAlarmTime().getTime());
577         final boolean missedAlarm = instance.mAlarmState == AlarmInstance.MISSED_STATE;
578         return missedAlarm ? ("MISSED " + timeKey) : timeKey;
579     }
580 }
581