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