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