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