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