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