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 androidx.core.app.NotificationCompat; 29 import androidx.core.app.NotificationManagerCompat; 30 import androidx.core.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 showLowPriorityNotification(Context context, AlarmInstance instance)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 showHighPriorityNotification(Context context, AlarmInstance instance)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) isGroupSummary(Notification 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) getFirstActiveNotification(Context context, String group, int canceledNotificationId, Notification postedNotification)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) getActiveGroupSummaryNotification(Context context, String group)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 updateUpcomingAlarmGroupNotification(Context context, int canceledNotificationId, Notification postedNotification)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 updateMissedAlarmGroupNotification(Context context, int canceledNotificationId, Notification postedNotification)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 showSnoozeNotification(Context context, AlarmInstance instance)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 showMissedNotification(Context context, AlarmInstance instance)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 showAlarmNotification(Service service, AlarmInstance instance)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 clearNotification(Context context, AlarmInstance instance)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 */ updateNotification(Context context, AlarmInstance instance)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 createViewAlarmIntent(Context context, AlarmInstance instance)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 */ createSortKey(AlarmInstance instance)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