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