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