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