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