Home | History | Annotate | Download | only in utils
      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  */
     16 package com.android.mail.utils;
     17 
     18 import android.app.AlarmManager;
     19 import android.app.Notification;
     20 import android.app.NotificationManager;
     21 import android.app.PendingIntent;
     22 import android.content.ContentResolver;
     23 import android.content.ContentValues;
     24 import android.content.Context;
     25 import android.content.Intent;
     26 import android.database.DataSetObserver;
     27 import android.net.Uri;
     28 import android.os.Parcel;
     29 import android.os.Parcelable;
     30 import android.os.SystemClock;
     31 import android.support.v4.app.NotificationCompat;
     32 import android.support.v4.app.TaskStackBuilder;
     33 import android.widget.RemoteViews;
     34 
     35 import com.android.mail.MailIntentService;
     36 import com.android.mail.NotificationActionIntentService;
     37 import com.android.mail.R;
     38 import com.android.mail.compose.ComposeActivity;
     39 import com.android.mail.providers.Account;
     40 import com.android.mail.providers.Conversation;
     41 import com.android.mail.providers.Folder;
     42 import com.android.mail.providers.Message;
     43 import com.android.mail.providers.UIProvider;
     44 import com.android.mail.providers.UIProvider.ConversationOperations;
     45 import com.google.common.collect.ImmutableMap;
     46 import com.google.common.collect.Sets;
     47 
     48 import java.util.ArrayList;
     49 import java.util.Collection;
     50 import java.util.List;
     51 import java.util.Map;
     52 import java.util.Set;
     53 
     54 public 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().getAccountManagerAccount(),
    679                 notificationAction.getFolder());
    680 
    681         final Notification notification =
    682                 createUndoNotification(context, notificationAction, notificationId);
    683 
    684         final NotificationManager notificationManager =
    685                 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    686         notificationManager.notify(notificationId, notification);
    687 
    688         sUndoNotifications.put(notificationId, notificationAction);
    689         sNotificationTimestamps.put(notificationId, notificationAction.getWhen());
    690     }
    691 
    692     /**
    693      * Called when an Undo notification has been tapped.
    694      */
    695     public static void cancelUndoNotification(final Context context,
    696             final NotificationAction notificationAction) {
    697         LogUtils.i(LOG_TAG, "cancelUndoNotification for %s",
    698                 notificationAction.getNotificationActionType());
    699 
    700         final Account account = notificationAction.getAccount();
    701         final Folder folder = notificationAction.getFolder();
    702         final Conversation conversation = notificationAction.getConversation();
    703         final int notificationId =
    704                 NotificationUtils.getNotificationId(account.getAccountManagerAccount(), folder);
    705 
    706         // Note: we must add the conversation before removing the undo notification
    707         // Otherwise, the observer for sUndoNotifications gets called, which calls
    708         // handleNotificationActions before the undone conversation has been added to the set.
    709         sUndoneConversations.add(conversation);
    710         removeUndoNotification(context, notificationId, false);
    711         resendNotifications(context, account, folder);
    712     }
    713 
    714     /**
    715      * If an undo notification is left alone for a long enough time, it will disappear, this method
    716      * will be called, and the action will be finalized.
    717      */
    718     public static void processUndoNotification(final Context context,
    719             final NotificationAction notificationAction) {
    720         LogUtils.i(LOG_TAG, "processUndoNotification, %s",
    721                 notificationAction.getNotificationActionType());
    722 
    723         final Account account = notificationAction.getAccount();
    724         final Folder folder = notificationAction.getFolder();
    725         final int notificationId = NotificationUtils.getNotificationId(
    726                 account.getAccountManagerAccount(), folder);
    727         removeUndoNotification(context, notificationId, true);
    728         sNotificationTimestamps.delete(notificationId);
    729         processDestructiveAction(context, notificationAction);
    730 
    731         resendNotifications(context, account, folder);
    732     }
    733 
    734     /**
    735      * Removes the undo notification.
    736      *
    737      * @param removeNow <code>true</code> to remove it from the drawer right away,
    738      *        <code>false</code> to just remove the reference to it
    739      */
    740     private static void removeUndoNotification(
    741             final Context context, final int notificationId, final boolean removeNow) {
    742         sUndoNotifications.delete(notificationId);
    743 
    744         if (removeNow) {
    745             final NotificationManager notificationManager =
    746                     (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    747             notificationManager.cancel(notificationId);
    748         }
    749     }
    750 
    751     /**
    752      * Broadcasts an {@link Intent} to inform the app to resend its notifications.
    753      */
    754     public static void resendNotifications(final Context context, final Account account,
    755             final Folder folder) {
    756         LogUtils.i(LOG_TAG, "resendNotifications account: %s, folder: %s",
    757                 LogUtils.sanitizeName(LOG_TAG, account.name),
    758                 LogUtils.sanitizeName(LOG_TAG, folder.name));
    759 
    760         final Intent intent = new Intent(MailIntentService.ACTION_RESEND_NOTIFICATIONS);
    761         intent.setPackage(context.getPackageName()); // Make sure we only deliver this to ourself
    762         intent.putExtra(Utils.EXTRA_ACCOUNT_URI, account.uri);
    763         intent.putExtra(Utils.EXTRA_FOLDER_URI, folder.folderUri.fullUri);
    764         context.startService(intent);
    765     }
    766 
    767     public static void registerUndoNotificationObserver(final DataSetObserver observer) {
    768         sUndoNotifications.getDataSetObservable().registerObserver(observer);
    769     }
    770 
    771     public static void unregisterUndoNotificationObserver(final DataSetObserver observer) {
    772         sUndoNotifications.getDataSetObservable().unregisterObserver(observer);
    773     }
    774 
    775     /**
    776      * <p>
    777      * This is a slight hack to avoid an exception in the remote AlarmManagerService process. The
    778      * AlarmManager adds extra data to this Intent which causes it to inflate. Since the remote
    779      * process does not know about the NotificationAction class, it throws a ClassNotFoundException.
    780      * </p>
    781      * <p>
    782      * To avoid this, we marshall the data ourselves and then parcel a plain byte[] array. The
    783      * NotificationActionIntentService class knows to build the NotificationAction object from the
    784      * byte[] array.
    785      * </p>
    786      */
    787     private static void putNotificationActionExtra(final Intent intent,
    788             final NotificationAction notificationAction) {
    789         final Parcel out = Parcel.obtain();
    790         notificationAction.writeToParcel(out, 0);
    791         out.setDataPosition(0);
    792         intent.putExtra(NotificationActionIntentService.EXTRA_NOTIFICATION_ACTION, out.marshall());
    793     }
    794 }
    795