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