blob: fa93733c652366ccec217c5054d211ddc9047022 [file] [log] [blame]
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.mail.utils;
17
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080018import android.app.Notification;
19import android.app.NotificationManager;
20import android.app.PendingIntent;
21import android.content.ContentResolver;
22import android.content.ContentUris;
23import android.content.ContentValues;
24import android.content.Context;
25import android.content.Intent;
26import android.content.res.Resources;
27import android.database.Cursor;
28import android.graphics.Bitmap;
29import android.graphics.BitmapFactory;
30import android.net.Uri;
Alan Lau15490232014-03-06 14:53:14 -080031import android.preview.support.v4.app.NotificationManagerCompat;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080032import android.provider.ContactsContract;
33import android.provider.ContactsContract.CommonDataKinds.Email;
34import android.provider.ContactsContract.Contacts.Photo;
35import android.support.v4.app.NotificationCompat;
Andrew Sappersteinf5810962013-06-27 10:41:33 -070036import android.support.v4.text.BidiFormatter;
Andy Huang8f10ced2014-04-01 12:19:18 -070037import android.support.v4.util.ArrayMap;
Alan Lau3af592c2014-04-17 11:19:11 -070038import android.support.wearable.notifications.WearableNotifications;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080039import android.text.SpannableString;
40import android.text.SpannableStringBuilder;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080041import android.text.TextUtils;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080042import android.text.style.CharacterStyle;
43import android.text.style.TextAppearanceSpan;
44import android.util.Pair;
Scott Kennedy61bd0e82012-12-10 18:18:17 -080045import android.util.SparseArray;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080046
Tony Mantler821e5782014-01-06 15:33:43 -080047import com.android.emailcommon.mail.Address;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080048import com.android.mail.EmailAddress;
49import com.android.mail.MailIntentService;
50import com.android.mail.R;
Andy Huang4fe0af82013-08-20 17:24:51 -070051import com.android.mail.analytics.Analytics;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080052import com.android.mail.browse.MessageCursor;
53import com.android.mail.browse.SendersView;
Scott Kennedyfe235122013-06-28 12:06:36 -070054import com.android.mail.photomanager.LetterTileProvider;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080055import com.android.mail.preferences.AccountPreferences;
56import com.android.mail.preferences.FolderPreferences;
57import com.android.mail.preferences.MailPrefs;
58import com.android.mail.providers.Account;
59import com.android.mail.providers.Conversation;
60import com.android.mail.providers.Folder;
61import com.android.mail.providers.Message;
62import com.android.mail.providers.UIProvider;
Scott Kennedyc94c80c2013-06-27 14:42:41 -070063import com.android.mail.ui.ImageCanvas.Dimensions;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080064import com.android.mail.utils.NotificationActionUtils.NotificationAction;
Scott Kennedy1bdbfef2013-08-01 21:48:26 -070065import com.google.android.mail.common.html.parser.HTML;
66import com.google.android.mail.common.html.parser.HTML4;
67import com.google.android.mail.common.html.parser.HtmlDocument;
68import com.google.android.mail.common.html.parser.HtmlTree;
Andrew Sappersteinddd17bc2013-04-22 14:03:40 -070069import com.google.common.base.Objects;
Scott Kennedy7f8aed62013-05-22 18:35:17 -070070import com.google.common.collect.ImmutableList;
Scott Kennedy09400ef2013-03-07 11:09:47 -080071import com.google.common.collect.Lists;
Scott Kennedy09400ef2013-03-07 11:09:47 -080072import com.google.common.collect.Sets;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080073
74import java.io.ByteArrayInputStream;
75import java.util.ArrayList;
76import java.util.Arrays;
77import java.util.Collection;
Alan Lau15490232014-03-06 14:53:14 -080078import java.util.HashMap;
79import java.util.HashSet;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080080import java.util.List;
Alan Lau15490232014-03-06 14:53:14 -080081import java.util.Map;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080082import java.util.Set;
83import java.util.concurrent.ConcurrentHashMap;
84
85public class NotificationUtils {
Scott Kennedyc7d75472013-07-08 18:21:28 -070086 public static final String LOG_TAG = "NotifUtils";
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080087
88 /** Contains a list of <(account, label), unread conversations> */
89 private static NotificationMap sActiveNotificationMap = null;
90
Scott Kennedy61bd0e82012-12-10 18:18:17 -080091 private static final SparseArray<Bitmap> sNotificationIcons = new SparseArray<Bitmap>();
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080092
93 private static TextAppearanceSpan sNotificationUnreadStyleSpan;
94 private static CharacterStyle sNotificationReadStyleSpan;
95
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080096 /** A factory that produces a plain text converter that removes elided text. */
97 private static final HtmlTree.PlainTextConverterFactory MESSAGE_CONVERTER_FACTORY =
98 new HtmlTree.PlainTextConverterFactory() {
99 @Override
100 public HtmlTree.PlainTextConverter createInstance() {
101 return new MailMessagePlainTextConverter();
102 }
103 };
104
Andrew Sapperstein963c7e42014-03-04 15:51:58 -0800105 private static BidiFormatter sBidiFormatter = BidiFormatter.getInstance();
Andrew Sappersteinf5810962013-06-27 10:41:33 -0700106
Alan Lau15490232014-03-06 14:53:14 -0800107 private static Map<NotificationKey, Set<Integer>> sChildNotificationsMap =
108 new HashMap<NotificationKey, Set<Integer>>();
109
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800110 /**
111 * Clears all notifications in response to the user tapping "Clear" in the status bar.
112 */
113 public static void clearAllNotfications(Context context) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700114 LogUtils.v(LOG_TAG, "Clearing all notifications.");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800115 final NotificationMap notificationMap = getNotificationMap(context);
116 notificationMap.clear();
117 notificationMap.saveNotificationMap(context);
118 }
119
120 /**
121 * Returns the notification map, creating it if necessary.
122 */
123 private static synchronized NotificationMap getNotificationMap(Context context) {
124 if (sActiveNotificationMap == null) {
125 sActiveNotificationMap = new NotificationMap();
126
127 // populate the map from the cached data
128 sActiveNotificationMap.loadNotificationMap(context);
129 }
130 return sActiveNotificationMap;
131 }
132
133 /**
134 * Class representing the existing notifications, and the number of unread and
135 * unseen conversations that triggered each.
136 */
Anthony Lee2354fd92014-02-14 09:28:37 -0800137 private static final class NotificationMap {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800138
139 private static final String NOTIFICATION_PART_SEPARATOR = " ";
140 private static final int NUM_NOTIFICATION_PARTS= 4;
Anthony Lee2354fd92014-02-14 09:28:37 -0800141 private final ConcurrentHashMap<NotificationKey, Pair<Integer, Integer>> mMap =
142 new ConcurrentHashMap<NotificationKey, Pair<Integer, Integer>>();
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800143
144 /**
Anthony Lee2354fd92014-02-14 09:28:37 -0800145 * Returns the number of key values pairs in the inner map.
146 */
147 public int size() {
148 return mMap.size();
149 }
150
151 /**
152 * Returns a set of key values.
153 */
154 public Set<NotificationKey> keySet() {
155 return mMap.keySet();
156 }
157
158 /**
159 * Remove the key from the inner map and return its value.
160 *
161 * @param key The key {@link NotificationKey} to be removed.
162 * @return The value associated with this key.
163 */
164 public Pair<Integer, Integer> remove(NotificationKey key) {
165 return mMap.remove(key);
166 }
167
168 /**
169 * Clear all key-value pairs in the map.
170 */
171 public void clear() {
172 mMap.clear();
173 }
174
175 /**
176 * Discover if a key-value pair with this key exists.
177 *
178 * @param key The key {@link NotificationKey} to be checked.
179 * @return If a key-value pair with this key exists in the map.
180 */
181 public boolean containsKey(NotificationKey key) {
182 return mMap.containsKey(key);
183 }
184
185 /**
186 * Returns the unread count for the given NotificationKey.
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800187 */
188 public Integer getUnread(NotificationKey key) {
Anthony Lee2354fd92014-02-14 09:28:37 -0800189 final Pair<Integer, Integer> value = mMap.get(key);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800190 return value != null ? value.first : null;
191 }
192
193 /**
Anthony Lee2354fd92014-02-14 09:28:37 -0800194 * Returns the unread unseen count for the given NotificationKey.
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800195 */
196 public Integer getUnseen(NotificationKey key) {
Anthony Lee2354fd92014-02-14 09:28:37 -0800197 final Pair<Integer, Integer> value = mMap.get(key);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800198 return value != null ? value.second : null;
199 }
200
201 /**
202 * Store the unread and unseen value for the given NotificationKey
203 */
204 public void put(NotificationKey key, int unread, int unseen) {
205 final Pair<Integer, Integer> value =
206 new Pair<Integer, Integer>(Integer.valueOf(unread), Integer.valueOf(unseen));
Anthony Lee2354fd92014-02-14 09:28:37 -0800207 mMap.put(key, value);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800208 }
209
210 /**
211 * Populates the notification map with previously cached data.
212 */
213 public synchronized void loadNotificationMap(final Context context) {
214 final MailPrefs mailPrefs = MailPrefs.get(context);
215 final Set<String> notificationSet = mailPrefs.getActiveNotificationSet();
216 if (notificationSet != null) {
217 for (String notificationEntry : notificationSet) {
218 // Get the parts of the string that make the notification entry
219 final String[] notificationParts =
220 TextUtils.split(notificationEntry, NOTIFICATION_PART_SEPARATOR);
221 if (notificationParts.length == NUM_NOTIFICATION_PARTS) {
222 final Uri accountUri = Uri.parse(notificationParts[0]);
223 final Cursor accountCursor = context.getContentResolver().query(
224 accountUri, UIProvider.ACCOUNTS_PROJECTION, null, null, null);
225 final Account account;
226 try {
227 if (accountCursor.moveToFirst()) {
228 account = new Account(accountCursor);
229 } else {
230 continue;
231 }
232 } finally {
233 accountCursor.close();
234 }
235
236 final Uri folderUri = Uri.parse(notificationParts[1]);
237 final Cursor folderCursor = context.getContentResolver().query(
238 folderUri, UIProvider.FOLDERS_PROJECTION, null, null, null);
239 final Folder folder;
240 try {
241 if (folderCursor.moveToFirst()) {
242 folder = new Folder(folderCursor);
243 } else {
244 continue;
245 }
246 } finally {
247 folderCursor.close();
248 }
249
250 final NotificationKey key = new NotificationKey(account, folder);
251 final Integer unreadValue = Integer.valueOf(notificationParts[2]);
252 final Integer unseenValue = Integer.valueOf(notificationParts[3]);
Anthony Lee2354fd92014-02-14 09:28:37 -0800253 put(key, unreadValue, unseenValue);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800254 }
255 }
256 }
257 }
258
259 /**
260 * Cache the notification map.
261 */
262 public synchronized void saveNotificationMap(Context context) {
263 final Set<String> notificationSet = Sets.newHashSet();
Anthony Lee2354fd92014-02-14 09:28:37 -0800264 final Set<NotificationKey> keys = mMap.keySet();
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800265 for (NotificationKey key : keys) {
Anthony Lee2354fd92014-02-14 09:28:37 -0800266 final Pair<Integer, Integer> value = mMap.get(key);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800267 final Integer unreadCount = value.first;
268 final Integer unseenCount = value.second;
Scott Kennedyff8553f2013-04-05 20:57:44 -0700269 if (unreadCount != null && unseenCount != null) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800270 final String[] partValues = new String[] {
Scott Kennedy259df5b2013-07-11 13:24:01 -0700271 key.account.uri.toString(), key.folder.folderUri.fullUri.toString(),
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800272 unreadCount.toString(), unseenCount.toString()};
273 notificationSet.add(TextUtils.join(NOTIFICATION_PART_SEPARATOR, partValues));
274 }
275 }
276 final MailPrefs mailPrefs = MailPrefs.get(context);
277 mailPrefs.cacheActiveNotificationSet(notificationSet);
278 }
279 }
280
281 /**
282 * @return the title of this notification with each account and the number of unread and unseen
283 * conversations for it. Also remove any account in the map that has 0 unread.
284 */
285 private static String createNotificationString(NotificationMap notifications) {
286 StringBuilder result = new StringBuilder();
287 int i = 0;
288 Set<NotificationKey> keysToRemove = Sets.newHashSet();
289 for (NotificationKey key : notifications.keySet()) {
290 Integer unread = notifications.getUnread(key);
291 Integer unseen = notifications.getUnseen(key);
292 if (unread == null || unread.intValue() == 0) {
293 keysToRemove.add(key);
294 } else {
295 if (i > 0) result.append(", ");
296 result.append(key.toString() + " (" + unread + ", " + unseen + ")");
297 i++;
298 }
299 }
300
301 for (NotificationKey key : keysToRemove) {
302 notifications.remove(key);
303 }
304
305 return result.toString();
306 }
307
308 /**
309 * Get all notifications for all accounts and cancel them.
310 **/
311 public static void cancelAllNotifications(Context context) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700312 LogUtils.d(LOG_TAG, "cancelAllNotifications - cancelling all");
Alan Lau15490232014-03-06 14:53:14 -0800313 NotificationManagerCompat nm = NotificationManagerCompat.from(context);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800314 nm.cancelAll();
315 clearAllNotfications(context);
316 }
317
318 /**
319 * Get all notifications for all accounts, cancel them, and repost.
320 * This happens when locale changes.
321 **/
Andrew Sapperstein963c7e42014-03-04 15:51:58 -0800322 public static void cancelAndResendNotificationsOnLocaleChange(Context context) {
323 LogUtils.d(LOG_TAG, "cancelAndResendNotificationsOnLocaleChange");
324 sBidiFormatter = BidiFormatter.getInstance();
Andrew Sappersteinddd17bc2013-04-22 14:03:40 -0700325 resendNotifications(context, true, null, null);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800326 }
327
328 /**
329 * Get all notifications for all accounts, optionally cancel them, and repost.
Andrew Sappersteinddd17bc2013-04-22 14:03:40 -0700330 * This happens when locale changes. If you only want to resend messages from one
331 * account-folder pair, pass in the account and folder that should be resent.
332 * All other account-folder pairs will not have their notifications resent.
333 * All notifications will be resent if account or folder is null.
334 *
335 * @param context Current context.
336 * @param cancelExisting True, if all notifications should be canceled before resending.
337 * False, otherwise.
338 * @param accountUri The {@link Uri} of the {@link Account} of the notification
339 * upon which an action occurred.
340 * @param folderUri The {@link Uri} of the {@link Folder} of the notification
341 * upon which an action occurred.
342 */
343 public static void resendNotifications(Context context, final boolean cancelExisting,
Scott Kennedy259df5b2013-07-11 13:24:01 -0700344 final Uri accountUri, final FolderUri folderUri) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700345 LogUtils.d(LOG_TAG, "resendNotifications ");
Scott Kennedy2638b482013-05-06 16:05:11 -0700346
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800347 if (cancelExisting) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700348 LogUtils.d(LOG_TAG, "resendNotifications - cancelling all");
Alan Lau15490232014-03-06 14:53:14 -0800349 NotificationManagerCompat nm = NotificationManagerCompat.from(context);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800350 nm.cancelAll();
351 }
352 // Re-validate the notifications.
353 final NotificationMap notificationMap = getNotificationMap(context);
354 final Set<NotificationKey> keys = notificationMap.keySet();
355 for (NotificationKey notification : keys) {
356 final Folder folder = notification.folder;
Tony Mantlerb90a8132013-10-03 12:35:44 -0700357 final int notificationId =
358 getNotificationId(notification.account.getAccountManagerAccount(), folder);
Andrew Sappersteinddd17bc2013-04-22 14:03:40 -0700359
360 // Only resend notifications if the notifications are from the same folder
361 // and same account as the undo notification that was previously displayed.
Scott Kennedy26b29ef2013-04-28 18:36:50 -0700362 if (accountUri != null && !Objects.equal(accountUri, notification.account.uri) &&
Scott Kennedy259df5b2013-07-11 13:24:01 -0700363 folderUri != null && !Objects.equal(folderUri, folder.folderUri)) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700364 LogUtils.d(LOG_TAG, "resendNotifications - not resending %s / %s"
Scott Kennedy2638b482013-05-06 16:05:11 -0700365 + " because it doesn't match %s / %s",
Scott Kennedy259df5b2013-07-11 13:24:01 -0700366 notification.account.uri, folder.folderUri, accountUri, folderUri);
Andrew Sappersteinddd17bc2013-04-22 14:03:40 -0700367 continue;
368 }
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800369
Scott Kennedy2f97af92013-07-30 10:42:34 -0700370 LogUtils.d(LOG_TAG, "resendNotifications - resending %s / %s",
Scott Kennedy259df5b2013-07-11 13:24:01 -0700371 notification.account.uri, folder.folderUri);
Scott Kennedy2638b482013-05-06 16:05:11 -0700372
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800373 final NotificationAction undoableAction =
374 NotificationActionUtils.sUndoNotifications.get(notificationId);
375 if (undoableAction == null) {
Andrew Sappersteinddd17bc2013-04-22 14:03:40 -0700376 validateNotifications(context, folder, notification.account, true,
377 false, notification);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800378 } else {
379 // Create an undo notification
380 NotificationActionUtils.createUndoNotification(context, undoableAction);
381 }
382 }
383 }
384
385 /**
386 * Validate the notifications for the specified account.
387 */
388 public static void validateAccountNotifications(Context context, String account) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700389 LogUtils.d(LOG_TAG, "validateAccountNotifications - %s", account);
Scott Kennedy4162f832013-05-06 17:37:55 -0700390
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800391 List<NotificationKey> notificationsToCancel = Lists.newArrayList();
392 // Iterate through the notification map to see if there are any entries that correspond to
393 // labels that are not in the sync set.
394 final NotificationMap notificationMap = getNotificationMap(context);
395 Set<NotificationKey> keys = notificationMap.keySet();
396 final AccountPreferences accountPreferences = new AccountPreferences(context, account);
397 final boolean enabled = accountPreferences.areNotificationsEnabled();
398 if (!enabled) {
399 // Cancel all notifications for this account
400 for (NotificationKey notification : keys) {
Tony Mantlerb90a8132013-10-03 12:35:44 -0700401 if (notification.account.getAccountManagerAccount().name.equals(account)) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800402 notificationsToCancel.add(notification);
403 }
404 }
405 } else {
406 // Iterate through the notification map to see if there are any entries that
407 // correspond to labels that are not in the notification set.
408 for (NotificationKey notification : keys) {
Tony Mantlerb90a8132013-10-03 12:35:44 -0700409 if (notification.account.getAccountManagerAccount().name.equals(account)) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800410 // If notification is not enabled for this label, remember this NotificationKey
411 // to later cancel the notification, and remove the entry from the map
412 final Folder folder = notification.folder;
Scott Kennedy259df5b2013-07-11 13:24:01 -0700413 final boolean isInbox = folder.folderUri.equals(
414 notification.account.settings.defaultInbox);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800415 final FolderPreferences folderPreferences = new FolderPreferences(
Tony Mantlerafb10d02013-10-11 16:44:32 -0700416 context, notification.account.getEmailAddress(), folder, isInbox);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800417
418 if (!folderPreferences.areNotificationsEnabled()) {
419 notificationsToCancel.add(notification);
420 }
421 }
422 }
423 }
424
425 // Cancel & remove the invalid notifications.
426 if (notificationsToCancel.size() > 0) {
427 NotificationManager nm = (NotificationManager) context.getSystemService(
428 Context.NOTIFICATION_SERVICE);
429 for (NotificationKey notification : notificationsToCancel) {
430 final Folder folder = notification.folder;
Tony Mantlerb90a8132013-10-03 12:35:44 -0700431 final int notificationId =
432 getNotificationId(notification.account.getAccountManagerAccount(), folder);
Scott Kennedy2f97af92013-07-30 10:42:34 -0700433 LogUtils.d(LOG_TAG, "validateAccountNotifications - cancelling %s / %s",
Tony Mantler26a20752014-02-28 16:44:24 -0800434 notification.account.getEmailAddress(), folder.persistentId);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800435 nm.cancel(notificationId);
436 notificationMap.remove(notification);
437 NotificationActionUtils.sUndoNotifications.remove(notificationId);
438 NotificationActionUtils.sNotificationTimestamps.delete(notificationId);
439 }
440 notificationMap.saveNotificationMap(context);
441 }
442 }
443
444 /**
445 * Display only one notification.
446 */
447 public static void setNewEmailIndicator(Context context, final int unreadCount,
448 final int unseenCount, final Account account, final Folder folder,
449 final boolean getAttention) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700450 LogUtils.d(LOG_TAG, "setNewEmailIndicator unreadCount = %d, unseenCount = %d, account = %s,"
Tony Mantler26a20752014-02-28 16:44:24 -0800451 + " folder = %s, getAttention = %b", unreadCount, unseenCount,
452 account.getEmailAddress(), folder.folderUri, getAttention);
Scott Kennedy4162f832013-05-06 17:37:55 -0700453
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800454 boolean ignoreUnobtrusiveSetting = false;
455
Tony Mantlerb90a8132013-10-03 12:35:44 -0700456 final int notificationId = getNotificationId(account.getAccountManagerAccount(), folder);
Scott Kennedydab8a942013-02-22 12:31:30 -0800457
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800458 // Update the notification map
459 final NotificationMap notificationMap = getNotificationMap(context);
460 final NotificationKey key = new NotificationKey(account, folder);
461 if (unreadCount == 0) {
Tony Mantler26a20752014-02-28 16:44:24 -0800462 LogUtils.d(LOG_TAG, "setNewEmailIndicator - cancelling %s / %s",
463 account.getEmailAddress(), folder.persistentId);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800464 notificationMap.remove(key);
Scott Kennedydab8a942013-02-22 12:31:30 -0800465 ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
466 .cancel(notificationId);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800467 } else {
Paul Westbrookb1b855b2013-09-27 14:08:18 -0700468 LogUtils.d(LOG_TAG, "setNewEmailIndicator - update count for: %s / %s " +
Tony Mantler26a20752014-02-28 16:44:24 -0800469 "to: unread: %d unseen %d", account.getEmailAddress(), folder.persistentId,
Paul Westbrookb1b855b2013-09-27 14:08:18 -0700470 unreadCount, unseenCount);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800471 if (!notificationMap.containsKey(key)) {
472 // This account previously didn't have any unread mail; ignore the "unobtrusive
473 // notifications" setting and play sound and/or vibrate the device even if a
474 // notification already exists (bug 2412348).
Paul Westbrookb1b855b2013-09-27 14:08:18 -0700475 LogUtils.d(LOG_TAG, "setNewEmailIndicator - ignoringUnobtrusiveSetting");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800476 ignoreUnobtrusiveSetting = true;
477 }
478 notificationMap.put(key, unreadCount, unseenCount);
479 }
480 notificationMap.saveNotificationMap(context);
481
482 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700483 LogUtils.v(LOG_TAG, "New email: %s mapSize: %d getAttention: %b",
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800484 createNotificationString(notificationMap), notificationMap.size(),
485 getAttention);
486 }
487
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800488 if (NotificationActionUtils.sUndoNotifications.get(notificationId) == null) {
489 validateNotifications(context, folder, account, getAttention, ignoreUnobtrusiveSetting,
490 key);
491 }
492 }
493
494 /**
495 * Validate the notifications notification.
496 */
497 private static void validateNotifications(Context context, final Folder folder,
498 final Account account, boolean getAttention, boolean ignoreUnobtrusiveSetting,
499 NotificationKey key) {
500
Alan Lau15490232014-03-06 14:53:14 -0800501 NotificationManagerCompat nm = NotificationManagerCompat.from(context);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800502
503 final NotificationMap notificationMap = getNotificationMap(context);
504 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700505 LogUtils.i(LOG_TAG, "Validating Notification: %s mapSize: %d "
Paul Westbrookb1b855b2013-09-27 14:08:18 -0700506 + "folder: %s getAttention: %b ignoreUnobtrusive: %b",
507 createNotificationString(notificationMap),
508 notificationMap.size(), folder.name, getAttention, ignoreUnobtrusiveSetting);
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700509 } else {
510 LogUtils.i(LOG_TAG, "Validating Notification, mapSize: %d "
Paul Westbrookb1b855b2013-09-27 14:08:18 -0700511 + "getAttention: %b ignoreUnobtrusive: %b", notificationMap.size(),
512 getAttention, ignoreUnobtrusiveSetting);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800513 }
514 // The number of unread messages for this account and label.
515 final Integer unread = notificationMap.getUnread(key);
516 final int unreadCount = unread != null ? unread.intValue() : 0;
517 final Integer unseen = notificationMap.getUnseen(key);
518 int unseenCount = unseen != null ? unseen.intValue() : 0;
519
520 Cursor cursor = null;
521
522 try {
523 final Uri.Builder uriBuilder = folder.conversationListUri.buildUpon();
524 uriBuilder.appendQueryParameter(
525 UIProvider.SEEN_QUERY_PARAMETER, Boolean.FALSE.toString());
Andy Huang0be92432013-03-22 18:31:44 -0700526 // Do not allow this quick check to disrupt any active network-enabled conversation
527 // cursor.
528 uriBuilder.appendQueryParameter(
529 UIProvider.ConversationListQueryParameters.USE_NETWORK,
530 Boolean.FALSE.toString());
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800531 cursor = context.getContentResolver().query(uriBuilder.build(),
532 UIProvider.CONVERSATION_PROJECTION, null, null, null);
Yu Ping Hu3a7b45b2013-06-18 18:10:50 -0700533 if (cursor == null) {
534 // This folder doesn't exist.
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700535 LogUtils.i(LOG_TAG,
536 "The cursor is null, so the specified folder probably does not exist");
Yu Ping Hu3a7b45b2013-06-18 18:10:50 -0700537 clearFolderNotification(context, account, folder, false);
538 return;
539 }
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800540 final int cursorUnseenCount = cursor.getCount();
541
542 // Make sure the unseen count matches the number of items in the cursor. But, we don't
543 // want to overwrite a 0 unseen count that was specified in the intent
544 if (unseenCount != 0 && unseenCount != cursorUnseenCount) {
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700545 LogUtils.i(LOG_TAG,
Scott Kennedy2f97af92013-07-30 10:42:34 -0700546 "Unseen count doesn't match cursor count. unseen: %d cursor count: %d",
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800547 unseenCount, cursorUnseenCount);
548 unseenCount = cursorUnseenCount;
549 }
550
551 // For the purpose of the notifications, the unseen count should be capped at the num of
552 // unread conversations.
553 if (unseenCount > unreadCount) {
554 unseenCount = unreadCount;
555 }
556
Tony Mantlerb90a8132013-10-03 12:35:44 -0700557 final int notificationId =
558 getNotificationId(account.getAccountManagerAccount(), folder);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800559
Alan Lau15490232014-03-06 14:53:14 -0800560 NotificationKey notificationKey = new NotificationKey(account, folder);
561
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800562 if (unseenCount == 0) {
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700563 LogUtils.i(LOG_TAG, "validateNotifications - cancelling account %s / folder %s",
Tony Mantler26a20752014-02-28 16:44:24 -0800564 LogUtils.sanitizeName(LOG_TAG, account.getEmailAddress()),
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700565 LogUtils.sanitizeName(LOG_TAG, folder.persistentId));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800566 nm.cancel(notificationId);
Alan Lau15490232014-03-06 14:53:14 -0800567 cancelChildNotifications(notificationKey, nm);
568
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800569 return;
570 }
571
572 // We now have all we need to create the notification and the pending intent
573 PendingIntent clickIntent;
574
575 NotificationCompat.Builder notification = new NotificationCompat.Builder(context);
Alan Lau15490232014-03-06 14:53:14 -0800576 WearableNotifications.Builder wearableNotification =
577 new WearableNotifications.Builder(notification);
578 Map<Integer, WearableNotifications.Builder> msgNotifications =
579 new ArrayMap<Integer, WearableNotifications.Builder>();
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800580 notification.setSmallIcon(R.drawable.stat_notify_email);
Tony Mantler26a20752014-02-28 16:44:24 -0800581 notification.setTicker(account.getDisplayName());
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800582
583 final long when;
584
585 final long oldWhen =
586 NotificationActionUtils.sNotificationTimestamps.get(notificationId);
587 if (oldWhen != 0) {
588 when = oldWhen;
589 } else {
590 when = System.currentTimeMillis();
591 }
592
593 notification.setWhen(when);
594
595 // The timestamp is now stored in the notification, so we can remove it from here
596 NotificationActionUtils.sNotificationTimestamps.delete(notificationId);
597
598 // Dispatch a CLEAR_NEW_MAIL_NOTIFICATIONS intent if the user taps the "X" next to a
599 // notification. Also this intent gets fired when the user taps on a notification as
600 // the AutoCancel flag has been set
601 final Intent cancelNotificationIntent =
602 new Intent(MailIntentService.ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS);
603 cancelNotificationIntent.setPackage(context.getPackageName());
Scott Kennedy09400ef2013-03-07 11:09:47 -0800604 cancelNotificationIntent.setData(Utils.appendVersionQueryParameter(context,
Scott Kennedy259df5b2013-07-11 13:24:01 -0700605 folder.folderUri.fullUri));
Scott Kennedy48cfe462013-04-10 11:32:02 -0700606 cancelNotificationIntent.putExtra(Utils.EXTRA_ACCOUNT, account);
607 cancelNotificationIntent.putExtra(Utils.EXTRA_FOLDER, folder);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800608
609 notification.setDeleteIntent(PendingIntent.getService(
610 context, notificationId, cancelNotificationIntent, 0));
611
612 // Ensure that the notification is cleared when the user selects it
613 notification.setAutoCancel(true);
614
615 boolean eventInfoConfigured = false;
616
Scott Kennedy259df5b2013-07-11 13:24:01 -0700617 final boolean isInbox = folder.folderUri.equals(account.settings.defaultInbox);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800618 final FolderPreferences folderPreferences =
Tony Mantlerafb10d02013-10-11 16:44:32 -0700619 new FolderPreferences(context, account.getEmailAddress(), folder, isInbox);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800620
621 if (isInbox) {
622 final AccountPreferences accountPreferences =
Tony Mantlerafb10d02013-10-11 16:44:32 -0700623 new AccountPreferences(context, account.getEmailAddress());
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800624 moveNotificationSetting(accountPreferences, folderPreferences);
625 }
626
627 if (!folderPreferences.areNotificationsEnabled()) {
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700628 LogUtils.i(LOG_TAG, "Notifications are disabled for this folder; not notifying");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800629 // Don't notify
630 return;
631 }
632
633 if (unreadCount > 0) {
634 // How can I order this properly?
635 if (cursor.moveToNext()) {
Scott Kennedybd8acad2013-05-29 10:22:25 -0700636 final Intent notificationIntent;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800637
Scott Kennedybd8acad2013-05-29 10:22:25 -0700638 // Launch directly to the conversation, if there is only 1 unseen conversation
Andy Huang4fe0af82013-08-20 17:24:51 -0700639 final boolean launchConversationMode = (unseenCount == 1);
640 if (launchConversationMode) {
Scott Kennedy09400ef2013-03-07 11:09:47 -0800641 notificationIntent = createViewConversationIntent(context, account, folder,
642 cursor);
Scott Kennedybd8acad2013-05-29 10:22:25 -0700643 } else {
644 notificationIntent = createViewConversationIntent(context, account, folder,
645 null);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800646 }
647
Andy Huang4fe0af82013-08-20 17:24:51 -0700648 Analytics.getInstance().sendEvent("notification_create",
649 launchConversationMode ? "conversation" : "conversation_list",
650 folder.getTypeDescription(), unseenCount);
651
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800652 if (notificationIntent == null) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700653 LogUtils.e(LOG_TAG, "Null intent when building notification");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800654 return;
655 }
656
Alan Lauf1f80e62014-04-16 17:33:07 -0700657 clickIntent = createClickPendingIntent(context, notificationIntent);
Andy Huang4fe0af82013-08-20 17:24:51 -0700658
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800659 configureLatestEventInfoFromConversation(context, account, folderPreferences,
Alan Lau15490232014-03-06 14:53:14 -0800660 notification, wearableNotification, msgNotifications, notificationId,
661 cursor, clickIntent, notificationIntent, unreadCount, unseenCount,
662 folder, when);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800663 eventInfoConfigured = true;
664 }
665 }
666
667 final boolean vibrate = folderPreferences.isNotificationVibrateEnabled();
668 final String ringtoneUri = folderPreferences.getNotificationRingtoneUri();
669 final boolean notifyOnce = !folderPreferences.isEveryMessageNotificationEnabled();
670
Scott Kennedyff8553f2013-04-05 20:57:44 -0700671 if (!ignoreUnobtrusiveSetting && notifyOnce) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800672 // If the user has "unobtrusive notifications" enabled, only alert the first time
673 // new mail is received in this account. This is the default behavior. See
674 // bugs 2412348 and 2413490.
Paul Westbrookb1b855b2013-09-27 14:08:18 -0700675 LogUtils.d(LOG_TAG, "Setting Alert Once");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800676 notification.setOnlyAlertOnce(true);
677 }
678
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700679 LogUtils.i(LOG_TAG, "Account: %s vibrate: %s",
Tony Mantler26a20752014-02-28 16:44:24 -0800680 LogUtils.sanitizeName(LOG_TAG, account.getEmailAddress()),
Scott Kennedyff8553f2013-04-05 20:57:44 -0700681 Boolean.toString(folderPreferences.isNotificationVibrateEnabled()));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800682
683 int defaults = 0;
684
Alan Lau15490232014-03-06 14:53:14 -0800685 // Check if any current child notifications exist previously. Only notify if one of
686 // them is new.
687 boolean hasNewChildNotification;
688 Set<Integer> prevChildNotifications = sChildNotificationsMap.get(notificationKey);
689 if (prevChildNotifications != null) {
690 hasNewChildNotification = false;
691 for (Integer currentNotificationId : msgNotifications.keySet()) {
692 if (!prevChildNotifications.contains(currentNotificationId)) {
693 hasNewChildNotification = true;
694 break;
695 }
696 }
697 } else {
698 hasNewChildNotification = true;
699 }
700
701 LogUtils.d(LOG_TAG, "getAttention=%s,oldWhen=%s,hasNewChildNotification=%s",
702 getAttention, oldWhen, hasNewChildNotification);
703
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800704 /*
705 * We do not want to notify if this is coming back from an Undo notification, hence the
706 * oldWhen check.
707 */
Alan Lau15490232014-03-06 14:53:14 -0800708 if (getAttention && oldWhen == 0 && hasNewChildNotification) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800709 final AccountPreferences accountPreferences =
Tony Mantlercfb969b2013-11-25 09:45:12 -0800710 new AccountPreferences(context, account.getEmailAddress());
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800711 if (accountPreferences.areNotificationsEnabled()) {
712 if (vibrate) {
713 defaults |= Notification.DEFAULT_VIBRATE;
714 }
715
716 notification.setSound(TextUtils.isEmpty(ringtoneUri) ? null
717 : Uri.parse(ringtoneUri));
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700718 LogUtils.i(LOG_TAG, "New email in %s vibrateWhen: %s, playing notification: %s",
Tony Mantler26a20752014-02-28 16:44:24 -0800719 LogUtils.sanitizeName(LOG_TAG, account.getEmailAddress()), vibrate,
720 ringtoneUri);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800721 }
722 }
723
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700724 // TODO(skennedy) Why do we do any of the above if we're just going to bail here?
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800725 if (eventInfoConfigured) {
726 defaults |= Notification.DEFAULT_LIGHTS;
727 notification.setDefaults(defaults);
728
729 if (oldWhen != 0) {
730 // We do not want to display the ticker again if we are re-displaying this
731 // notification (like from an Undo notification)
732 notification.setTicker(null);
733 }
734
Alan Lau15490232014-03-06 14:53:14 -0800735 nm.notify(notificationId, wearableNotification.build());
736
737 if (prevChildNotifications != null) {
738 Set<Integer> currentNotificationIds = msgNotifications.keySet();
739 for (Integer prevChildNotificationId : prevChildNotifications) {
740 if (!currentNotificationIds.contains(prevChildNotificationId)) {
741 nm.cancel(prevChildNotificationId);
742 LogUtils.d(LOG_TAG, "canceling child notification %s",
743 prevChildNotificationId);
744 }
745 }
746 }
747
748 for (Map.Entry<Integer, WearableNotifications.Builder> entry
749 : msgNotifications.entrySet()) {
750 nm.notify(entry.getKey(), entry.getValue().build());
751 LogUtils.d(LOG_TAG, "notifying child notification %s", entry.getKey());
752 }
753
754 Set<Integer> childNotificationIds = new HashSet<Integer>();
755 childNotificationIds.addAll(msgNotifications.keySet());
756 sChildNotificationsMap.put(notificationKey, childNotificationIds);
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700757 } else {
758 LogUtils.i(LOG_TAG, "event info not configured - not notifying");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800759 }
760 } finally {
761 if (cursor != null) {
762 cursor.close();
763 }
764 }
765 }
766
Alan Lauf1f80e62014-04-16 17:33:07 -0700767 private static PendingIntent createClickPendingIntent(Context context,
768 Intent notificationIntent) {
769 // Amend the click intent with a hint that its source was a notification,
770 // but remove the hint before it's used to generate notification action
771 // intents. This prevents the following sequence:
772 // 1. generate single notification
773 // 2. user clicks reply, then completes Compose activity
774 // 3. main activity launches, gets FROM_NOTIFICATION hint in intent
775 notificationIntent.putExtra(Utils.EXTRA_FROM_NOTIFICATION, true);
776 PendingIntent clickIntent = PendingIntent.getActivity(context, -1, notificationIntent,
777 PendingIntent.FLAG_UPDATE_CURRENT);
778 notificationIntent.removeExtra(Utils.EXTRA_FROM_NOTIFICATION);
779 return clickIntent;
780 }
781
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800782 /**
783 * @return an {@link Intent} which, if launched, will display the corresponding conversation
784 */
Scott Kennedy09400ef2013-03-07 11:09:47 -0800785 private static Intent createViewConversationIntent(final Context context, final Account account,
786 final Folder folder, final Cursor cursor) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800787 if (folder == null || account == null) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700788 LogUtils.e(LOG_TAG, "createViewConversationIntent(): "
Scott Kennedy13a73272013-05-09 09:45:03 -0700789 + "Null account or folder. account: %s folder: %s", account, folder);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800790 return null;
791 }
792
793 final Intent intent;
794
795 if (cursor == null) {
Scott Kennedy259df5b2013-07-11 13:24:01 -0700796 intent = Utils.createViewFolderIntent(context, folder.folderUri.fullUri, account);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800797 } else {
798 // A conversation cursor has been specified, so this intent is intended to be go
799 // directly to the one new conversation
800
801 // Get the Conversation object
802 final Conversation conversation = new Conversation(cursor);
Scott Kennedy259df5b2013-07-11 13:24:01 -0700803 intent = Utils.createViewConversationIntent(context, conversation,
804 folder.folderUri.fullUri, account);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800805 }
806
807 return intent;
808 }
809
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800810 private static Bitmap getDefaultNotificationIcon(
811 final Context context, final Folder folder, final boolean multipleNew) {
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700812 final int resId;
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800813 if (folder.notificationIconResId != 0) {
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700814 resId = folder.notificationIconResId;
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800815 } else if (multipleNew) {
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700816 resId = R.drawable.ic_notification_multiple_mail_holo_dark;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800817 } else {
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700818 resId = R.drawable.ic_contact_picture;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800819 }
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700820
821 final Bitmap icon = getIcon(context, resId);
822
823 if (icon == null) {
824 LogUtils.e(LOG_TAG, "Couldn't decode notif icon res id %d", resId);
825 }
826
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800827 return icon;
828 }
829
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800830 private static Bitmap getIcon(final Context context, final int resId) {
831 final Bitmap cachedIcon = sNotificationIcons.get(resId);
832 if (cachedIcon != null) {
833 return cachedIcon;
834 }
835
836 final Bitmap icon = BitmapFactory.decodeResource(context.getResources(), resId);
837 sNotificationIcons.put(resId, icon);
838
839 return icon;
840 }
841
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800842 private static void configureLatestEventInfoFromConversation(final Context context,
843 final Account account, final FolderPreferences folderPreferences,
Alan Lau15490232014-03-06 14:53:14 -0800844 final NotificationCompat.Builder notification,
845 final WearableNotifications.Builder summaryWearNotif,
846 final Map<Integer, WearableNotifications.Builder> msgNotifications,
847 final int summaryNotificationId, final Cursor conversationCursor,
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800848 final PendingIntent clickIntent, final Intent notificationIntent,
Tony Mantlerb90a8132013-10-03 12:35:44 -0700849 final int unreadCount, final int unseenCount,
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800850 final Folder folder, final long when) {
851 final Resources res = context.getResources();
Tony Mantler26a20752014-02-28 16:44:24 -0800852 final String notificationAccountDisplayName = account.getDisplayName();
853 final String notificationAccountEmail = account.getEmailAddress();
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800854
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700855 LogUtils.i(LOG_TAG, "Showing notification with unreadCount of %d and unseenCount of %d",
Scott Kennedy2f97af92013-07-30 10:42:34 -0700856 unreadCount, unseenCount);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800857
858 String notificationTicker = null;
859
860 // Boolean indicating that this notification is for a non-inbox label.
Scott Kennedy259df5b2013-07-11 13:24:01 -0700861 final boolean isInbox = folder.folderUri.fullUri.equals(account.settings.defaultInbox);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800862
863 // Notification label name for user label notifications.
864 final String notificationLabelName = isInbox ? null : folder.name;
865
866 if (unseenCount > 1) {
867 // Build the string that describes the number of new messages
868 final String newMessagesString = res.getString(R.string.new_messages, unseenCount);
869
870 // Use the default notification icon
871 notification.setLargeIcon(
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800872 getDefaultNotificationIcon(context, folder, true /* multiple new messages */));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800873
874 // The ticker initially start as the new messages string.
875 notificationTicker = newMessagesString;
876
877 // The title of the notification is the new messages string
878 notification.setContentTitle(newMessagesString);
879
Scott Kennedyc8bc5d12013-04-25 08:46:06 -0700880 // TODO(skennedy) Can we remove this check?
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800881 if (com.android.mail.utils.Utils.isRunningJellybeanOrLater()) {
882 // For a new-style notification
883 final int maxNumDigestItems = context.getResources().getInteger(
884 R.integer.max_num_notification_digest_items);
885
886 // The body of the notification is the account name, or the label name.
Tony Mantler26a20752014-02-28 16:44:24 -0800887 notification.setSubText(
888 isInbox ? notificationAccountDisplayName : notificationLabelName);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800889
890 final NotificationCompat.InboxStyle digest =
891 new NotificationCompat.InboxStyle(notification);
892
Alan Lau15490232014-03-06 14:53:14 -0800893 // Group by account.
894 String notificationGroupKey =
895 account.uri.toString() + "/" + folder.folderUri.fullUri;
896 summaryWearNotif.setGroup(notificationGroupKey,
897 WearableNotifications.GROUP_ORDER_SUMMARY);
898
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800899 int numDigestItems = 0;
900 do {
901 final Conversation conversation = new Conversation(conversationCursor);
902
903 if (!conversation.read) {
904 boolean multipleUnreadThread = false;
905 // TODO(cwren) extract this pattern into a helper
906
907 Cursor cursor = null;
908 MessageCursor messageCursor = null;
909 try {
910 final Uri.Builder uriBuilder = conversation.messageListUri.buildUpon();
911 uriBuilder.appendQueryParameter(
912 UIProvider.LABEL_QUERY_PARAMETER, notificationLabelName);
913 cursor = context.getContentResolver().query(uriBuilder.build(),
914 UIProvider.MESSAGE_PROJECTION, null, null, null);
915 messageCursor = new MessageCursor(cursor);
916
Yu Ping Hub18d75a2013-03-12 17:04:58 -0700917 String from = "";
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800918 String fromAddress = "";
919 if (messageCursor.moveToPosition(messageCursor.getCount() - 1)) {
920 final Message message = messageCursor.getMessage();
921 fromAddress = message.getFrom();
Yu Ping Hub18d75a2013-03-12 17:04:58 -0700922 if (fromAddress == null) {
923 fromAddress = "";
924 }
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800925 from = getDisplayableSender(fromAddress);
926 }
927 while (messageCursor.moveToPosition(messageCursor.getPosition() - 1)) {
928 final Message message = messageCursor.getMessage();
929 if (!message.read &&
930 !fromAddress.contentEquals(message.getFrom())) {
931 multipleUnreadThread = true;
932 break;
933 }
934 }
935 final SpannableStringBuilder sendersBuilder;
936 if (multipleUnreadThread) {
937 final int sendersLength =
938 res.getInteger(R.integer.swipe_senders_length);
939
940 sendersBuilder = getStyledSenders(context, conversationCursor,
Tony Mantler26a20752014-02-28 16:44:24 -0800941 sendersLength, notificationAccountEmail);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800942 } else {
Paul Westbrook12fb02b2013-08-27 14:34:49 -0700943 sendersBuilder =
944 new SpannableStringBuilder(getWrappedFromString(from));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800945 }
946 final CharSequence digestLine = getSingleMessageInboxLine(context,
947 sendersBuilder.toString(),
948 conversation.subject,
Tony Mantleredd6c1a2013-10-08 14:47:43 -0700949 conversation.getSnippet());
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800950 digest.addLine(digestLine);
951 numDigestItems++;
Alan Lau15490232014-03-06 14:53:14 -0800952
953 // Adding child notification for Wear.
954 NotificationCompat.Builder childNotif =
955 new NotificationCompat.Builder(context);
956 childNotif.setSmallIcon(R.drawable.stat_notify_email);
957 childNotif.setContentText(digestLine);
Alan Lauf1f80e62014-04-16 17:33:07 -0700958 Intent childNotificationIntent = createViewConversationIntent(context,
959 account, folder, conversationCursor);
960 PendingIntent childClickIntent = createClickPendingIntent(context,
961 childNotificationIntent);
962 childNotif.setContentIntent(childClickIntent);
Alan Lau15490232014-03-06 14:53:14 -0800963
964 WearableNotifications.Builder childWearNotif =
965 new WearableNotifications.Builder(childNotif).setGroup(
966 notificationGroupKey, numDigestItems);
967 int childNotificationId = getNotificationId(summaryNotificationId,
968 conversation.hashCode());
969
970 configureNotifForOneConversation(context, account, folderPreferences,
971 childNotif, childWearNotif, conversationCursor,
972 notificationIntent, folder, when, res,
973 notificationAccountDisplayName, notificationAccountEmail,
974 isInbox, notificationLabelName, childNotificationId);
975 msgNotifications.put(childNotificationId, childWearNotif);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800976 } finally {
977 if (messageCursor != null) {
978 messageCursor.close();
979 }
980 if (cursor != null) {
981 cursor.close();
982 }
983 }
984 }
985 } while (numDigestItems <= maxNumDigestItems && conversationCursor.moveToNext());
986 } else {
987 // The body of the notification is the account name, or the label name.
988 notification.setContentText(
Tony Mantler26a20752014-02-28 16:44:24 -0800989 isInbox ? notificationAccountDisplayName : notificationLabelName);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800990 }
991 } else {
Alan Lau15490232014-03-06 14:53:14 -0800992 // For notifications for a single new conversation, we want to get the information
993 // from the conversation
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800994
995 // Move the cursor to the most recent unread conversation
996 seekToLatestUnreadConversation(conversationCursor);
997
Alan Lau15490232014-03-06 14:53:14 -0800998 notificationTicker = configureNotifForOneConversation(context, account,
999 folderPreferences, notification, summaryWearNotif, conversationCursor,
1000 notificationIntent, folder, when, res, notificationAccountDisplayName,
1001 notificationAccountEmail, isInbox, notificationLabelName,
1002 summaryNotificationId);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001003 }
1004
1005 // Build the notification ticker
1006 if (notificationLabelName != null && notificationTicker != null) {
1007 // This is a per label notification, format the ticker with that information
1008 notificationTicker = res.getString(R.string.label_notification_ticker,
1009 notificationLabelName, notificationTicker);
1010 }
1011
1012 if (notificationTicker != null) {
1013 // If we didn't generate a notification ticker, it will default to account name
1014 notification.setTicker(notificationTicker);
1015 }
1016
1017 // Set the number in the notification
1018 if (unreadCount > 1) {
1019 notification.setNumber(unreadCount);
1020 }
1021
1022 notification.setContentIntent(clickIntent);
1023 }
1024
Alan Lau15490232014-03-06 14:53:14 -08001025 /**
1026 * Configure the notification for one conversation. When there are multiple conversations,
1027 * this method is used to configure bundled notification for Android Wear.
1028 */
1029 private static String configureNotifForOneConversation(Context context, Account account,
1030 FolderPreferences folderPreferences, NotificationCompat.Builder notification,
1031 WearableNotifications.Builder summaryWearNotif, Cursor conversationCursor,
1032 Intent notificationIntent, Folder folder, long when, Resources res,
1033 String notificationAccountDisplayName, String notificationAccountEmail, boolean isInbox,
1034 String notificationLabelName, int notificationId) {
1035
1036 String notificationTicker;
1037
1038 final Conversation conversation = new Conversation(conversationCursor);
1039
1040 Cursor cursor = null;
1041 MessageCursor messageCursor = null;
1042 boolean multipleUnseenThread = false;
1043 String from = null;
1044 try {
1045 final Uri uri = conversation.messageListUri.buildUpon().appendQueryParameter(
1046 UIProvider.LABEL_QUERY_PARAMETER, folder.persistentId).build();
1047 cursor = context.getContentResolver().query(uri, UIProvider.MESSAGE_PROJECTION,
1048 null, null, null);
1049 messageCursor = new MessageCursor(cursor);
1050 // Use the information from the last sender in the conversation that triggered
1051 // this notification.
1052
1053 String fromAddress = "";
1054 if (messageCursor.moveToPosition(messageCursor.getCount() - 1)) {
1055 final Message message = messageCursor.getMessage();
1056 fromAddress = message.getFrom();
1057 from = getDisplayableSender(fromAddress);
1058 notification.setLargeIcon(
1059 getContactIcon(context, from, getSenderAddress(fromAddress), folder));
1060 }
1061
1062 // Assume that the last message in this conversation is unread
1063 int firstUnseenMessagePos = messageCursor.getPosition();
1064 while (messageCursor.moveToPosition(messageCursor.getPosition() - 1)) {
1065 final Message message = messageCursor.getMessage();
1066 final boolean unseen = !message.seen;
1067 if (unseen) {
1068 firstUnseenMessagePos = messageCursor.getPosition();
1069 if (!multipleUnseenThread
1070 && !fromAddress.contentEquals(message.getFrom())) {
1071 multipleUnseenThread = true;
1072 }
1073 }
1074 }
1075
1076 // TODO(skennedy) Can we remove this check?
1077 if (Utils.isRunningJellybeanOrLater()) {
1078 // For a new-style notification
1079
1080 if (multipleUnseenThread) {
1081 // The title of a single conversation is the list of senders.
1082 int sendersLength = res.getInteger(R.integer.swipe_senders_length);
1083
1084 final SpannableStringBuilder sendersBuilder = getStyledSenders(
1085 context, conversationCursor, sendersLength,
1086 notificationAccountEmail);
1087
1088 notification.setContentTitle(sendersBuilder);
1089 // For a single new conversation, the ticker is based on the sender's name.
1090 notificationTicker = sendersBuilder.toString();
1091 } else {
1092 from = getWrappedFromString(from);
1093 // The title of a single message the sender.
1094 notification.setContentTitle(from);
1095 // For a single new conversation, the ticker is based on the sender's name.
1096 notificationTicker = from;
1097 }
1098
1099 // The notification content will be the subject of the conversation.
1100 notification.setContentText(
1101 getSingleMessageLittleText(context, conversation.subject));
1102
1103 // The notification subtext will be the subject of the conversation for inbox
1104 // notifications, or will based on the the label name for user label
1105 // notifications.
1106 notification.setSubText(isInbox ?
1107 notificationAccountDisplayName : notificationLabelName);
1108
1109 if (multipleUnseenThread) {
1110 notification.setLargeIcon(
1111 getDefaultNotificationIcon(context, folder, true));
1112 }
1113 final NotificationCompat.BigTextStyle bigText =
1114 new NotificationCompat.BigTextStyle(notification);
1115
1116 // Seek the message cursor to the first unread message
1117 final Message message;
1118 if (messageCursor.moveToPosition(firstUnseenMessagePos)) {
1119 message = messageCursor.getMessage();
1120 bigText.bigText(getSingleMessageBigText(context,
1121 conversation.subject, message));
1122 } else {
1123 LogUtils.e(LOG_TAG, "Failed to load message");
1124 message = null;
1125 }
1126
1127 if (message != null) {
1128 final Set<String> notificationActions =
1129 folderPreferences.getNotificationActions(account);
1130
1131 NotificationActionUtils.addNotificationActions(context, notificationIntent,
1132 notification, summaryWearNotif, account, conversation, message,
1133 folder, notificationId, when, notificationActions);
1134 }
1135 } else {
1136 // For an old-style notification
1137
1138 // The title of a single conversation notification is built from both the sender
1139 // and subject of the new message.
1140 notification.setContentTitle(getSingleMessageNotificationTitle(context,
1141 from, conversation.subject));
1142
1143 // The notification content will be the subject of the conversation for inbox
1144 // notifications, or will based on the the label name for user label
1145 // notifications.
1146 notification.setContentText(
1147 isInbox ? notificationAccountDisplayName : notificationLabelName);
1148
1149 // For a single new conversation, the ticker is based on the sender's name.
1150 notificationTicker = from;
1151 }
1152 } finally {
1153 if (messageCursor != null) {
1154 messageCursor.close();
1155 }
1156 if (cursor != null) {
1157 cursor.close();
1158 }
1159 }
1160 return notificationTicker;
1161 }
1162
Paul Westbrook12fb02b2013-08-27 14:34:49 -07001163 private static String getWrappedFromString(String from) {
1164 if (from == null) {
1165 LogUtils.e(LOG_TAG, "null from string in getWrappedFromString");
1166 from = "";
1167 }
Andrew Sapperstein963c7e42014-03-04 15:51:58 -08001168 from = sBidiFormatter.unicodeWrap(from);
Paul Westbrook12fb02b2013-08-27 14:34:49 -07001169 return from;
1170 }
1171
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001172 private static SpannableStringBuilder getStyledSenders(final Context context,
1173 final Cursor conversationCursor, final int maxLength, final String account) {
1174 final Conversation conversation = new Conversation(conversationCursor);
1175 final com.android.mail.providers.ConversationInfo conversationInfo =
1176 conversation.conversationInfo;
1177 final ArrayList<SpannableString> senders = new ArrayList<SpannableString>();
1178 if (sNotificationUnreadStyleSpan == null) {
1179 sNotificationUnreadStyleSpan = new TextAppearanceSpan(
1180 context, R.style.NotificationSendersUnreadTextAppearance);
1181 sNotificationReadStyleSpan =
1182 new TextAppearanceSpan(context, R.style.NotificationSendersReadTextAppearance);
1183 }
1184 SendersView.format(context, conversationInfo, "", maxLength, senders, null, null, account,
James Lemieux10ea28a2014-03-27 14:33:58 -07001185 sNotificationUnreadStyleSpan, sNotificationReadStyleSpan,
1186 false /* showToHeader */, false /* resourceCachingRequired */);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001187
1188 return ellipsizeStyledSenders(context, senders);
1189 }
1190
1191 private static String sSendersSplitToken = null;
1192 private static String sElidedPaddingToken = null;
1193
1194 private static SpannableStringBuilder ellipsizeStyledSenders(final Context context,
1195 ArrayList<SpannableString> styledSenders) {
1196 if (sSendersSplitToken == null) {
1197 sSendersSplitToken = context.getString(R.string.senders_split_token);
1198 sElidedPaddingToken = context.getString(R.string.elided_padding_token);
1199 }
1200
1201 SpannableStringBuilder builder = new SpannableStringBuilder();
1202 SpannableString prevSender = null;
1203 for (SpannableString sender : styledSenders) {
1204 if (sender == null) {
Scott Kennedy2f97af92013-07-30 10:42:34 -07001205 LogUtils.e(LOG_TAG, "null sender iterating over styledSenders");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001206 continue;
1207 }
1208 CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class);
1209 if (SendersView.sElidedString.equals(sender.toString())) {
1210 prevSender = sender;
1211 sender = copyStyles(spans, sElidedPaddingToken + sender + sElidedPaddingToken);
1212 } else if (builder.length() > 0
1213 && (prevSender == null || !SendersView.sElidedString.equals(prevSender
1214 .toString()))) {
1215 prevSender = sender;
1216 sender = copyStyles(spans, sSendersSplitToken + sender);
1217 } else {
1218 prevSender = sender;
1219 }
1220 builder.append(sender);
1221 }
1222 return builder;
1223 }
1224
1225 private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) {
1226 SpannableString s = new SpannableString(newText);
1227 if (spans != null && spans.length > 0) {
1228 s.setSpan(spans[0], 0, s.length(), 0);
1229 }
1230 return s;
1231 }
1232
1233 /**
1234 * Seeks the cursor to the position of the most recent unread conversation. If no unread
1235 * conversation is found, the position of the cursor will be restored, and false will be
1236 * returned.
1237 */
1238 private static boolean seekToLatestUnreadConversation(final Cursor cursor) {
1239 final int initialPosition = cursor.getPosition();
1240 do {
1241 final Conversation conversation = new Conversation(cursor);
1242 if (!conversation.read) {
1243 return true;
1244 }
1245 } while (cursor.moveToNext());
1246
1247 // Didn't find an unread conversation, reset the position.
1248 cursor.moveToPosition(initialPosition);
1249 return false;
1250 }
1251
1252 /**
1253 * Sets the bigtext for a notification for a single new conversation
1254 *
1255 * @param context
1256 * @param senders Sender of the new message that triggered the notification.
1257 * @param subject Subject of the new message that triggered the notification
1258 * @param snippet Snippet of the new message that triggered the notification
1259 * @return a {@link CharSequence} suitable for use in
1260 * {@link android.support.v4.app.NotificationCompat.BigTextStyle}
1261 */
1262 private static CharSequence getSingleMessageInboxLine(Context context,
1263 String senders, String subject, String snippet) {
1264 // TODO(cwren) finish this step toward commmon code with getSingleMessageBigText
1265
1266 final String subjectSnippet = !TextUtils.isEmpty(subject) ? subject : snippet;
1267
1268 final TextAppearanceSpan notificationPrimarySpan =
1269 new TextAppearanceSpan(context, R.style.NotificationPrimaryText);
1270
1271 if (TextUtils.isEmpty(senders)) {
1272 // If the senders are empty, just use the subject/snippet.
1273 return subjectSnippet;
1274 } else if (TextUtils.isEmpty(subjectSnippet)) {
1275 // If the subject/snippet is empty, just use the senders.
1276 final SpannableString spannableString = new SpannableString(senders);
1277 spannableString.setSpan(notificationPrimarySpan, 0, senders.length(), 0);
1278
1279 return spannableString;
1280 } else {
1281 final String formatString = context.getResources().getString(
1282 R.string.multiple_new_message_notification_item);
1283 final TextAppearanceSpan notificationSecondarySpan =
1284 new TextAppearanceSpan(context, R.style.NotificationSecondaryText);
1285
Andrew Sapperstein8173cf42013-07-01 13:17:18 -07001286 // senders is already individually unicode wrapped so it does not need to be done here
Andrew Sappersteinf5810962013-06-27 10:41:33 -07001287 final String instantiatedString = String.format(formatString,
Andrew Sapperstein8173cf42013-07-01 13:17:18 -07001288 senders,
Andrew Sapperstein963c7e42014-03-04 15:51:58 -08001289 sBidiFormatter.unicodeWrap(subjectSnippet));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001290
1291 final SpannableString spannableString = new SpannableString(instantiatedString);
1292
1293 final boolean isOrderReversed = formatString.indexOf("%2$s") <
1294 formatString.indexOf("%1$s");
1295 final int primaryOffset =
1296 (isOrderReversed ? instantiatedString.lastIndexOf(senders) :
1297 instantiatedString.indexOf(senders));
1298 final int secondaryOffset =
1299 (isOrderReversed ? instantiatedString.lastIndexOf(subjectSnippet) :
1300 instantiatedString.indexOf(subjectSnippet));
1301 spannableString.setSpan(notificationPrimarySpan,
1302 primaryOffset, primaryOffset + senders.length(), 0);
1303 spannableString.setSpan(notificationSecondarySpan,
1304 secondaryOffset, secondaryOffset + subjectSnippet.length(), 0);
1305 return spannableString;
1306 }
1307 }
1308
1309 /**
1310 * Sets the bigtext for a notification for a single new conversation
1311 * @param context
1312 * @param subject Subject of the new message that triggered the notification
Andrew Sappersteinf5810962013-06-27 10:41:33 -07001313 * @return a {@link CharSequence} suitable for use in
1314 * {@link NotificationCompat.Builder#setContentText}
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001315 */
1316 private static CharSequence getSingleMessageLittleText(Context context, String subject) {
1317 final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
1318 context, R.style.NotificationPrimaryText);
1319
1320 final SpannableString spannableString = new SpannableString(subject);
1321 spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0);
1322
1323 return spannableString;
1324 }
1325
1326 /**
1327 * Sets the bigtext for a notification for a single new conversation
1328 *
1329 * @param context
1330 * @param subject Subject of the new message that triggered the notification
1331 * @param message the {@link Message} to be displayed.
1332 * @return a {@link CharSequence} suitable for use in
1333 * {@link android.support.v4.app.NotificationCompat.BigTextStyle}
1334 */
1335 private static CharSequence getSingleMessageBigText(Context context, String subject,
1336 final Message message) {
1337
1338 final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
1339 context, R.style.NotificationPrimaryText);
1340
1341 final String snippet = getMessageBodyWithoutElidedText(message);
1342
1343 // Change multiple newlines (with potential white space between), into a single new line
1344 final String collapsedSnippet =
1345 !TextUtils.isEmpty(snippet) ? snippet.replaceAll("\\n\\s+", "\n") : "";
1346
1347 if (TextUtils.isEmpty(subject)) {
1348 // If the subject is empty, just use the snippet.
1349 return snippet;
1350 } else if (TextUtils.isEmpty(collapsedSnippet)) {
1351 // If the snippet is empty, just use the subject.
1352 final SpannableString spannableString = new SpannableString(subject);
1353 spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0);
1354
1355 return spannableString;
1356 } else {
1357 final String notificationBigTextFormat = context.getResources().getString(
1358 R.string.single_new_message_notification_big_text);
1359
1360 // Localizers may change the order of the parameters, look at how the format
1361 // string is structured.
1362 final boolean isSubjectFirst = notificationBigTextFormat.indexOf("%2$s") >
1363 notificationBigTextFormat.indexOf("%1$s");
1364 final String bigText =
1365 String.format(notificationBigTextFormat, subject, collapsedSnippet);
1366 final SpannableString spannableString = new SpannableString(bigText);
1367
1368 final int subjectOffset =
1369 (isSubjectFirst ? bigText.indexOf(subject) : bigText.lastIndexOf(subject));
1370 spannableString.setSpan(notificationSubjectSpan,
1371 subjectOffset, subjectOffset + subject.length(), 0);
1372
1373 return spannableString;
1374 }
1375 }
1376
1377 /**
1378 * Gets the title for a notification for a single new conversation
1379 * @param context
1380 * @param sender Sender of the new message that triggered the notification.
1381 * @param subject Subject of the new message that triggered the notification
1382 * @return a {@link CharSequence} suitable for use as a {@link Notification} title.
1383 */
1384 private static CharSequence getSingleMessageNotificationTitle(Context context,
1385 String sender, String subject) {
1386
1387 if (TextUtils.isEmpty(subject)) {
1388 // If the subject is empty, just set the title to the sender's information.
1389 return sender;
1390 } else {
1391 final String notificationTitleFormat = context.getResources().getString(
1392 R.string.single_new_message_notification_title);
1393
1394 // Localizers may change the order of the parameters, look at how the format
1395 // string is structured.
1396 final boolean isSubjectLast = notificationTitleFormat.indexOf("%2$s") >
1397 notificationTitleFormat.indexOf("%1$s");
1398 final String titleString = String.format(notificationTitleFormat, sender, subject);
1399
1400 // Format the string so the subject is using the secondaryText style
1401 final SpannableString titleSpannable = new SpannableString(titleString);
1402
1403 // Find the offset of the subject.
1404 final int subjectOffset =
1405 isSubjectLast ? titleString.lastIndexOf(subject) : titleString.indexOf(subject);
1406 final TextAppearanceSpan notificationSubjectSpan =
1407 new TextAppearanceSpan(context, R.style.NotificationSecondaryText);
1408 titleSpannable.setSpan(notificationSubjectSpan,
1409 subjectOffset, subjectOffset + subject.length(), 0);
1410 return titleSpannable;
1411 }
1412 }
1413
1414 /**
Scott Kennedy7f8aed62013-05-22 18:35:17 -07001415 * Clears the notifications for the specified account/folder.
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001416 */
Scott Kennedy7f8aed62013-05-22 18:35:17 -07001417 public static void clearFolderNotification(Context context, Account account, Folder folder,
1418 final boolean markSeen) {
Tony Mantler26a20752014-02-28 16:44:24 -08001419 LogUtils.v(LOG_TAG, "Clearing all notifications for %s/%s", account.getEmailAddress(),
1420 folder.name);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001421 final NotificationMap notificationMap = getNotificationMap(context);
1422 final NotificationKey key = new NotificationKey(account, folder);
1423 notificationMap.remove(key);
1424 notificationMap.saveNotificationMap(context);
1425
Alan Lau15490232014-03-06 14:53:14 -08001426 final NotificationManagerCompat notificationManager =
1427 NotificationManagerCompat.from(context);
Tony Mantlerb90a8132013-10-03 12:35:44 -07001428 notificationManager.cancel(getNotificationId(account.getAccountManagerAccount(), folder));
Scott Kennedy7f8aed62013-05-22 18:35:17 -07001429
Alan Lau15490232014-03-06 14:53:14 -08001430 cancelChildNotifications(key, notificationManager);
1431
Scott Kennedy7f8aed62013-05-22 18:35:17 -07001432 if (markSeen) {
1433 markSeen(context, folder);
1434 }
1435 }
1436
1437 /**
1438 * Clears all notifications for the specified account.
1439 */
Tony Mantlerb90a8132013-10-03 12:35:44 -07001440 public static void clearAccountNotifications(final Context context,
1441 final android.accounts.Account account) {
Scott Kennedy2f97af92013-07-30 10:42:34 -07001442 LogUtils.v(LOG_TAG, "Clearing all notifications for %s", account);
Scott Kennedy7f8aed62013-05-22 18:35:17 -07001443 final NotificationMap notificationMap = getNotificationMap(context);
1444
1445 // Find all NotificationKeys for this account
1446 final ImmutableList.Builder<NotificationKey> keyBuilder = ImmutableList.builder();
1447
1448 for (final NotificationKey key : notificationMap.keySet()) {
Tony Mantlerb90a8132013-10-03 12:35:44 -07001449 if (account.equals(key.account.getAccountManagerAccount())) {
Scott Kennedy7f8aed62013-05-22 18:35:17 -07001450 keyBuilder.add(key);
1451 }
1452 }
1453
1454 final List<NotificationKey> notificationKeys = keyBuilder.build();
1455
Alan Lau15490232014-03-06 14:53:14 -08001456 final NotificationManagerCompat notificationManager =
1457 NotificationManagerCompat.from(context);
Scott Kennedy7f8aed62013-05-22 18:35:17 -07001458
1459 for (final NotificationKey notificationKey : notificationKeys) {
1460 final Folder folder = notificationKey.folder;
1461 notificationManager.cancel(getNotificationId(account, folder));
1462 notificationMap.remove(notificationKey);
Alan Lau15490232014-03-06 14:53:14 -08001463
1464 cancelChildNotifications(notificationKey, notificationManager);
Scott Kennedy7f8aed62013-05-22 18:35:17 -07001465 }
1466
1467 notificationMap.saveNotificationMap(context);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001468 }
1469
Alan Lau15490232014-03-06 14:53:14 -08001470 private static void cancelChildNotifications(NotificationKey key,
1471 NotificationManagerCompat nm) {
1472 Set<Integer> childNotifications = sChildNotificationsMap.get(key);
1473 if (childNotifications != null) {
1474 for (Integer childNotification : childNotifications) {
1475 nm.cancel(childNotification);
1476 }
1477 sChildNotificationsMap.remove(key);
1478 }
1479 }
1480
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001481 private static ArrayList<Long> findContacts(Context context, Collection<String> addresses) {
1482 ArrayList<String> whereArgs = new ArrayList<String>();
1483 StringBuilder whereBuilder = new StringBuilder();
1484 String[] questionMarks = new String[addresses.size()];
1485
1486 whereArgs.addAll(addresses);
1487 Arrays.fill(questionMarks, "?");
1488 whereBuilder.append(Email.DATA1 + " IN (").
1489 append(TextUtils.join(",", questionMarks)).
1490 append(")");
1491
1492 ContentResolver resolver = context.getContentResolver();
1493 Cursor c = resolver.query(Email.CONTENT_URI,
1494 new String[]{Email.CONTACT_ID}, whereBuilder.toString(),
1495 whereArgs.toArray(new String[0]), null);
1496
1497 ArrayList<Long> contactIds = new ArrayList<Long>();
1498 if (c == null) {
1499 return contactIds;
1500 }
1501 try {
1502 while (c.moveToNext()) {
1503 contactIds.add(c.getLong(0));
1504 }
1505 } finally {
1506 c.close();
1507 }
1508 return contactIds;
1509 }
1510
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001511 private static Bitmap getContactIcon(final Context context, final String displayName,
1512 final String senderAddress, final Folder folder) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001513 if (senderAddress == null) {
1514 return null;
1515 }
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001516
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001517 Bitmap icon = null;
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001518
1519 final List<Long> contactIds = findContacts( context, Arrays.asList(
1520 new String[] { senderAddress }));
1521
1522 // Get the ideal size for this icon.
1523 final Resources res = context.getResources();
1524 final int idealIconHeight =
1525 res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
1526 final int idealIconWidth =
1527 res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001528
1529 if (contactIds != null) {
Scott Kennedy4046d062013-06-27 14:46:09 -07001530 for (final long id : contactIds) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001531 final Uri contactUri =
1532 ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id);
1533 final Uri photoUri = Uri.withAppendedPath(contactUri, Photo.CONTENT_DIRECTORY);
1534 final Cursor cursor = context.getContentResolver().query(
1535 photoUri, new String[] { Photo.PHOTO }, null, null, null);
1536
1537 if (cursor != null) {
1538 try {
1539 if (cursor.moveToFirst()) {
Scott Kennedy4046d062013-06-27 14:46:09 -07001540 final byte[] data = cursor.getBlob(0);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001541 if (data != null) {
1542 icon = BitmapFactory.decodeStream(new ByteArrayInputStream(data));
1543 if (icon != null && icon.getHeight() < idealIconHeight) {
1544 // We should scale this image to fit the intended size
1545 icon = Bitmap.createScaledBitmap(
1546 icon, idealIconWidth, idealIconHeight, true);
1547 }
1548 if (icon != null) {
1549 break;
1550 }
1551 }
1552 }
1553 } finally {
1554 cursor.close();
1555 }
1556 }
1557 }
1558 }
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001559
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001560 if (icon == null) {
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001561 // Make a colorful tile!
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001562 final Dimensions dimensions = new Dimensions(idealIconWidth, idealIconHeight,
1563 Dimensions.SCALE_ONE);
1564
Scott Kennedyfe235122013-06-28 12:06:36 -07001565 icon = new LetterTileProvider(context).getLetterTile(dimensions,
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001566 displayName, senderAddress);
1567 }
1568
1569 if (icon == null) {
1570 // Icon should be the default mail icon.
Scott Kennedy61bd0e82012-12-10 18:18:17 -08001571 icon = getDefaultNotificationIcon(context, folder, false /* single new message */);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001572 }
1573 return icon;
1574 }
1575
1576 private static String getMessageBodyWithoutElidedText(final Message message) {
Scott Kennedy5e7e88b2013-03-07 16:42:12 -08001577 return getMessageBodyWithoutElidedText(message.getBodyAsHtml());
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001578 }
1579
1580 public static String getMessageBodyWithoutElidedText(String html) {
1581 if (TextUtils.isEmpty(html)) {
1582 return "";
1583 }
1584 // Get the html "tree" for this message body
1585 final HtmlTree htmlTree = com.android.mail.utils.Utils.getHtmlTree(html);
1586 htmlTree.setPlainTextConverterFactory(MESSAGE_CONVERTER_FACTORY);
1587
1588 return htmlTree.getPlainText();
1589 }
1590
1591 public static void markSeen(final Context context, final Folder folder) {
Scott Kennedy259df5b2013-07-11 13:24:01 -07001592 final Uri uri = folder.folderUri.fullUri;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001593
1594 final ContentValues values = new ContentValues(1);
1595 values.put(UIProvider.ConversationColumns.SEEN, 1);
1596
1597 context.getContentResolver().update(uri, values, null, null);
1598 }
1599
1600 /**
1601 * Returns a displayable string representing
1602 * the message sender. It has a preference toward showing the name,
1603 * but will fall back to the address if that is all that is available.
1604 */
1605 private static String getDisplayableSender(String sender) {
1606 final EmailAddress address = EmailAddress.getEmailAddress(sender);
1607
1608 String displayableSender = address.getName();
Scott Kennedy390cab92013-05-23 14:29:49 -07001609
1610 if (!TextUtils.isEmpty(displayableSender)) {
Tony Mantler821e5782014-01-06 15:33:43 -08001611 return Address.decodeAddressPersonal(displayableSender);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001612 }
Scott Kennedy390cab92013-05-23 14:29:49 -07001613
1614 // If that fails, default to the sender address.
1615 displayableSender = address.getAddress();
1616
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001617 // If we were unable to tokenize a name or address,
1618 // just use whatever was in the sender.
1619 if (TextUtils.isEmpty(displayableSender)) {
1620 displayableSender = sender;
1621 }
1622 return displayableSender;
1623 }
1624
1625 /**
1626 * Returns only the address portion of a message sender.
1627 */
1628 private static String getSenderAddress(String sender) {
1629 final EmailAddress address = EmailAddress.getEmailAddress(sender);
1630
1631 String tokenizedAddress = address.getAddress();
1632
1633 // If we were unable to tokenize a name or address,
1634 // just use whatever was in the sender.
1635 if (TextUtils.isEmpty(tokenizedAddress)) {
1636 tokenizedAddress = sender;
1637 }
1638 return tokenizedAddress;
1639 }
1640
Tony Mantlerb90a8132013-10-03 12:35:44 -07001641 public static int getNotificationId(final android.accounts.Account account,
1642 final Folder folder) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001643 return 1 ^ account.hashCode() ^ folder.hashCode();
1644 }
1645
Alan Lau15490232014-03-06 14:53:14 -08001646 private static int getNotificationId(int summaryNotificationId, int childHashCode) {
1647 return summaryNotificationId ^ childHashCode;
1648 }
1649
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001650 private static class NotificationKey {
1651 public final Account account;
1652 public final Folder folder;
1653
1654 public NotificationKey(Account account, Folder folder) {
1655 this.account = account;
1656 this.folder = folder;
1657 }
1658
1659 @Override
1660 public boolean equals(Object other) {
1661 if (!(other instanceof NotificationKey)) {
1662 return false;
1663 }
1664 NotificationKey key = (NotificationKey) other;
Tony Mantlerb90a8132013-10-03 12:35:44 -07001665 return account.getAccountManagerAccount().equals(key.account.getAccountManagerAccount())
1666 && folder.equals(key.folder);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001667 }
1668
1669 @Override
1670 public String toString() {
Tony Mantler26a20752014-02-28 16:44:24 -08001671 return account.getDisplayName() + " " + folder.name;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001672 }
1673
1674 @Override
1675 public int hashCode() {
Tony Mantlerb90a8132013-10-03 12:35:44 -07001676 final int accountHashCode = account.getAccountManagerAccount().hashCode();
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001677 final int folderHashCode = folder.hashCode();
1678 return accountHashCode ^ folderHashCode;
1679 }
1680 }
1681
1682 /**
1683 * Contains the logic for converting the contents of one HtmlTree into
1684 * plaintext.
1685 */
1686 public static class MailMessagePlainTextConverter extends HtmlTree.DefaultPlainTextConverter {
1687 // Strings for parsing html message bodies
1688 private static final String ELIDED_TEXT_ELEMENT_NAME = "div";
1689 private static final String ELIDED_TEXT_ELEMENT_ATTRIBUTE_NAME = "class";
1690 private static final String ELIDED_TEXT_ELEMENT_ATTRIBUTE_CLASS_VALUE = "elided-text";
1691
1692 private static final HTML.Attribute ELIDED_TEXT_ATTRIBUTE =
1693 new HTML.Attribute(ELIDED_TEXT_ELEMENT_ATTRIBUTE_NAME, HTML.Attribute.NO_TYPE);
1694
1695 private static final HtmlDocument.Node ELIDED_TEXT_REPLACEMENT_NODE =
1696 HtmlDocument.createSelfTerminatingTag(HTML4.BR_ELEMENT, null, null, null);
1697
1698 private int mEndNodeElidedTextBlock = -1;
1699
1700 @Override
1701 public void addNode(HtmlDocument.Node n, int nodeNum, int endNum) {
1702 // If we are in the middle of an elided text block, don't add this node
1703 if (nodeNum < mEndNodeElidedTextBlock) {
1704 return;
1705 } else if (nodeNum == mEndNodeElidedTextBlock) {
1706 super.addNode(ELIDED_TEXT_REPLACEMENT_NODE, nodeNum, endNum);
1707 return;
1708 }
1709
1710 // If this tag starts another elided text block, we want to remember the end
1711 if (n instanceof HtmlDocument.Tag) {
1712 boolean foundElidedTextTag = false;
1713 final HtmlDocument.Tag htmlTag = (HtmlDocument.Tag)n;
1714 final HTML.Element htmlElement = htmlTag.getElement();
1715 if (ELIDED_TEXT_ELEMENT_NAME.equals(htmlElement.getName())) {
1716 // Make sure that the class is what is expected
1717 final List<HtmlDocument.TagAttribute> attributes =
1718 htmlTag.getAttributes(ELIDED_TEXT_ATTRIBUTE);
1719 for (HtmlDocument.TagAttribute attribute : attributes) {
1720 if (ELIDED_TEXT_ELEMENT_ATTRIBUTE_CLASS_VALUE.equals(
1721 attribute.getValue())) {
1722 // Found an "elided-text" div. Remember information about this tag
1723 mEndNodeElidedTextBlock = endNum;
1724 foundElidedTextTag = true;
1725 break;
1726 }
1727 }
1728 }
1729
1730 if (foundElidedTextTag) {
1731 return;
1732 }
1733 }
1734
Scott Kennedyc56b2332013-05-23 18:24:53 -07001735 super.addNode(n, nodeNum, endNum);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001736 }
1737 }
1738
1739 /**
1740 * During account setup in Email, we may not have an inbox yet, so the notification setting had
1741 * to be stored in {@link AccountPreferences}. If it is still there, we need to move it to the
1742 * {@link FolderPreferences} now.
1743 */
1744 public static void moveNotificationSetting(final AccountPreferences accountPreferences,
1745 final FolderPreferences folderPreferences) {
1746 if (accountPreferences.isDefaultInboxNotificationsEnabledSet()) {
1747 // If this setting has been changed some other way, don't overwrite it
1748 if (!folderPreferences.isNotificationsEnabledSet()) {
1749 final boolean notificationsEnabled =
1750 accountPreferences.getDefaultInboxNotificationsEnabled();
1751
1752 folderPreferences.setNotificationsEnabled(notificationsEnabled);
1753 }
1754
1755 accountPreferences.clearDefaultInboxNotificationsEnabled();
1756 }
1757 }
1758}