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