• 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.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