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         builder.setCategory(NotificationCompat.CATEGORY_EMAIL);
    639 
    640         final RemoteViews undoView =
    641                 new RemoteViews(context.getPackageName(), R.layout.undo_notification);
    642         undoView.setTextViewText(
    643                 R.id.description_text, context.getString(notificationAction.getActionTextResId()));
    644 
    645         final String packageName = context.getPackageName();
    646 
    647         final Intent clickIntent = new Intent(NotificationActionIntentService.ACTION_UNDO);
    648         clickIntent.setPackage(packageName);
    649         clickIntent.setData(notificationAction.mConversation.uri);
    650         putNotificationActionExtra(clickIntent, notificationAction);
    651         final PendingIntent clickPendingIntent = PendingIntent.getService(context, notificationId,
    652                 clickIntent, PendingIntent.FLAG_CANCEL_CURRENT);
    653 
    654         undoView.setOnClickPendingIntent(R.id.status_bar_latest_event_content, clickPendingIntent);
    655 
    656         builder.setContent(undoView);
    657 
    658         // When the notification is cleared, we perform the destructive action
    659         final Intent deleteIntent = new Intent(NotificationActionIntentService.ACTION_DESTRUCT);
    660         deleteIntent.setPackage(packageName);
    661         deleteIntent.setData(notificationAction.mConversation.uri);
    662         putNotificationActionExtra(deleteIntent, notificationAction);
    663         final PendingIntent deletePendingIntent = PendingIntent.getService(context,
    664                 notificationId, deleteIntent, PendingIntent.FLAG_CANCEL_CURRENT);
    665         builder.setDeleteIntent(deletePendingIntent);
    666 
    667         final Notification notification = builder.build();
    668 
    669         return notification;
    670     }
    671 
    672     /**
    673      * Registers a timeout for the undo notification such that when it expires, the undo bar will
    674      * disappear, and the action will be performed.
    675      */
    676     public static void registerUndoTimeout(
    677             final Context context, final NotificationAction notificationAction) {
    678         LogUtils.i(LOG_TAG, "registerUndoTimeout for %s",
    679                 notificationAction.getNotificationActionType());
    680 
    681         if (sUndoTimeoutMillis == -1) {
    682             sUndoTimeoutMillis =
    683                     context.getResources().getInteger(R.integer.undo_notification_timeout);
    684         }
    685 
    686         final AlarmManager alarmManager =
    687                 (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
    688 
    689         final long triggerAtMills = SystemClock.elapsedRealtime() + sUndoTimeoutMillis;
    690 
    691         final PendingIntent pendingIntent =
    692                 createUndoTimeoutPendingIntent(context, notificationAction);
    693 
    694         alarmManager.set(AlarmManager.ELAPSED_REALTIME, triggerAtMills, pendingIntent);
    695     }
    696 
    697     /**
    698      * Cancels the undo timeout for a notification action. This should be called if the undo
    699      * notification is clicked (to prevent the action from being performed anyway) or cleared (since
    700      * we have already performed the action).
    701      */
    702     public static void cancelUndoTimeout(
    703             final Context context, final NotificationAction notificationAction) {
    704         LogUtils.i(LOG_TAG, "cancelUndoTimeout for %s",
    705                 notificationAction.getNotificationActionType());
    706 
    707         final AlarmManager alarmManager =
    708                 (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
    709 
    710         final PendingIntent pendingIntent =
    711                 createUndoTimeoutPendingIntent(context, notificationAction);
    712 
    713         alarmManager.cancel(pendingIntent);
    714     }
    715 
    716     /**
    717      * Creates a {@link PendingIntent} to be used for creating and canceling the undo timeout
    718      * alarm.
    719      */
    720     private static PendingIntent createUndoTimeoutPendingIntent(
    721             final Context context, final NotificationAction notificationAction) {
    722         final Intent intent = new Intent(NotificationActionIntentService.ACTION_UNDO_TIMEOUT);
    723         intent.setPackage(context.getPackageName());
    724         intent.setData(notificationAction.mConversation.uri);
    725         putNotificationActionExtra(intent, notificationAction);
    726 
    727         final int requestCode = notificationAction.getAccount().hashCode()
    728                 ^ notificationAction.getFolder().hashCode();
    729         final PendingIntent pendingIntent =
    730                 PendingIntent.getService(context, requestCode, intent, 0);
    731 
    732         return pendingIntent;
    733     }
    734 
    735     /**
    736      * Processes the specified destructive action (archive, delete, mute) on the message.
    737      */
    738     public static void processDestructiveAction(
    739             final Context context, final NotificationAction notificationAction) {
    740         LogUtils.i(LOG_TAG, "processDestructiveAction: %s",
    741                 notificationAction.getNotificationActionType());
    742 
    743         final NotificationActionType destructAction =
    744                 notificationAction.getNotificationActionType();
    745         final Conversation conversation = notificationAction.getConversation();
    746         final Folder folder = notificationAction.getFolder();
    747 
    748         final ContentResolver contentResolver = context.getContentResolver();
    749         final Uri uri = conversation.uri.buildUpon().appendQueryParameter(
    750                 UIProvider.FORCE_UI_NOTIFICATIONS_QUERY_PARAMETER, Boolean.TRUE.toString()).build();
    751 
    752         switch (destructAction) {
    753             case ARCHIVE_REMOVE_LABEL: {
    754                 if (folder.isInbox()) {
    755                     // Inbox, so archive
    756                     final ContentValues values = new ContentValues(1);
    757                     values.put(UIProvider.ConversationOperations.OPERATION_KEY,
    758                             UIProvider.ConversationOperations.ARCHIVE);
    759 
    760                     contentResolver.update(uri, values, null, null);
    761                 } else {
    762                     // Not inbox, so remove label
    763                     final ContentValues values = new ContentValues(1);
    764 
    765                     final String removeFolderUri = folder.folderUri.fullUri.buildUpon()
    766                             .appendPath(Boolean.FALSE.toString()).toString();
    767                     values.put(ConversationOperations.FOLDERS_UPDATED, removeFolderUri);
    768 
    769                     contentResolver.update(uri, values, null, null);
    770                 }
    771                 break;
    772             }
    773             case DELETE: {
    774                 contentResolver.delete(uri, null, null);
    775                 break;
    776             }
    777             default:
    778                 throw new IllegalArgumentException(
    779                         "The specified NotificationActionType is not a destructive action.");
    780         }
    781     }
    782 
    783     /**
    784      * Creates and displays an Undo notification for the specified {@link NotificationAction}.
    785      */
    786     public static void createUndoNotification(final Context context,
    787             final NotificationAction notificationAction) {
    788         LogUtils.i(LOG_TAG, "createUndoNotification for %s",
    789                 notificationAction.getNotificationActionType());
    790 
    791         final int notificationId = NotificationUtils.getNotificationId(
    792                 notificationAction.getAccount().getAccountManagerAccount(),
    793                 notificationAction.getFolder());
    794 
    795         final Notification notification =
    796                 createUndoNotification(context, notificationAction, notificationId);
    797 
    798         final NotificationManager notificationManager =
    799                 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    800         notificationManager.notify(notificationId, notification);
    801 
    802         sUndoNotifications.put(notificationId, notificationAction);
    803         sNotificationTimestamps.put(notificationId, notificationAction.getWhen());
    804     }
    805 
    806     /**
    807      * Called when an Undo notification has been tapped.
    808      */
    809     public static void cancelUndoNotification(final Context context,
    810             final NotificationAction notificationAction) {
    811         LogUtils.i(LOG_TAG, "cancelUndoNotification for %s",
    812                 notificationAction.getNotificationActionType());
    813 
    814         final Account account = notificationAction.getAccount();
    815         final Folder folder = notificationAction.getFolder();
    816         final Conversation conversation = notificationAction.getConversation();
    817         final int notificationId =
    818                 NotificationUtils.getNotificationId(account.getAccountManagerAccount(), folder);
    819 
    820         // Note: we must add the conversation before removing the undo notification
    821         // Otherwise, the observer for sUndoNotifications gets called, which calls
    822         // handleNotificationActions before the undone conversation has been added to the set.
    823         sUndoneConversations.add(conversation);
    824         removeUndoNotification(context, notificationId, false);
    825         resendNotifications(context, account, folder);
    826     }
    827 
    828     /**
    829      * If an undo notification is left alone for a long enough time, it will disappear, this method
    830      * will be called, and the action will be finalized.
    831      */
    832     public static void processUndoNotification(final Context context,
    833             final NotificationAction notificationAction) {
    834         LogUtils.i(LOG_TAG, "processUndoNotification, %s",
    835                 notificationAction.getNotificationActionType());
    836 
    837         final Account account = notificationAction.getAccount();
    838         final Folder folder = notificationAction.getFolder();
    839         final int notificationId = NotificationUtils.getNotificationId(
    840                 account.getAccountManagerAccount(), folder);
    841         removeUndoNotification(context, notificationId, true);
    842         sNotificationTimestamps.delete(notificationId);
    843         processDestructiveAction(context, notificationAction);
    844     }
    845 
    846     /**
    847      * Removes the undo notification.
    848      *
    849      * @param removeNow <code>true</code> to remove it from the drawer right away,
    850      *        <code>false</code> to just remove the reference to it
    851      */
    852     private static void removeUndoNotification(
    853             final Context context, final int notificationId, final boolean removeNow) {
    854         sUndoNotifications.delete(notificationId);
    855 
    856         if (removeNow) {
    857             final NotificationManager notificationManager =
    858                     (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    859             notificationManager.cancel(notificationId);
    860         }
    861     }
    862 
    863     /**
    864      * Broadcasts an {@link Intent} to inform the app to resend its notifications.
    865      */
    866     public static void resendNotifications(final Context context, final Account account,
    867             final Folder folder) {
    868         LogUtils.i(LOG_TAG, "resendNotifications account: %s, folder: %s",
    869                 account == null ? null : LogUtils.sanitizeName(LOG_TAG, account.getEmailAddress()),
    870                 folder == null ? null : LogUtils.sanitizeName(LOG_TAG, folder.name));
    871 
    872         final Intent intent = new Intent(MailIntentService.ACTION_RESEND_NOTIFICATIONS);
    873         intent.setPackage(context.getPackageName()); // Make sure we only deliver this to ourselves
    874         if (account != null) {
    875             intent.putExtra(Utils.EXTRA_ACCOUNT_URI, account.uri);
    876         }
    877         if (folder != null) {
    878             intent.putExtra(Utils.EXTRA_FOLDER_URI, folder.folderUri.fullUri);
    879         }
    880         context.startService(intent);
    881     }
    882 
    883     public static void registerUndoNotificationObserver(final DataSetObserver observer) {
    884         sUndoNotifications.getDataSetObservable().registerObserver(observer);
    885     }
    886 
    887     public static void unregisterUndoNotificationObserver(final DataSetObserver observer) {
    888         sUndoNotifications.getDataSetObservable().unregisterObserver(observer);
    889     }
    890 
    891     /**
    892      * <p>
    893      * This is a slight hack to avoid an exception in the remote AlarmManagerService process. The
    894      * AlarmManager adds extra data to this Intent which causes it to inflate. Since the remote
    895      * process does not know about the NotificationAction class, it throws a ClassNotFoundException.
    896      * </p>
    897      * <p>
    898      * To avoid this, we marshall the data ourselves and then parcel a plain byte[] array. The
    899      * NotificationActionIntentService class knows to build the NotificationAction object from the
    900      * byte[] array.
    901      * </p>
    902      */
    903     private static void putNotificationActionExtra(final Intent intent,
    904             final NotificationAction notificationAction) {
    905         final Parcel out = Parcel.obtain();
    906         notificationAction.writeToParcel(out, 0);
    907         out.setDataPosition(0);
    908         intent.putExtra(NotificationActionIntentService.EXTRA_NOTIFICATION_ACTION, out.marshall());
    909     }
    910 }
    911