NotificationActionUtils.java revision 3168c2a75edbf597932ab9edfd96037ceba99b35
1/* 2 * Copyright (C) 2012 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 */ 16package com.android.mail.utils; 17 18import android.app.AlarmManager; 19import android.app.Notification; 20import android.app.NotificationManager; 21import android.app.PendingIntent; 22import android.content.ContentResolver; 23import android.content.ContentValues; 24import android.content.Context; 25import android.content.Intent; 26import android.net.Uri; 27import android.os.Parcel; 28import android.os.Parcelable; 29import android.os.SystemClock; 30import android.support.v4.app.NotificationCompat; 31import android.support.v4.app.TaskStackBuilder; 32import android.support.v4.util.SparseArrayCompat; 33import android.text.TextUtils; 34import android.text.format.DateUtils; 35import android.widget.RemoteViews; 36 37import com.android.mail.NotificationActionIntentService; 38import com.android.mail.R; 39import com.android.mail.compose.ComposeActivity; 40import com.android.mail.providers.Account; 41import com.android.mail.providers.Conversation; 42import com.android.mail.providers.Folder; 43import com.android.mail.providers.Message; 44import com.android.mail.providers.UIProvider; 45import com.android.mail.providers.UIProvider.FolderType; 46import com.google.common.collect.ImmutableMap; 47 48import java.util.ArrayList; 49import java.util.HashSet; 50import java.util.List; 51import java.util.Map; 52import java.util.Set; 53 54public class NotificationActionUtils { 55 private static final long UNDO_TIMEOUT_MILLIS = 10 * DateUtils.SECOND_IN_MILLIS; 56 57 /** 58 * If an {@link NotificationAction} exists here for a given notification key, then we should 59 * display this undo notification rather than an email notification. 60 */ 61 public static final SparseArrayCompat<NotificationAction> sUndoNotifications = 62 new SparseArrayCompat<NotificationAction>(); 63 64 /** 65 * If an undo notification is displayed, its timestamp 66 * ({@link android.app.Notification.Builder#setWhen(long)}) is stored here so we can use it for 67 * the original notification if the action is undone. 68 */ 69 public static final SparseLongArray sNotificationTimestamps = new SparseLongArray(); 70 71 public static final String ACTION_RESEND_NOTIFICATIONS = 72 "com.android.mail.action.RESEND_NOTIFICATIONS"; 73 74 public enum NotificationActionType { 75 // TODO: Confirm icons 76 REPLY("reply", R.drawable.ic_reply_holo_dark, R.string.notification_action_reply), 77 REPLY_ALL("reply_all", R.drawable.ic_reply_all_holo_dark, 78 R.string.notification_action_reply_all), 79 FORWARD("forward", R.drawable.ic_forward_holo_dark, R.string.notification_action_forward), 80 ARCHIVE_REMOVE_LABEL("archive", R.drawable.ic_menu_archive_holo_light, 81 R.drawable.ic_remove_label, R.string.notification_action_archive, 82 R.string.notification_action_remove_label, new ActionToggler() { 83 @Override 84 public boolean shouldDisplayPrimary(final Folder folder, 85 final Conversation conversation, final Message message) { 86 return folder.type == FolderType.INBOX; 87 } 88 }), 89 DELETE("delete", R.drawable.ic_menu_trash_holo_light, R.string.notification_action_delete), 90 // TODO: We may remove the ability to mark unread 91 MARK_READ("mark_read", R.drawable.ic_menu_mark_read_holo_light, 92 R.drawable.ic_menu_mark_unread_holo_light, R.string.notification_action_mark_read, 93 R.string.notification_action_mark_unread, new ActionToggler() { 94 @Override 95 public boolean shouldDisplayPrimary(final Folder folder, 96 final Conversation conversation, final Message message) { 97 return message == null || !message.read; 98 } 99 }), 100 ADD_STAR("add_star", R.drawable.ic_menu_star_holo_light, 101 R.drawable.ic_menu_star_off_holo_light, R.string.notification_action_add_star, 102 R.string.notification_action_remove_star, new ActionToggler() { 103 @Override 104 public boolean shouldDisplayPrimary(final Folder folder, 105 final Conversation conversation, final Message message) { 106 return message == null || !message.starred; 107 } 108 }), 109 MARK_IMPORTANT("mark_important", R.drawable.ic_email_caret_double_important_unread, 110 R.drawable.ic_email_caret_single_important_unread, 111 R.string.notification_action_mark_important, 112 R.string.notification_action_mark_not_important, new ActionToggler() { 113 @Override 114 public boolean shouldDisplayPrimary(final Folder folder, 115 final Conversation conversation, final Message message) { 116 return conversation == null || !conversation.isImportant(); 117 } 118 }), 119 MUTE("mute", R.drawable.ic_cancel_holo_light, R.string.notification_action_delete); 120 121 private final String mPersistedValue; 122 123 private final int mActionIcon; 124 private final int mActionIcon2; 125 126 private final int mDisplayString; 127 private final int mDisplayString2; 128 129 private final ActionToggler mActionToggler; 130 131 private static final Map<String, NotificationActionType> sPersistedMapping; 132 133 private interface ActionToggler { 134 /** 135 * Determines if we should display the primary or secondary text/icon. 136 * 137 * @return <code>true</code> to display primary, <code>false</code> to display secondary 138 */ 139 boolean shouldDisplayPrimary(Folder folder, Conversation conversation, Message message); 140 } 141 142 static { 143 final NotificationActionType[] values = values(); 144 final ImmutableMap.Builder<String, NotificationActionType> mapBuilder = 145 new ImmutableMap.Builder<String, NotificationActionType>(); 146 147 for (int i = 0; i < values.length; i++) { 148 mapBuilder.put(values[i].getPersistedValue(), values[i]); 149 } 150 151 sPersistedMapping = mapBuilder.build(); 152 } 153 154 private NotificationActionType(final String persistedValue, final int actionIcon, 155 final int displayString) { 156 mPersistedValue = persistedValue; 157 mActionIcon = actionIcon; 158 mActionIcon2 = -1; 159 mDisplayString = displayString; 160 mDisplayString2 = -1; 161 mActionToggler = null; 162 } 163 164 private NotificationActionType(final String persistedValue, final int actionIcon, 165 final int actionIcon2, final int displayString, final int displayString2, 166 final ActionToggler actionToggler) { 167 mPersistedValue = persistedValue; 168 mActionIcon = actionIcon; 169 mActionIcon2 = actionIcon2; 170 mDisplayString = displayString; 171 mDisplayString2 = displayString2; 172 mActionToggler = actionToggler; 173 } 174 175 public static NotificationActionType getActionType(final String persistedValue) { 176 return sPersistedMapping.get(persistedValue); 177 } 178 179 public String getPersistedValue() { 180 return mPersistedValue; 181 } 182 183 public int getActionIconResId(final Folder folder, final Conversation conversation, 184 final Message message) { 185 if (mActionToggler == null || mActionToggler.shouldDisplayPrimary(folder, conversation, 186 message)) { 187 return mActionIcon; 188 } 189 190 return mActionIcon2; 191 } 192 193 public int getDisplayStringResId(final Folder folder, final Conversation conversation, 194 final Message message) { 195 if (mActionToggler == null || mActionToggler.shouldDisplayPrimary(folder, conversation, 196 message)) { 197 return mDisplayString; 198 } 199 200 return mDisplayString2; 201 } 202 } 203 204 /** 205 * Adds the appropriate notification actions to the specified 206 * {@link android.app.Notification.Builder} 207 * 208 * @param notificationIntent The {@link Intent} used when the notification is clicked 209 * @param when The value passed into {@link android.app.Notification.Builder#setWhen(long)}. 210 * This is used for maintaining notification ordering with the undo bar 211 * @param notificationActionsString A comma-delimited {@link String} of the actions to display 212 */ 213 public static void addNotificationActions(final Context context, 214 final Intent notificationIntent, final Notification.Builder notification, 215 final Account account, final Conversation conversation, final Message message, 216 final Folder folder, final int notificationId, final long when, 217 final String notificationActionsString) { 218 final String[] notificationActionsValueArray = 219 TextUtils.isEmpty(notificationActionsString) ? new String[0] 220 : notificationActionsString.split(","); 221 final List<NotificationActionType> notificationActions = 222 new ArrayList<NotificationActionType>(notificationActionsValueArray.length); 223 for (int i = 0; i < notificationActionsValueArray.length; i++) { 224 notificationActions.add( 225 NotificationActionType.getActionType(notificationActionsValueArray[i])); 226 } 227 228 sortNotificationActions(folder, notificationActions); 229 230 for (final NotificationActionType notificationAction : notificationActions) { 231 notification.addAction(notificationAction.getActionIconResId( 232 folder, conversation, message), context.getString(notificationAction 233 .getDisplayStringResId(folder, conversation, message)), 234 getNotificationActionPendingIntent(context, account, conversation, message, 235 folder, notificationIntent, notificationAction, notificationId, when)); 236 } 237 } 238 239 /** 240 * Sorts the notification actions into the appropriate order, based on current label 241 * 242 * @param folder The {@link Folder} being notified 243 * @param notificationActions The actions to sort 244 */ 245 private static void sortNotificationActions( 246 final Folder folder, final List<NotificationActionType> notificationActions) { 247 final Set<NotificationActionType> tempActions = 248 new HashSet<NotificationActionType>(notificationActions); 249 notificationActions.clear(); 250 251 if (folder.type == FolderType.INBOX) { 252 // Inbox 253 /* 254 * Action 1: Archive, Delete, Mute, Mark read, Add star, Mark important, Reply, Reply 255 * all, Forward 256 */ 257 /* 258 * Action 2: Reply, Reply all, Forward, Mark important, Add star, Mark read, Mute, 259 * Delete, Archive 260 */ 261 if (tempActions.contains(NotificationActionType.ARCHIVE_REMOVE_LABEL)) { 262 notificationActions.add(NotificationActionType.ARCHIVE_REMOVE_LABEL); 263 } 264 if (tempActions.contains(NotificationActionType.DELETE)) { 265 notificationActions.add(NotificationActionType.DELETE); 266 } 267 if (tempActions.contains(NotificationActionType.MUTE)) { 268 notificationActions.add(NotificationActionType.MUTE); 269 } 270 if (tempActions.contains(NotificationActionType.MARK_READ)) { 271 notificationActions.add(NotificationActionType.MARK_READ); 272 } 273 if (tempActions.contains(NotificationActionType.ADD_STAR)) { 274 notificationActions.add(NotificationActionType.ADD_STAR); 275 } 276 if (tempActions.contains(NotificationActionType.MARK_IMPORTANT)) { 277 notificationActions.add(NotificationActionType.MARK_IMPORTANT); 278 } 279 if (tempActions.contains(NotificationActionType.REPLY)) { 280 notificationActions.add(NotificationActionType.REPLY); 281 } 282 if (tempActions.contains(NotificationActionType.REPLY_ALL)) { 283 notificationActions.add(NotificationActionType.REPLY_ALL); 284 } 285 if (tempActions.contains(NotificationActionType.FORWARD)) { 286 notificationActions.add(NotificationActionType.FORWARD); 287 } 288 } else if (folder.isProviderFolder()) { 289 // Gmail system labels 290 /* 291 * Action 1: Delete, Mute, Mark read, Add star, Mark important, Reply, Reply all, 292 * Forward 293 */ 294 /* 295 * Action 2: Reply, Reply all, Forward, Mark important, Add star, Mark read, Mute, 296 * Delete 297 */ 298 if (tempActions.contains(NotificationActionType.DELETE)) { 299 notificationActions.add(NotificationActionType.DELETE); 300 } 301 if (tempActions.contains(NotificationActionType.MUTE)) { 302 notificationActions.add(NotificationActionType.MUTE); 303 } 304 if (tempActions.contains(NotificationActionType.MARK_READ)) { 305 notificationActions.add(NotificationActionType.MARK_READ); 306 } 307 if (tempActions.contains(NotificationActionType.ADD_STAR)) { 308 notificationActions.add(NotificationActionType.ADD_STAR); 309 } 310 if (tempActions.contains(NotificationActionType.MARK_IMPORTANT)) { 311 notificationActions.add(NotificationActionType.MARK_IMPORTANT); 312 } 313 if (tempActions.contains(NotificationActionType.REPLY)) { 314 notificationActions.add(NotificationActionType.REPLY); 315 } 316 if (tempActions.contains(NotificationActionType.REPLY_ALL)) { 317 notificationActions.add(NotificationActionType.REPLY_ALL); 318 } 319 if (tempActions.contains(NotificationActionType.FORWARD)) { 320 notificationActions.add(NotificationActionType.FORWARD); 321 } 322 } else { 323 // Gmail user created labels 324 /* 325 * Action 1: Remove label, Delete, Mark read, Add star, Mark important, Reply, Reply 326 * all, Forward 327 */ 328 /* 329 * Action 2: Reply, Reply all, Forward, Mark important, Add star, Mark read, Delete 330 */ 331 if (tempActions.contains(NotificationActionType.ARCHIVE_REMOVE_LABEL)) { 332 notificationActions.add(NotificationActionType.ARCHIVE_REMOVE_LABEL); 333 } 334 if (tempActions.contains(NotificationActionType.DELETE)) { 335 notificationActions.add(NotificationActionType.DELETE); 336 } 337 if (tempActions.contains(NotificationActionType.MARK_READ)) { 338 notificationActions.add(NotificationActionType.MARK_READ); 339 } 340 if (tempActions.contains(NotificationActionType.ADD_STAR)) { 341 notificationActions.add(NotificationActionType.ADD_STAR); 342 } 343 if (tempActions.contains(NotificationActionType.MARK_IMPORTANT)) { 344 notificationActions.add(NotificationActionType.MARK_IMPORTANT); 345 } 346 if (tempActions.contains(NotificationActionType.REPLY)) { 347 notificationActions.add(NotificationActionType.REPLY); 348 } 349 if (tempActions.contains(NotificationActionType.REPLY_ALL)) { 350 notificationActions.add(NotificationActionType.REPLY_ALL); 351 } 352 if (tempActions.contains(NotificationActionType.FORWARD)) { 353 notificationActions.add(NotificationActionType.FORWARD); 354 } 355 } 356 } 357 358 /** 359 * Creates a {@link PendingIntent} for the specified notification action. 360 */ 361 private static PendingIntent getNotificationActionPendingIntent(final Context context, 362 final Account account, final Conversation conversation, final Message message, 363 final Folder folder, final Intent notificationIntent, 364 final NotificationActionType action, final int notificationId, final long when) { 365 final Uri messageUri = message.uri; 366 367 final NotificationAction notificationAction = new NotificationAction(action, account, 368 conversation, message, folder, conversation.id, message.serverId, message.id, when); 369 370 switch (action) { 371 case REPLY: { 372 // Build a task stack that forces the conversation view on the stack before the 373 // reply activity. 374 final TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context); 375 376 final Intent replyIntent = createReplyIntent(context, account, messageUri, false); 377 // To make sure that the reply intents one notification don't clobber over 378 // intents for other notification, force a data uri on the intent 379 final Uri notificationUri = 380 Uri.parse("gmailfrom://gmail-ls/account/" + "reply/" + notificationId); 381 replyIntent.setData(notificationUri); 382 383 taskStackBuilder.addNextIntent(notificationIntent).addNextIntent(replyIntent); 384 385 return taskStackBuilder.getPendingIntent( 386 notificationId, PendingIntent.FLAG_UPDATE_CURRENT); 387 } case REPLY_ALL: { 388 // Build a task stack that forces the conversation view on the stack before the 389 // reply activity. 390 final TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context); 391 392 final Intent replyIntent = createReplyIntent(context, account, messageUri, true); 393 // To make sure that the reply intents one notification don't clobber over 394 // intents for other notification, force a data uri on the intent 395 final Uri notificationUri = 396 Uri.parse("gmailfrom://gmail-ls/account/" + "reply/" + notificationId); 397 replyIntent.setData(notificationUri); 398 399 taskStackBuilder.addNextIntent(notificationIntent).addNextIntent(replyIntent); 400 401 return taskStackBuilder.getPendingIntent( 402 notificationId, PendingIntent.FLAG_UPDATE_CURRENT); 403 } case FORWARD: { 404 // Build a task stack that forces the conversation view on the stack before the 405 // reply activity. 406 final TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context); 407 408 final Intent replyIntent = createForwardIntent(context, account, messageUri); 409 // To make sure that the reply intents one notification don't clobber over 410 // intents for other notification, force a data uri on the intent 411 final Uri notificationUri = 412 Uri.parse("gmailfrom://gmail-ls/account/" + "reply/" + notificationId); 413 replyIntent.setData(notificationUri); 414 415 taskStackBuilder.addNextIntent(notificationIntent).addNextIntent(replyIntent); 416 417 return taskStackBuilder.getPendingIntent( 418 notificationId, PendingIntent.FLAG_UPDATE_CURRENT); 419 } case ARCHIVE_REMOVE_LABEL: { 420 final String intentAction = 421 NotificationActionIntentService.ACTION_ARCHIVE_REMOVE_LABEL; 422 423 final Intent intent = new Intent(intentAction); 424 intent.putExtra(NotificationActionIntentService.EXTRA_NOTIFICATION_ACTION, 425 notificationAction); 426 427 return PendingIntent.getService( 428 context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT); 429 } case DELETE: { 430 final String intentAction = NotificationActionIntentService.ACTION_DELETE; 431 432 final Intent intent = new Intent(intentAction); 433 intent.putExtra(NotificationActionIntentService.EXTRA_NOTIFICATION_ACTION, 434 notificationAction); 435 436 return PendingIntent.getService( 437 context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT); 438 } case MARK_READ: { 439 // TODO: We may remove the ability to mark unread 440 final String intentAction = 441 message.read ? NotificationActionIntentService.ACTION_MARK_UNREAD 442 : NotificationActionIntentService.ACTION_MARK_READ; 443 444 final Intent intent = new Intent(intentAction); 445 intent.putExtra(NotificationActionIntentService.EXTRA_NOTIFICATION_ACTION, 446 notificationAction); 447 448 return PendingIntent.getService( 449 context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT); 450 } case ADD_STAR: { 451 final String intentAction = 452 message.starred ? NotificationActionIntentService.ACTION_UNSTAR 453 : NotificationActionIntentService.ACTION_STAR; 454 455 final Intent intent = new Intent(intentAction); 456 intent.putExtra(NotificationActionIntentService.EXTRA_NOTIFICATION_ACTION, 457 notificationAction); 458 459 return PendingIntent.getService( 460 context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT); 461 } case MARK_IMPORTANT: { 462 final String intentAction = 463 conversation.isImportant() ? 464 NotificationActionIntentService.ACTION_MARK_NOT_IMPORTANT 465 : NotificationActionIntentService.ACTION_MARK_IMPORTANT; 466 467 final Intent intent = new Intent(intentAction); 468 intent.putExtra(NotificationActionIntentService.EXTRA_NOTIFICATION_ACTION, 469 notificationAction); 470 471 return PendingIntent.getService( 472 context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT); 473 } 474 case MUTE: { 475 final String intentAction = NotificationActionIntentService.ACTION_MUTE; 476 477 final Intent intent = new Intent(intentAction); 478 intent.putExtra(NotificationActionIntentService.EXTRA_NOTIFICATION_ACTION, 479 notificationAction); 480 481 return PendingIntent.getService( 482 context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT); 483 } 484 } 485 486 throw new IllegalArgumentException("Invalid NotificationActionType"); 487 } 488 489 /** 490 * @return an intent which, if launched, will reply to the conversation 491 */ 492 public static Intent createReplyIntent(final Context context, final Account account, 493 final Uri messageUri, final boolean isReplyAll) { 494 final Intent intent = ComposeActivity.createReplyIntent(context, account, messageUri, 495 isReplyAll); 496 return intent; 497 } 498 499 /** 500 * @return an intent which, if launched, will forward the conversation 501 */ 502 public static Intent createForwardIntent( 503 final Context context, final Account account, final Uri messageUri) { 504 final Intent intent = ComposeActivity.createForwardIntent(context, account, messageUri); 505 return intent; 506 } 507 508 public static class NotificationAction implements Parcelable { 509 private final NotificationActionType mNotificationActionType; 510 private final Account mAccount; 511 private final Conversation mConversation; 512 private final Message mMessage; 513 private final Folder mFolder; 514 private final long mConversationId; 515 private final String mMessageId; 516 private final long mLocalMessageId; 517 private final long mWhen; 518 519 public NotificationAction(final NotificationActionType notificationActionType, 520 final Account account, final Conversation conversation, final Message message, 521 final Folder folder, final long conversationId, final String messageId, 522 final long localMessageId, final long when) { 523 mNotificationActionType = notificationActionType; 524 mAccount = account; 525 mConversation = conversation; 526 mMessage = message; 527 mFolder = folder; 528 mConversationId = conversationId; 529 mMessageId = messageId; 530 mLocalMessageId = localMessageId; 531 mWhen = when; 532 } 533 534 public NotificationActionType getNotificationActionType() { 535 return mNotificationActionType; 536 } 537 538 public Account getAccount() { 539 return mAccount; 540 } 541 542 public Conversation getConversation() { 543 return mConversation; 544 } 545 546 public Message getMessage() { 547 return mMessage; 548 } 549 550 public Folder getFolder() { 551 return mFolder; 552 } 553 554 public long getConversationId() { 555 return mConversationId; 556 } 557 558 public String getMessageId() { 559 return mMessageId; 560 } 561 562 public long getLocalMessageId() { 563 return mLocalMessageId; 564 } 565 566 public long getWhen() { 567 return mWhen; 568 } 569 570 public int getActionTextResId() { 571 switch (mNotificationActionType) { 572 case ARCHIVE_REMOVE_LABEL: 573 if (mFolder.type == FolderType.INBOX) { 574 return R.string.notification_action_undo_archive; 575 } else { 576 return R.string.notification_action_undo_remove_label; 577 } 578 case DELETE: 579 return R.string.notification_action_undo_delete; 580 case MUTE: 581 return R.string.notification_action_undo_mute; 582 default: 583 throw new IllegalStateException( 584 "There is no action text for this NotificationActionType."); 585 } 586 } 587 588 @Override 589 public int describeContents() { 590 return 0; 591 } 592 593 @Override 594 public void writeToParcel(final Parcel out, final int flags) { 595 out.writeInt(mNotificationActionType.ordinal()); 596 out.writeParcelable(mAccount, 0); 597 out.writeParcelable(mConversation, 0); 598 out.writeParcelable(mMessage, 0); 599 out.writeParcelable(mFolder, 0); 600 out.writeLong(mConversationId); 601 out.writeString(mMessageId); 602 out.writeLong(mLocalMessageId); 603 out.writeLong(mWhen); 604 } 605 606 public static final Parcelable.Creator<NotificationAction> CREATOR = 607 new Parcelable.Creator<NotificationAction>() { 608 @Override 609 public NotificationAction createFromParcel(final Parcel in) { 610 return new NotificationAction(in); 611 } 612 613 @Override 614 public NotificationAction[] newArray(final int size) { 615 return new NotificationAction[size]; 616 } 617 }; 618 619 private NotificationAction(final Parcel in) { 620 mNotificationActionType = NotificationActionType.values()[in.readInt()]; 621 mAccount = in.readParcelable(Account.class.getClassLoader()); 622 mConversation = in.readParcelable(Conversation.class.getClassLoader()); 623 mMessage = in.readParcelable(Message.class.getClassLoader()); 624 mFolder = in.readParcelable(Folder.class.getClassLoader()); 625 mConversationId = in.readLong(); 626 mMessageId = in.readString(); 627 mLocalMessageId = in.readLong(); 628 mWhen = in.readLong(); 629 } 630 } 631 632 public static Notification createUndoNotification(final Context context, 633 final NotificationAction notificationAction, final int notificationId) { 634 final NotificationCompat.Builder builder = new NotificationCompat.Builder(context); 635 636 builder.setSmallIcon(R.drawable.stat_notify_email); 637 builder.setWhen(notificationAction.getWhen()); 638 639 final RemoteViews undoView = 640 new RemoteViews(context.getPackageName(), R.layout.undo_notification); 641 undoView.setTextViewText( 642 R.id.description_text, context.getString(notificationAction.getActionTextResId())); 643 644 final Intent clickIntent = new Intent(NotificationActionIntentService.ACTION_UNDO); 645 clickIntent.putExtra(NotificationActionIntentService.EXTRA_NOTIFICATION_ACTION, 646 notificationAction); 647 final PendingIntent clickPendingIntent = PendingIntent.getService(context, notificationId, 648 clickIntent, PendingIntent.FLAG_CANCEL_CURRENT); 649 650 undoView.setOnClickPendingIntent(R.id.status_bar_latest_event_content, clickPendingIntent); 651 652 builder.setContent(undoView); 653 654 // When the notification is cleared, we perform the destructive action 655 final Intent deleteIntent = new Intent(NotificationActionIntentService.ACTION_DESTRUCT); 656 deleteIntent.putExtra(NotificationActionIntentService.EXTRA_NOTIFICATION_ACTION, 657 notificationAction); 658 final PendingIntent deletePendingIntent = PendingIntent.getService(context, 659 notificationId, deleteIntent, PendingIntent.FLAG_CANCEL_CURRENT); 660 builder.setDeleteIntent(deletePendingIntent); 661 662 final Notification notification = builder.build(); 663 664 return notification; 665 } 666 667 /** 668 * Registers a timeout for the undo notification such that when it expires, the undo bar will 669 * disappear, and the action will be performed. 670 */ 671 public static void registerUndoTimeout( 672 final Context context, final NotificationAction notificationAction) { 673 final AlarmManager alarmManager = 674 (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 675 676 final long triggerAtMills = SystemClock.elapsedRealtime() + UNDO_TIMEOUT_MILLIS; 677 678 final PendingIntent pendingIntent = 679 createUndoTimeoutPendingIntent(context, notificationAction); 680 681 alarmManager.set(AlarmManager.ELAPSED_REALTIME, triggerAtMills, pendingIntent); 682 } 683 684 /** 685 * Cancels the undo timeout for a notification action. This should be called if the undo 686 * notification is clicked (to prevent the action from being performed anyway) or cleared (since 687 * we have already performed the action). 688 */ 689 public static void cancelUndoTimeout( 690 final Context context, final NotificationAction notificationAction) { 691 final AlarmManager alarmManager = 692 (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 693 694 final PendingIntent pendingIntent = 695 createUndoTimeoutPendingIntent(context, notificationAction); 696 697 alarmManager.cancel(pendingIntent); 698 } 699 700 /** 701 * Creates a {@link PendingIntent} to be used for creating and canceling the undo timeout 702 * alarm. 703 */ 704 private static PendingIntent createUndoTimeoutPendingIntent( 705 final Context context, final NotificationAction notificationAction) { 706 final Intent intent = new Intent(NotificationActionIntentService.ACTION_UNDO_TIMEOUT); 707 intent.putExtra( 708 NotificationActionIntentService.EXTRA_NOTIFICATION_ACTION, notificationAction); 709 710 final int requestCode = notificationAction.getAccount().hashCode() 711 ^ notificationAction.getFolder().hashCode(); 712 final PendingIntent pendingIntent = 713 PendingIntent.getService(context, requestCode, intent, 0); 714 715 return pendingIntent; 716 } 717 718 /** 719 * Processes the specified destructive action (archive, delete, mute) on the message. 720 */ 721 public static void processDestructiveAction( 722 final Context context, final NotificationAction notificationAction) { 723 final NotificationActionType destructAction = 724 notificationAction.getNotificationActionType(); 725 final Conversation conversation = notificationAction.getConversation(); 726 final Folder folder = notificationAction.getFolder(); 727 728 final ContentResolver contentResolver = context.getContentResolver(); 729 final Uri uri = conversation.uri; 730 731 switch (destructAction) { 732 case ARCHIVE_REMOVE_LABEL: { 733 if (folder.type == FolderType.INBOX) { 734 // Inbox, so archive 735 final ContentValues values = new ContentValues(1); 736 values.put(UIProvider.ConversationOperations.OPERATION_KEY, 737 UIProvider.ConversationOperations.ARCHIVE); 738 739 contentResolver.update(uri, values, null, null); 740 } else { 741 // Not inbox, so remove label 742 final List<Folder> folders = conversation.getRawFolders(); 743 folders.remove(folder); 744 final String folderString = Folder.getSerializedFolderString(folders); 745 746 final ContentValues values = new ContentValues(1); 747 values.put(Conversation.UPDATE_FOLDER_COLUMN, folderString); 748 749 contentResolver.update(uri, values, null, null); 750 } 751 break; 752 } 753 case DELETE: { 754 contentResolver.delete(uri, null, null); 755 break; 756 } 757 case MUTE: { 758 final ContentValues values = new ContentValues(1); 759 values.put(UIProvider.ConversationOperations.OPERATION_KEY, 760 UIProvider.ConversationOperations.MUTE); 761 762 contentResolver.update(uri, values, null, null); 763 break; 764 } 765 default: 766 throw new IllegalArgumentException( 767 "The specified NotificationActionType is not a destructive action."); 768 } 769 } 770 771 /** 772 * Creates and displays an Undo notification for the specified {@link NotificationAction}. 773 */ 774 public static void createUndoNotification(final Context context, 775 final NotificationAction notificationAction) { 776 final int notificationId = getUndoNotificationId( 777 notificationAction.getAccount().name, notificationAction.getFolder()); 778 779 final Notification notification = 780 createUndoNotification(context, notificationAction, notificationId); 781 782 final NotificationManager notificationManager = 783 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 784 notificationManager.notify(notificationId, notification); 785 786 sUndoNotifications.put(notificationId, notificationAction); 787 sNotificationTimestamps.put(notificationId, notificationAction.getWhen()); 788 } 789 790 public static void cancelUndoNotification(final Context context, 791 final NotificationAction notificationAction) { 792 sUndoNotifications.delete(getUndoNotificationId( 793 notificationAction.getAccount().name, notificationAction.getFolder())); 794 resendNotifications(context); 795 } 796 797 /** 798 * If an undo notification is left alone for a long enough time, it will disappear, this method 799 * will be called, and the action will be finalized. 800 */ 801 public static void processUndoNotification(final Context context, 802 final NotificationAction notificationAction) { 803 final int notificationId = getUndoNotificationId( 804 notificationAction.getAccount().name, notificationAction.getFolder()); 805 sUndoNotifications.delete(notificationId); 806 sNotificationTimestamps.delete(notificationId); 807 processDestructiveAction(context, notificationAction); 808 resendNotifications(context); 809 } 810 811 private static int getUndoNotificationId(final String account, final Folder folder) { 812 // TODO: When notifications are fully in UnifiedEmail, remove this method and use the one 813 // in Utils 814 // 1 == Gmail.NOTIFICATION_ID 815 return 1 ^ account.hashCode() ^ folder.hashCode(); 816 } 817 818 /** 819 * Broadcasts an {@link Intent} to inform the app to resend its notifications. 820 */ 821 public static void resendNotifications(final Context context) { 822 final Intent intent = new Intent(ACTION_RESEND_NOTIFICATIONS); 823 intent.setPackage(context.getPackageName()); // Make sure we only deliver this to ourself 824 context.startService(intent); 825 } 826} 827