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