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