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 */ 16 package com.android.mail.utils; 17 18 import android.app.AlarmManager; 19 import android.app.Notification; 20 import android.app.NotificationManager; 21 import android.app.PendingIntent; 22 import android.content.ContentResolver; 23 import android.content.ContentValues; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.database.DataSetObserver; 27 import android.net.Uri; 28 import android.os.Parcel; 29 import android.os.Parcelable; 30 import android.os.SystemClock; 31 import android.support.v4.app.NotificationCompat; 32 import android.support.v4.app.TaskStackBuilder; 33 import android.widget.RemoteViews; 34 35 import com.android.mail.MailIntentService; 36 import com.android.mail.NotificationActionIntentService; 37 import com.android.mail.R; 38 import com.android.mail.compose.ComposeActivity; 39 import com.android.mail.providers.Account; 40 import com.android.mail.providers.Conversation; 41 import com.android.mail.providers.Folder; 42 import com.android.mail.providers.Message; 43 import com.android.mail.providers.UIProvider; 44 import com.android.mail.providers.UIProvider.ConversationOperations; 45 import com.google.common.collect.ImmutableMap; 46 import com.google.common.collect.Sets; 47 48 import java.util.ArrayList; 49 import java.util.Collection; 50 import java.util.List; 51 import java.util.Map; 52 import java.util.Set; 53 54 public 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().getAccountManagerAccount(), 679 notificationAction.getFolder()); 680 681 final Notification notification = 682 createUndoNotification(context, notificationAction, notificationId); 683 684 final NotificationManager notificationManager = 685 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 686 notificationManager.notify(notificationId, notification); 687 688 sUndoNotifications.put(notificationId, notificationAction); 689 sNotificationTimestamps.put(notificationId, notificationAction.getWhen()); 690 } 691 692 /** 693 * Called when an Undo notification has been tapped. 694 */ 695 public static void cancelUndoNotification(final Context context, 696 final NotificationAction notificationAction) { 697 LogUtils.i(LOG_TAG, "cancelUndoNotification for %s", 698 notificationAction.getNotificationActionType()); 699 700 final Account account = notificationAction.getAccount(); 701 final Folder folder = notificationAction.getFolder(); 702 final Conversation conversation = notificationAction.getConversation(); 703 final int notificationId = 704 NotificationUtils.getNotificationId(account.getAccountManagerAccount(), folder); 705 706 // Note: we must add the conversation before removing the undo notification 707 // Otherwise, the observer for sUndoNotifications gets called, which calls 708 // handleNotificationActions before the undone conversation has been added to the set. 709 sUndoneConversations.add(conversation); 710 removeUndoNotification(context, notificationId, false); 711 resendNotifications(context, account, folder); 712 } 713 714 /** 715 * If an undo notification is left alone for a long enough time, it will disappear, this method 716 * will be called, and the action will be finalized. 717 */ 718 public static void processUndoNotification(final Context context, 719 final NotificationAction notificationAction) { 720 LogUtils.i(LOG_TAG, "processUndoNotification, %s", 721 notificationAction.getNotificationActionType()); 722 723 final Account account = notificationAction.getAccount(); 724 final Folder folder = notificationAction.getFolder(); 725 final int notificationId = NotificationUtils.getNotificationId( 726 account.getAccountManagerAccount(), folder); 727 removeUndoNotification(context, notificationId, true); 728 sNotificationTimestamps.delete(notificationId); 729 processDestructiveAction(context, notificationAction); 730 731 resendNotifications(context, account, folder); 732 } 733 734 /** 735 * Removes the undo notification. 736 * 737 * @param removeNow <code>true</code> to remove it from the drawer right away, 738 * <code>false</code> to just remove the reference to it 739 */ 740 private static void removeUndoNotification( 741 final Context context, final int notificationId, final boolean removeNow) { 742 sUndoNotifications.delete(notificationId); 743 744 if (removeNow) { 745 final NotificationManager notificationManager = 746 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 747 notificationManager.cancel(notificationId); 748 } 749 } 750 751 /** 752 * Broadcasts an {@link Intent} to inform the app to resend its notifications. 753 */ 754 public static void resendNotifications(final Context context, final Account account, 755 final Folder folder) { 756 LogUtils.i(LOG_TAG, "resendNotifications account: %s, folder: %s", 757 LogUtils.sanitizeName(LOG_TAG, account.name), 758 LogUtils.sanitizeName(LOG_TAG, folder.name)); 759 760 final Intent intent = new Intent(MailIntentService.ACTION_RESEND_NOTIFICATIONS); 761 intent.setPackage(context.getPackageName()); // Make sure we only deliver this to ourself 762 intent.putExtra(Utils.EXTRA_ACCOUNT_URI, account.uri); 763 intent.putExtra(Utils.EXTRA_FOLDER_URI, folder.folderUri.fullUri); 764 context.startService(intent); 765 } 766 767 public static void registerUndoNotificationObserver(final DataSetObserver observer) { 768 sUndoNotifications.getDataSetObservable().registerObserver(observer); 769 } 770 771 public static void unregisterUndoNotificationObserver(final DataSetObserver observer) { 772 sUndoNotifications.getDataSetObservable().unregisterObserver(observer); 773 } 774 775 /** 776 * <p> 777 * This is a slight hack to avoid an exception in the remote AlarmManagerService process. The 778 * AlarmManager adds extra data to this Intent which causes it to inflate. Since the remote 779 * process does not know about the NotificationAction class, it throws a ClassNotFoundException. 780 * </p> 781 * <p> 782 * To avoid this, we marshall the data ourselves and then parcel a plain byte[] array. The 783 * NotificationActionIntentService class knows to build the NotificationAction object from the 784 * byte[] array. 785 * </p> 786 */ 787 private static void putNotificationActionExtra(final Intent intent, 788 final NotificationAction notificationAction) { 789 final Parcel out = Parcel.obtain(); 790 notificationAction.writeToParcel(out, 0); 791 out.setDataPosition(0); 792 intent.putExtra(NotificationActionIntentService.EXTRA_NOTIFICATION_ACTION, out.marshall()); 793 } 794 } 795