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