Gmail changes to support Android Wear
* Replace reply/reply all action with Android Wear action.
* ComposeActivity handles the reply intent and sends email in the background
  using AsyncTask.
* Create notification bundle for all message so that user can apply action to
  each of the message inside the bundle.

Bug 13405604
Bug 13629669

Change-Id: Ia7efa28c7f333b9571d7e99e83cba6124f843e50
diff --git a/src/com/android/mail/utils/NotificationUtils.java b/src/com/android/mail/utils/NotificationUtils.java
index b45076a..8f41e28 100644
--- a/src/com/android/mail/utils/NotificationUtils.java
+++ b/src/com/android/mail/utils/NotificationUtils.java
@@ -28,6 +28,8 @@
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.net.Uri;
+import android.preview.support.v4.app.NotificationManagerCompat;
+import android.preview.support.wearable.notifications.WearableNotifications;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.Contacts.Photo;
@@ -38,6 +40,7 @@
 import android.text.TextUtils;
 import android.text.style.CharacterStyle;
 import android.text.style.TextAppearanceSpan;
+import android.util.ArrayMap;
 import android.util.Pair;
 import android.util.SparseArray;
 
@@ -72,7 +75,10 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -98,6 +104,9 @@
 
     private static BidiFormatter sBidiFormatter = BidiFormatter.getInstance();
 
+    private static Map<NotificationKey, Set<Integer>> sChildNotificationsMap =
+            new HashMap<NotificationKey, Set<Integer>>();
+
     /**
      * Clears all notifications in response to the user tapping "Clear" in the status bar.
      */
@@ -301,8 +310,7 @@
      **/
     public static void cancelAllNotifications(Context context) {
         LogUtils.d(LOG_TAG, "cancelAllNotifications - cancelling all");
-        NotificationManager nm = (NotificationManager) context.getSystemService(
-                Context.NOTIFICATION_SERVICE);
+        NotificationManagerCompat nm = NotificationManagerCompat.from(context);
         nm.cancelAll();
         clearAllNotfications(context);
     }
@@ -338,8 +346,7 @@
 
         if (cancelExisting) {
             LogUtils.d(LOG_TAG, "resendNotifications - cancelling all");
-            NotificationManager nm =
-                    (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+            NotificationManagerCompat nm = NotificationManagerCompat.from(context);
             nm.cancelAll();
         }
         // Re-validate the notifications.
@@ -491,8 +498,7 @@
             final Account account, boolean getAttention, boolean ignoreUnobtrusiveSetting,
             NotificationKey key) {
 
-        NotificationManager nm = (NotificationManager)
-                context.getSystemService(Context.NOTIFICATION_SERVICE);
+        NotificationManagerCompat nm = NotificationManagerCompat.from(context);
 
         final NotificationMap notificationMap = getNotificationMap(context);
         if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
@@ -551,11 +557,15 @@
             final int notificationId =
                     getNotificationId(account.getAccountManagerAccount(), folder);
 
+            NotificationKey notificationKey = new NotificationKey(account, folder);
+
             if (unseenCount == 0) {
                 LogUtils.i(LOG_TAG, "validateNotifications - cancelling account %s / folder %s",
                         LogUtils.sanitizeName(LOG_TAG, account.getEmailAddress()),
                         LogUtils.sanitizeName(LOG_TAG, folder.persistentId));
                 nm.cancel(notificationId);
+                cancelChildNotifications(notificationKey, nm);
+
                 return;
             }
 
@@ -563,6 +573,10 @@
             PendingIntent clickIntent;
 
             NotificationCompat.Builder notification = new NotificationCompat.Builder(context);
+            WearableNotifications.Builder wearableNotification =
+                    new WearableNotifications.Builder(notification);
+            Map<Integer, WearableNotifications.Builder> msgNotifications =
+                    new ArrayMap<Integer, WearableNotifications.Builder>();
             notification.setSmallIcon(R.drawable.stat_notify_email);
             notification.setTicker(account.getDisplayName());
 
@@ -652,8 +666,9 @@
                     notificationIntent.removeExtra(Utils.EXTRA_FROM_NOTIFICATION);
 
                     configureLatestEventInfoFromConversation(context, account, folderPreferences,
-                            notification, cursor, clickIntent, notificationIntent,
-                            unreadCount, unseenCount, folder, when);
+                            notification, wearableNotification, msgNotifications, notificationId,
+                            cursor, clickIntent, notificationIntent, unreadCount, unseenCount,
+                            folder, when);
                     eventInfoConfigured = true;
                 }
             }
@@ -676,11 +691,30 @@
 
             int defaults = 0;
 
+            // Check if any current child notifications exist previously.  Only notify if one of
+            // them is new.
+            boolean hasNewChildNotification;
+            Set<Integer> prevChildNotifications = sChildNotificationsMap.get(notificationKey);
+            if (prevChildNotifications != null) {
+                hasNewChildNotification = false;
+                for (Integer currentNotificationId : msgNotifications.keySet()) {
+                    if (!prevChildNotifications.contains(currentNotificationId)) {
+                        hasNewChildNotification = true;
+                        break;
+                    }
+                }
+            } else {
+                hasNewChildNotification = true;
+            }
+
+            LogUtils.d(LOG_TAG, "getAttention=%s,oldWhen=%s,hasNewChildNotification=%s",
+                    getAttention, oldWhen, hasNewChildNotification);
+
             /*
              * We do not want to notify if this is coming back from an Undo notification, hence the
              * oldWhen check.
              */
-            if (getAttention && oldWhen == 0) {
+            if (getAttention && oldWhen == 0 && hasNewChildNotification) {
                 final AccountPreferences accountPreferences =
                         new AccountPreferences(context, account.getEmailAddress());
                 if (accountPreferences.areNotificationsEnabled()) {
@@ -707,7 +741,28 @@
                     notification.setTicker(null);
                 }
 
-                nm.notify(notificationId, notification.build());
+                nm.notify(notificationId, wearableNotification.build());
+
+                if (prevChildNotifications != null) {
+                    Set<Integer> currentNotificationIds = msgNotifications.keySet();
+                    for (Integer prevChildNotificationId : prevChildNotifications) {
+                        if (!currentNotificationIds.contains(prevChildNotificationId)) {
+                            nm.cancel(prevChildNotificationId);
+                            LogUtils.d(LOG_TAG, "canceling child notification %s",
+                                    prevChildNotificationId);
+                        }
+                    }
+                }
+
+                for (Map.Entry<Integer, WearableNotifications.Builder> entry
+                        : msgNotifications.entrySet()) {
+                    nm.notify(entry.getKey(), entry.getValue().build());
+                    LogUtils.d(LOG_TAG, "notifying child notification %s", entry.getKey());
+                }
+
+                Set<Integer> childNotificationIds = new HashSet<Integer>();
+                childNotificationIds.addAll(msgNotifications.keySet());
+                sChildNotificationsMap.put(notificationKey, childNotificationIds);
             } else {
                 LogUtils.i(LOG_TAG, "event info not configured - not notifying");
             }
@@ -780,7 +835,10 @@
 
     private static void configureLatestEventInfoFromConversation(final Context context,
             final Account account, final FolderPreferences folderPreferences,
-            final NotificationCompat.Builder notification, final Cursor conversationCursor,
+            final NotificationCompat.Builder notification,
+            final WearableNotifications.Builder summaryWearNotif,
+            final Map<Integer, WearableNotifications.Builder> msgNotifications,
+            final int summaryNotificationId, final Cursor conversationCursor,
             final PendingIntent clickIntent, final Intent notificationIntent,
             final int unreadCount, final int unseenCount,
             final Folder folder, final long when) {
@@ -826,6 +884,12 @@
                 final NotificationCompat.InboxStyle digest =
                         new NotificationCompat.InboxStyle(notification);
 
+                // Group by account.
+                String notificationGroupKey =
+                        account.uri.toString() + "/" + folder.folderUri.fullUri;
+                summaryWearNotif.setGroup(notificationGroupKey,
+                        WearableNotifications.GROUP_ORDER_SUMMARY);
+
                 int numDigestItems = 0;
                 do {
                     final Conversation conversation = new Conversation(conversationCursor);
@@ -879,6 +943,25 @@
                                     conversation.getSnippet());
                             digest.addLine(digestLine);
                             numDigestItems++;
+
+                            // Adding child notification for Wear.
+                            NotificationCompat.Builder childNotif =
+                                    new NotificationCompat.Builder(context);
+                            childNotif.setSmallIcon(R.drawable.stat_notify_email);
+                            childNotif.setContentText(digestLine);
+
+                            WearableNotifications.Builder childWearNotif =
+                                    new WearableNotifications.Builder(childNotif).setGroup(
+                                            notificationGroupKey, numDigestItems);
+                            int childNotificationId = getNotificationId(summaryNotificationId,
+                                    conversation.hashCode());
+
+                            configureNotifForOneConversation(context, account, folderPreferences,
+                                    childNotif, childWearNotif, conversationCursor,
+                                    notificationIntent, folder, when, res,
+                                    notificationAccountDisplayName, notificationAccountEmail,
+                                    isInbox, notificationLabelName, childNotificationId);
+                            msgNotifications.put(childNotificationId, childWearNotif);
                         } finally {
                             if (messageCursor != null) {
                                 messageCursor.close();
@@ -895,137 +978,17 @@
                         isInbox ? notificationAccountDisplayName : notificationLabelName);
             }
         } else {
-            // For notifications for a single new conversation, we want to get the information from
-            // the conversation
+            // For notifications for a single new conversation, we want to get the information
+            // from the conversation
 
             // Move the cursor to the most recent unread conversation
             seekToLatestUnreadConversation(conversationCursor);
 
-            final Conversation conversation = new Conversation(conversationCursor);
-
-            Cursor cursor = null;
-            MessageCursor messageCursor = null;
-            boolean multipleUnseenThread = false;
-            String from = null;
-            try {
-                final Uri uri = conversation.messageListUri.buildUpon().appendQueryParameter(
-                        UIProvider.LABEL_QUERY_PARAMETER, folder.persistentId).build();
-                cursor = context.getContentResolver().query(uri, UIProvider.MESSAGE_PROJECTION,
-                        null, null, null);
-                messageCursor = new MessageCursor(cursor);
-                // Use the information from the last sender in the conversation that triggered
-                // this notification.
-
-                String fromAddress = "";
-                if (messageCursor.moveToPosition(messageCursor.getCount() - 1)) {
-                    final Message message = messageCursor.getMessage();
-                    fromAddress = message.getFrom();
-                    from = getDisplayableSender(fromAddress);
-                    notification.setLargeIcon(
-                            getContactIcon(context, from, getSenderAddress(fromAddress), folder));
-                }
-
-                // Assume that the last message in this conversation is unread
-                int firstUnseenMessagePos = messageCursor.getPosition();
-                while (messageCursor.moveToPosition(messageCursor.getPosition() - 1)) {
-                    final Message message = messageCursor.getMessage();
-                    final boolean unseen = !message.seen;
-                    if (unseen) {
-                        firstUnseenMessagePos = messageCursor.getPosition();
-                        if (!multipleUnseenThread
-                                && !fromAddress.contentEquals(message.getFrom())) {
-                            multipleUnseenThread = true;
-                        }
-                    }
-                }
-
-                // TODO(skennedy) Can we remove this check?
-                if (Utils.isRunningJellybeanOrLater()) {
-                    // For a new-style notification
-
-                    if (multipleUnseenThread) {
-                        // The title of a single conversation is the list of senders.
-                        int sendersLength = res.getInteger(R.integer.swipe_senders_length);
-
-                        final SpannableStringBuilder sendersBuilder = getStyledSenders(
-                                context, conversationCursor, sendersLength,
-                                notificationAccountEmail);
-
-                        notification.setContentTitle(sendersBuilder);
-                        // For a single new conversation, the ticker is based on the sender's name.
-                        notificationTicker = sendersBuilder.toString();
-                    } else {
-                        from = getWrappedFromString(from);
-                        // The title of a single message the sender.
-                        notification.setContentTitle(from);
-                        // For a single new conversation, the ticker is based on the sender's name.
-                        notificationTicker = from;
-                    }
-
-                    // The notification content will be the subject of the conversation.
-                    notification.setContentText(
-                            getSingleMessageLittleText(context, conversation.subject));
-
-                    // The notification subtext will be the subject of the conversation for inbox
-                    // notifications, or will based on the the label name for user label
-                    // notifications.
-                    notification.setSubText(isInbox ?
-                            notificationAccountDisplayName : notificationLabelName);
-
-                    if (multipleUnseenThread) {
-                        notification.setLargeIcon(
-                                getDefaultNotificationIcon(context, folder, true));
-                    }
-                    final NotificationCompat.BigTextStyle bigText =
-                            new NotificationCompat.BigTextStyle(notification);
-
-                    // Seek the message cursor to the first unread message
-                    final Message message;
-                    if (messageCursor.moveToPosition(firstUnseenMessagePos)) {
-                        message = messageCursor.getMessage();
-                        bigText.bigText(getSingleMessageBigText(context,
-                                conversation.subject, message));
-                    } else {
-                        LogUtils.e(LOG_TAG, "Failed to load message");
-                        message = null;
-                    }
-
-                    if (message != null) {
-                        final Set<String> notificationActions =
-                                folderPreferences.getNotificationActions(account);
-
-                        final int notificationId = getNotificationId(
-                                account.getAccountManagerAccount(), folder);
-
-                        NotificationActionUtils.addNotificationActions(context, notificationIntent,
-                                notification, account, conversation, message, folder,
-                                notificationId, when, notificationActions);
-                    }
-                } else {
-                    // For an old-style notification
-
-                    // The title of a single conversation notification is built from both the sender
-                    // and subject of the new message.
-                    notification.setContentTitle(getSingleMessageNotificationTitle(context,
-                            from, conversation.subject));
-
-                    // The notification content will be the subject of the conversation for inbox
-                    // notifications, or will based on the the label name for user label
-                    // notifications.
-                    notification.setContentText(
-                            isInbox ? notificationAccountDisplayName : notificationLabelName);
-
-                    // For a single new conversation, the ticker is based on the sender's name.
-                    notificationTicker = from;
-                }
-            } finally {
-                if (messageCursor != null) {
-                    messageCursor.close();
-                }
-                if (cursor != null) {
-                    cursor.close();
-                }
-            }
+            notificationTicker = configureNotifForOneConversation(context, account,
+                    folderPreferences, notification, summaryWearNotif, conversationCursor,
+                    notificationIntent, folder, when, res, notificationAccountDisplayName,
+                    notificationAccountEmail, isInbox, notificationLabelName,
+                    summaryNotificationId);
         }
 
         // Build the notification ticker
@@ -1048,6 +1011,144 @@
         notification.setContentIntent(clickIntent);
     }
 
+    /**
+     * Configure the notification for one conversation.  When there are multiple conversations,
+     * this method is used to configure bundled notification for Android Wear.
+     */
+    private static String configureNotifForOneConversation(Context context, Account account,
+            FolderPreferences folderPreferences, NotificationCompat.Builder notification,
+            WearableNotifications.Builder summaryWearNotif, Cursor conversationCursor,
+            Intent notificationIntent, Folder folder, long when, Resources res,
+            String notificationAccountDisplayName, String notificationAccountEmail, boolean isInbox,
+            String notificationLabelName, int notificationId) {
+
+        String notificationTicker;
+
+        final Conversation conversation = new Conversation(conversationCursor);
+
+        Cursor cursor = null;
+        MessageCursor messageCursor = null;
+        boolean multipleUnseenThread = false;
+        String from = null;
+        try {
+            final Uri uri = conversation.messageListUri.buildUpon().appendQueryParameter(
+                    UIProvider.LABEL_QUERY_PARAMETER, folder.persistentId).build();
+            cursor = context.getContentResolver().query(uri, UIProvider.MESSAGE_PROJECTION,
+                    null, null, null);
+            messageCursor = new MessageCursor(cursor);
+            // Use the information from the last sender in the conversation that triggered
+            // this notification.
+
+            String fromAddress = "";
+            if (messageCursor.moveToPosition(messageCursor.getCount() - 1)) {
+                final Message message = messageCursor.getMessage();
+                fromAddress = message.getFrom();
+                from = getDisplayableSender(fromAddress);
+                notification.setLargeIcon(
+                        getContactIcon(context, from, getSenderAddress(fromAddress), folder));
+            }
+
+            // Assume that the last message in this conversation is unread
+            int firstUnseenMessagePos = messageCursor.getPosition();
+            while (messageCursor.moveToPosition(messageCursor.getPosition() - 1)) {
+                final Message message = messageCursor.getMessage();
+                final boolean unseen = !message.seen;
+                if (unseen) {
+                    firstUnseenMessagePos = messageCursor.getPosition();
+                    if (!multipleUnseenThread
+                            && !fromAddress.contentEquals(message.getFrom())) {
+                        multipleUnseenThread = true;
+                    }
+                }
+            }
+
+            // TODO(skennedy) Can we remove this check?
+            if (Utils.isRunningJellybeanOrLater()) {
+                // For a new-style notification
+
+                if (multipleUnseenThread) {
+                    // The title of a single conversation is the list of senders.
+                    int sendersLength = res.getInteger(R.integer.swipe_senders_length);
+
+                    final SpannableStringBuilder sendersBuilder = getStyledSenders(
+                            context, conversationCursor, sendersLength,
+                            notificationAccountEmail);
+
+                    notification.setContentTitle(sendersBuilder);
+                    // For a single new conversation, the ticker is based on the sender's name.
+                    notificationTicker = sendersBuilder.toString();
+                } else {
+                    from = getWrappedFromString(from);
+                    // The title of a single message the sender.
+                    notification.setContentTitle(from);
+                    // For a single new conversation, the ticker is based on the sender's name.
+                    notificationTicker = from;
+                }
+
+                // The notification content will be the subject of the conversation.
+                notification.setContentText(
+                        getSingleMessageLittleText(context, conversation.subject));
+
+                // The notification subtext will be the subject of the conversation for inbox
+                // notifications, or will based on the the label name for user label
+                // notifications.
+                notification.setSubText(isInbox ?
+                        notificationAccountDisplayName : notificationLabelName);
+
+                if (multipleUnseenThread) {
+                    notification.setLargeIcon(
+                            getDefaultNotificationIcon(context, folder, true));
+                }
+                final NotificationCompat.BigTextStyle bigText =
+                        new NotificationCompat.BigTextStyle(notification);
+
+                // Seek the message cursor to the first unread message
+                final Message message;
+                if (messageCursor.moveToPosition(firstUnseenMessagePos)) {
+                    message = messageCursor.getMessage();
+                    bigText.bigText(getSingleMessageBigText(context,
+                            conversation.subject, message));
+                } else {
+                    LogUtils.e(LOG_TAG, "Failed to load message");
+                    message = null;
+                }
+
+                if (message != null) {
+                    final Set<String> notificationActions =
+                            folderPreferences.getNotificationActions(account);
+
+                    NotificationActionUtils.addNotificationActions(context, notificationIntent,
+                            notification, summaryWearNotif, account, conversation, message,
+                            folder, notificationId, when, notificationActions);
+                }
+            } else {
+                // For an old-style notification
+
+                // The title of a single conversation notification is built from both the sender
+                // and subject of the new message.
+                notification.setContentTitle(getSingleMessageNotificationTitle(context,
+                        from, conversation.subject));
+
+                // The notification content will be the subject of the conversation for inbox
+                // notifications, or will based on the the label name for user label
+                // notifications.
+                notification.setContentText(
+                        isInbox ? notificationAccountDisplayName : notificationLabelName);
+
+                // For a single new conversation, the ticker is based on the sender's name.
+                notificationTicker = from;
+            }
+        } finally {
+            if (messageCursor != null) {
+                messageCursor.close();
+            }
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+        return notificationTicker;
+    }
+
     private static String getWrappedFromString(String from) {
         if (from == null) {
             LogUtils.e(LOG_TAG, "null from string in getWrappedFromString");
@@ -1310,10 +1411,12 @@
         notificationMap.remove(key);
         notificationMap.saveNotificationMap(context);
 
-        final NotificationManager notificationManager =
-                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+        final NotificationManagerCompat notificationManager =
+                NotificationManagerCompat.from(context);
         notificationManager.cancel(getNotificationId(account.getAccountManagerAccount(), folder));
 
+        cancelChildNotifications(key, notificationManager);
+
         if (markSeen) {
             markSeen(context, folder);
         }
@@ -1338,18 +1441,31 @@
 
         final List<NotificationKey> notificationKeys = keyBuilder.build();
 
-        final NotificationManager notificationManager =
-                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+        final NotificationManagerCompat notificationManager =
+                NotificationManagerCompat.from(context);
 
         for (final NotificationKey notificationKey : notificationKeys) {
             final Folder folder = notificationKey.folder;
             notificationManager.cancel(getNotificationId(account, folder));
             notificationMap.remove(notificationKey);
+
+            cancelChildNotifications(notificationKey, notificationManager);
         }
 
         notificationMap.saveNotificationMap(context);
     }
 
+    private static void cancelChildNotifications(NotificationKey key,
+            NotificationManagerCompat nm) {
+        Set<Integer> childNotifications = sChildNotificationsMap.get(key);
+        if (childNotifications != null) {
+            for (Integer childNotification : childNotifications) {
+                nm.cancel(childNotification);
+            }
+            sChildNotificationsMap.remove(key);
+        }
+    }
+
     private static ArrayList<Long> findContacts(Context context, Collection<String> addresses) {
         ArrayList<String> whereArgs = new ArrayList<String>();
         StringBuilder whereBuilder = new StringBuilder();
@@ -1515,6 +1631,10 @@
         return 1 ^ account.hashCode() ^ folder.hashCode();
     }
 
+    private static int getNotificationId(int summaryNotificationId, int childHashCode) {
+        return summaryNotificationId ^ childHashCode;
+    }
+
     private static class NotificationKey {
         public final Account account;
         public final Folder folder;