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