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