blob: 8b8b0d1f134ef1ab5504a08a1c816bb7dd8aec1c [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;
31import android.provider.ContactsContract;
32import android.provider.ContactsContract.CommonDataKinds.Email;
33import android.provider.ContactsContract.Contacts.Photo;
34import android.support.v4.app.NotificationCompat;
Andrew Sappersteinf5810962013-06-27 10:41:33 -070035import android.support.v4.text.BidiFormatter;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080036import android.text.SpannableString;
37import android.text.SpannableStringBuilder;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080038import android.text.TextUtils;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080039import android.text.style.CharacterStyle;
40import android.text.style.TextAppearanceSpan;
41import android.util.Pair;
Scott Kennedy61bd0e82012-12-10 18:18:17 -080042import android.util.SparseArray;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080043
44import com.android.mail.EmailAddress;
45import com.android.mail.MailIntentService;
46import com.android.mail.R;
47import com.android.mail.browse.MessageCursor;
48import com.android.mail.browse.SendersView;
Scott Kennedyc94c80c2013-06-27 14:42:41 -070049import com.android.mail.photomanager.ContactPhotoManager;
Scott Kennedyfe235122013-06-28 12:06:36 -070050import com.android.mail.photomanager.LetterTileProvider;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080051import com.android.mail.preferences.AccountPreferences;
52import com.android.mail.preferences.FolderPreferences;
53import com.android.mail.preferences.MailPrefs;
54import com.android.mail.providers.Account;
Scott Kennedy390cab92013-05-23 14:29:49 -070055import com.android.mail.providers.Address;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080056import com.android.mail.providers.Conversation;
57import com.android.mail.providers.Folder;
58import com.android.mail.providers.Message;
59import com.android.mail.providers.UIProvider;
Scott Kennedyc94c80c2013-06-27 14:42:41 -070060import com.android.mail.ui.ImageCanvas.Dimensions;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080061import com.android.mail.utils.NotificationActionUtils.NotificationAction;
Scott Kennedy09400ef2013-03-07 11:09:47 -080062import com.google.android.common.html.parser.HTML;
63import com.google.android.common.html.parser.HTML4;
64import com.google.android.common.html.parser.HtmlDocument;
65import com.google.android.common.html.parser.HtmlTree;
Andrew Sappersteinddd17bc2013-04-22 14:03:40 -070066import com.google.common.base.Objects;
Scott Kennedy7f8aed62013-05-22 18:35:17 -070067import com.google.common.collect.ImmutableList;
Scott Kennedy09400ef2013-03-07 11:09:47 -080068import com.google.common.collect.Lists;
Scott Kennedy09400ef2013-03-07 11:09:47 -080069import com.google.common.collect.Sets;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080070
71import java.io.ByteArrayInputStream;
72import java.util.ArrayList;
73import java.util.Arrays;
74import java.util.Collection;
75import java.util.List;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080076import java.util.Set;
77import java.util.concurrent.ConcurrentHashMap;
78
79public class NotificationUtils {
80 public static final String LOG_TAG = LogTag.getLogTag();
81
82 /** Contains a list of <(account, label), unread conversations> */
83 private static NotificationMap sActiveNotificationMap = null;
84
Scott Kennedy61bd0e82012-12-10 18:18:17 -080085 private static final SparseArray<Bitmap> sNotificationIcons = new SparseArray<Bitmap>();
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080086
87 private static TextAppearanceSpan sNotificationUnreadStyleSpan;
88 private static CharacterStyle sNotificationReadStyleSpan;
89
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080090 /** A factory that produces a plain text converter that removes elided text. */
91 private static final HtmlTree.PlainTextConverterFactory MESSAGE_CONVERTER_FACTORY =
92 new HtmlTree.PlainTextConverterFactory() {
93 @Override
94 public HtmlTree.PlainTextConverter createInstance() {
95 return new MailMessagePlainTextConverter();
96 }
97 };
98
Andrew Sappersteinf5810962013-06-27 10:41:33 -070099 private static final BidiFormatter sBidiFormatter = BidiFormatter.getInstance();
100
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800101 /**
102 * Clears all notifications in response to the user tapping "Clear" in the status bar.
103 */
104 public static void clearAllNotfications(Context context) {
Scott Kennedy13a73272013-05-09 09:45:03 -0700105 LogUtils.v(LOG_TAG, "NotificationUtils: Clearing all notifications.");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800106 final NotificationMap notificationMap = getNotificationMap(context);
107 notificationMap.clear();
108 notificationMap.saveNotificationMap(context);
109 }
110
111 /**
112 * Returns the notification map, creating it if necessary.
113 */
114 private static synchronized NotificationMap getNotificationMap(Context context) {
115 if (sActiveNotificationMap == null) {
116 sActiveNotificationMap = new NotificationMap();
117
118 // populate the map from the cached data
119 sActiveNotificationMap.loadNotificationMap(context);
120 }
121 return sActiveNotificationMap;
122 }
123
124 /**
125 * Class representing the existing notifications, and the number of unread and
126 * unseen conversations that triggered each.
127 */
128 private static class NotificationMap
129 extends ConcurrentHashMap<NotificationKey, Pair<Integer, Integer>> {
130
131 private static final String NOTIFICATION_PART_SEPARATOR = " ";
132 private static final int NUM_NOTIFICATION_PARTS= 4;
133
134 /**
135 * Retuns the unread count for the given NotificationKey.
136 */
137 public Integer getUnread(NotificationKey key) {
138 final Pair<Integer, Integer> value = get(key);
139 return value != null ? value.first : null;
140 }
141
142 /**
143 * Retuns the unread unseen count for the given NotificationKey.
144 */
145 public Integer getUnseen(NotificationKey key) {
146 final Pair<Integer, Integer> value = get(key);
147 return value != null ? value.second : null;
148 }
149
150 /**
151 * Store the unread and unseen value for the given NotificationKey
152 */
153 public void put(NotificationKey key, int unread, int unseen) {
154 final Pair<Integer, Integer> value =
155 new Pair<Integer, Integer>(Integer.valueOf(unread), Integer.valueOf(unseen));
156 put(key, value);
157 }
158
159 /**
160 * Populates the notification map with previously cached data.
161 */
162 public synchronized void loadNotificationMap(final Context context) {
163 final MailPrefs mailPrefs = MailPrefs.get(context);
164 final Set<String> notificationSet = mailPrefs.getActiveNotificationSet();
165 if (notificationSet != null) {
166 for (String notificationEntry : notificationSet) {
167 // Get the parts of the string that make the notification entry
168 final String[] notificationParts =
169 TextUtils.split(notificationEntry, NOTIFICATION_PART_SEPARATOR);
170 if (notificationParts.length == NUM_NOTIFICATION_PARTS) {
171 final Uri accountUri = Uri.parse(notificationParts[0]);
172 final Cursor accountCursor = context.getContentResolver().query(
173 accountUri, UIProvider.ACCOUNTS_PROJECTION, null, null, null);
174 final Account account;
175 try {
176 if (accountCursor.moveToFirst()) {
177 account = new Account(accountCursor);
178 } else {
179 continue;
180 }
181 } finally {
182 accountCursor.close();
183 }
184
185 final Uri folderUri = Uri.parse(notificationParts[1]);
186 final Cursor folderCursor = context.getContentResolver().query(
187 folderUri, UIProvider.FOLDERS_PROJECTION, null, null, null);
188 final Folder folder;
189 try {
190 if (folderCursor.moveToFirst()) {
191 folder = new Folder(folderCursor);
192 } else {
193 continue;
194 }
195 } finally {
196 folderCursor.close();
197 }
198
199 final NotificationKey key = new NotificationKey(account, folder);
200 final Integer unreadValue = Integer.valueOf(notificationParts[2]);
201 final Integer unseenValue = Integer.valueOf(notificationParts[3]);
202 final Pair<Integer, Integer> unreadUnseenValue =
203 new Pair<Integer, Integer>(unreadValue, unseenValue);
204 put(key, unreadUnseenValue);
205 }
206 }
207 }
208 }
209
210 /**
211 * Cache the notification map.
212 */
213 public synchronized void saveNotificationMap(Context context) {
214 final Set<String> notificationSet = Sets.newHashSet();
215 final Set<NotificationKey> keys = keySet();
216 for (NotificationKey key : keys) {
217 final Pair<Integer, Integer> value = get(key);
218 final Integer unreadCount = value.first;
219 final Integer unseenCount = value.second;
Scott Kennedyff8553f2013-04-05 20:57:44 -0700220 if (unreadCount != null && unseenCount != null) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800221 final String[] partValues = new String[] {
222 key.account.uri.toString(), key.folder.uri.toString(),
223 unreadCount.toString(), unseenCount.toString()};
224 notificationSet.add(TextUtils.join(NOTIFICATION_PART_SEPARATOR, partValues));
225 }
226 }
227 final MailPrefs mailPrefs = MailPrefs.get(context);
228 mailPrefs.cacheActiveNotificationSet(notificationSet);
229 }
230 }
231
232 /**
233 * @return the title of this notification with each account and the number of unread and unseen
234 * conversations for it. Also remove any account in the map that has 0 unread.
235 */
236 private static String createNotificationString(NotificationMap notifications) {
237 StringBuilder result = new StringBuilder();
238 int i = 0;
239 Set<NotificationKey> keysToRemove = Sets.newHashSet();
240 for (NotificationKey key : notifications.keySet()) {
241 Integer unread = notifications.getUnread(key);
242 Integer unseen = notifications.getUnseen(key);
243 if (unread == null || unread.intValue() == 0) {
244 keysToRemove.add(key);
245 } else {
246 if (i > 0) result.append(", ");
247 result.append(key.toString() + " (" + unread + ", " + unseen + ")");
248 i++;
249 }
250 }
251
252 for (NotificationKey key : keysToRemove) {
253 notifications.remove(key);
254 }
255
256 return result.toString();
257 }
258
259 /**
260 * Get all notifications for all accounts and cancel them.
261 **/
262 public static void cancelAllNotifications(Context context) {
Scott Kennedy93486c02013-04-17 12:15:43 -0700263 LogUtils.d(LOG_TAG, "NotificationUtils: cancelAllNotifications - cancelling all");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800264 NotificationManager nm = (NotificationManager) context.getSystemService(
265 Context.NOTIFICATION_SERVICE);
266 nm.cancelAll();
267 clearAllNotfications(context);
268 }
269
270 /**
271 * Get all notifications for all accounts, cancel them, and repost.
272 * This happens when locale changes.
273 **/
274 public static void cancelAndResendNotifications(Context context) {
Scott Kennedy4162f832013-05-06 17:37:55 -0700275 LogUtils.d(LOG_TAG, "NotificationUtils: cancelAndResendNotifications");
Andrew Sappersteinddd17bc2013-04-22 14:03:40 -0700276 resendNotifications(context, true, null, null);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800277 }
278
279 /**
280 * Get all notifications for all accounts, optionally cancel them, and repost.
Andrew Sappersteinddd17bc2013-04-22 14:03:40 -0700281 * This happens when locale changes. If you only want to resend messages from one
282 * account-folder pair, pass in the account and folder that should be resent.
283 * All other account-folder pairs will not have their notifications resent.
284 * All notifications will be resent if account or folder is null.
285 *
286 * @param context Current context.
287 * @param cancelExisting True, if all notifications should be canceled before resending.
288 * False, otherwise.
289 * @param accountUri The {@link Uri} of the {@link Account} of the notification
290 * upon which an action occurred.
291 * @param folderUri The {@link Uri} of the {@link Folder} of the notification
292 * upon which an action occurred.
293 */
294 public static void resendNotifications(Context context, final boolean cancelExisting,
295 final Uri accountUri, final Uri folderUri) {
Scott Kennedy2638b482013-05-06 16:05:11 -0700296 LogUtils.d(LOG_TAG, "NotificationUtils: resendNotifications ");
297
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800298 if (cancelExisting) {
Scott Kennedy93486c02013-04-17 12:15:43 -0700299 LogUtils.d(LOG_TAG, "NotificationUtils: resendNotifications - cancelling all");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800300 NotificationManager nm =
301 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
302 nm.cancelAll();
303 }
304 // Re-validate the notifications.
305 final NotificationMap notificationMap = getNotificationMap(context);
306 final Set<NotificationKey> keys = notificationMap.keySet();
307 for (NotificationKey notification : keys) {
308 final Folder folder = notification.folder;
Andrew Sappersteinddd17bc2013-04-22 14:03:40 -0700309 final int notificationId = getNotificationId(notification.account.name,
310 folder);
311
312 // Only resend notifications if the notifications are from the same folder
313 // and same account as the undo notification that was previously displayed.
Scott Kennedy26b29ef2013-04-28 18:36:50 -0700314 if (accountUri != null && !Objects.equal(accountUri, notification.account.uri) &&
315 folderUri != null && !Objects.equal(folderUri, folder.uri)) {
Scott Kennedy2638b482013-05-06 16:05:11 -0700316 LogUtils.d(LOG_TAG, "NotificationUtils: resendNotifications - not resending %s / %s"
317 + " because it doesn't match %s / %s",
318 notification.account.uri, folder.uri, accountUri, folderUri);
Andrew Sappersteinddd17bc2013-04-22 14:03:40 -0700319 continue;
320 }
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800321
Scott Kennedy2638b482013-05-06 16:05:11 -0700322 LogUtils.d(LOG_TAG, "NotificationUtils: resendNotifications - resending %s / %s",
323 notification.account.uri, folder.uri);
324
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800325 final NotificationAction undoableAction =
326 NotificationActionUtils.sUndoNotifications.get(notificationId);
327 if (undoableAction == null) {
Andrew Sappersteinddd17bc2013-04-22 14:03:40 -0700328 validateNotifications(context, folder, notification.account, true,
329 false, notification);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800330 } else {
331 // Create an undo notification
332 NotificationActionUtils.createUndoNotification(context, undoableAction);
333 }
334 }
335 }
336
337 /**
338 * Validate the notifications for the specified account.
339 */
340 public static void validateAccountNotifications(Context context, String account) {
Scott Kennedy4162f832013-05-06 17:37:55 -0700341 LogUtils.d(LOG_TAG, "NotificationUtils: validateAccountNotifications - %s", account);
342
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800343 List<NotificationKey> notificationsToCancel = Lists.newArrayList();
344 // Iterate through the notification map to see if there are any entries that correspond to
345 // labels that are not in the sync set.
346 final NotificationMap notificationMap = getNotificationMap(context);
347 Set<NotificationKey> keys = notificationMap.keySet();
348 final AccountPreferences accountPreferences = new AccountPreferences(context, account);
349 final boolean enabled = accountPreferences.areNotificationsEnabled();
350 if (!enabled) {
351 // Cancel all notifications for this account
352 for (NotificationKey notification : keys) {
353 if (notification.account.name.equals(account)) {
354 notificationsToCancel.add(notification);
355 }
356 }
357 } else {
358 // Iterate through the notification map to see if there are any entries that
359 // correspond to labels that are not in the notification set.
360 for (NotificationKey notification : keys) {
361 if (notification.account.name.equals(account)) {
362 // If notification is not enabled for this label, remember this NotificationKey
363 // to later cancel the notification, and remove the entry from the map
364 final Folder folder = notification.folder;
365 final boolean isInbox =
366 notification.account.settings.defaultInbox.equals(folder.uri);
367 final FolderPreferences folderPreferences = new FolderPreferences(
368 context, notification.account.name, folder, isInbox);
369
370 if (!folderPreferences.areNotificationsEnabled()) {
371 notificationsToCancel.add(notification);
372 }
373 }
374 }
375 }
376
377 // Cancel & remove the invalid notifications.
378 if (notificationsToCancel.size() > 0) {
379 NotificationManager nm = (NotificationManager) context.getSystemService(
380 Context.NOTIFICATION_SERVICE);
381 for (NotificationKey notification : notificationsToCancel) {
382 final Folder folder = notification.folder;
383 final int notificationId = getNotificationId(notification.account.name, folder);
Scott Kennedy93486c02013-04-17 12:15:43 -0700384 LogUtils.d(LOG_TAG,
385 "NotificationUtils: validateAccountNotifications - cancelling %s / %s",
386 notification.account.name, folder.persistentId);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800387 nm.cancel(notificationId);
388 notificationMap.remove(notification);
389 NotificationActionUtils.sUndoNotifications.remove(notificationId);
390 NotificationActionUtils.sNotificationTimestamps.delete(notificationId);
391 }
392 notificationMap.saveNotificationMap(context);
393 }
394 }
395
396 /**
397 * Display only one notification.
398 */
399 public static void setNewEmailIndicator(Context context, final int unreadCount,
400 final int unseenCount, final Account account, final Folder folder,
401 final boolean getAttention) {
Scott Kennedy4162f832013-05-06 17:37:55 -0700402 LogUtils.d(LOG_TAG, "NotificationUtils: setNewEmailIndicator unreadCount = %d, "
403 + "unseenCount = %d, account = %s, folder = %s, getAttention = %b", unreadCount,
404 unseenCount, account.name, folder.uri, getAttention);
405
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800406 boolean ignoreUnobtrusiveSetting = false;
407
Scott Kennedydab8a942013-02-22 12:31:30 -0800408 final int notificationId = getNotificationId(account.name, folder);
409
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800410 // Update the notification map
411 final NotificationMap notificationMap = getNotificationMap(context);
412 final NotificationKey key = new NotificationKey(account, folder);
413 if (unreadCount == 0) {
Scott Kennedy93486c02013-04-17 12:15:43 -0700414 LogUtils.d(LOG_TAG,
415 "NotificationUtils: setNewEmailIndicator - cancelling %s / %s",
416 account.name, folder.persistentId);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800417 notificationMap.remove(key);
Scott Kennedydab8a942013-02-22 12:31:30 -0800418 ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
419 .cancel(notificationId);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800420 } else {
421 if (!notificationMap.containsKey(key)) {
422 // This account previously didn't have any unread mail; ignore the "unobtrusive
423 // notifications" setting and play sound and/or vibrate the device even if a
424 // notification already exists (bug 2412348).
425 ignoreUnobtrusiveSetting = true;
426 }
427 notificationMap.put(key, unreadCount, unseenCount);
428 }
429 notificationMap.saveNotificationMap(context);
430
431 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
Scott Kennedy13a73272013-05-09 09:45:03 -0700432 LogUtils.v(LOG_TAG, "NotificationUtils: New email: %s mapSize: %d getAttention: %b",
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800433 createNotificationString(notificationMap), notificationMap.size(),
434 getAttention);
435 }
436
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800437 if (NotificationActionUtils.sUndoNotifications.get(notificationId) == null) {
438 validateNotifications(context, folder, account, getAttention, ignoreUnobtrusiveSetting,
439 key);
440 }
441 }
442
443 /**
444 * Validate the notifications notification.
445 */
446 private static void validateNotifications(Context context, final Folder folder,
447 final Account account, boolean getAttention, boolean ignoreUnobtrusiveSetting,
448 NotificationKey key) {
449
450 NotificationManager nm = (NotificationManager)
451 context.getSystemService(Context.NOTIFICATION_SERVICE);
452
453 final NotificationMap notificationMap = getNotificationMap(context);
454 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
Scott Kennedy13a73272013-05-09 09:45:03 -0700455 LogUtils.v(LOG_TAG, "NotificationUtils: Validating Notification: %s mapSize: %d "
456 + "folder: %s getAttention: %b", createNotificationString(notificationMap),
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800457 notificationMap.size(), folder.name, getAttention);
458 }
459 // The number of unread messages for this account and label.
460 final Integer unread = notificationMap.getUnread(key);
461 final int unreadCount = unread != null ? unread.intValue() : 0;
462 final Integer unseen = notificationMap.getUnseen(key);
463 int unseenCount = unseen != null ? unseen.intValue() : 0;
464
465 Cursor cursor = null;
466
467 try {
468 final Uri.Builder uriBuilder = folder.conversationListUri.buildUpon();
469 uriBuilder.appendQueryParameter(
470 UIProvider.SEEN_QUERY_PARAMETER, Boolean.FALSE.toString());
Andy Huang0be92432013-03-22 18:31:44 -0700471 // Do not allow this quick check to disrupt any active network-enabled conversation
472 // cursor.
473 uriBuilder.appendQueryParameter(
474 UIProvider.ConversationListQueryParameters.USE_NETWORK,
475 Boolean.FALSE.toString());
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800476 cursor = context.getContentResolver().query(uriBuilder.build(),
477 UIProvider.CONVERSATION_PROJECTION, null, null, null);
Yu Ping Hu3a7b45b2013-06-18 18:10:50 -0700478 if (cursor == null) {
479 // This folder doesn't exist.
480 clearFolderNotification(context, account, folder, false);
481 return;
482 }
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800483 final int cursorUnseenCount = cursor.getCount();
484
485 // Make sure the unseen count matches the number of items in the cursor. But, we don't
486 // want to overwrite a 0 unseen count that was specified in the intent
487 if (unseenCount != 0 && unseenCount != cursorUnseenCount) {
Scott Kennedy13a73272013-05-09 09:45:03 -0700488 LogUtils.d(LOG_TAG, "NotificationUtils: "
489 + "Unseen count doesn't match cursor count. unseen: %d cursor count: %d",
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800490 unseenCount, cursorUnseenCount);
491 unseenCount = cursorUnseenCount;
492 }
493
494 // For the purpose of the notifications, the unseen count should be capped at the num of
495 // unread conversations.
496 if (unseenCount > unreadCount) {
497 unseenCount = unreadCount;
498 }
499
500 final int notificationId = getNotificationId(account.name, folder);
501
502 if (unseenCount == 0) {
Scott Kennedy93486c02013-04-17 12:15:43 -0700503 LogUtils.d(LOG_TAG,
504 "NotificationUtils: validateNotifications - cancelling %s / %s",
505 account.name, folder.persistentId);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800506 nm.cancel(notificationId);
507 return;
508 }
509
510 // We now have all we need to create the notification and the pending intent
511 PendingIntent clickIntent;
512
513 NotificationCompat.Builder notification = new NotificationCompat.Builder(context);
514 notification.setSmallIcon(R.drawable.stat_notify_email);
515 notification.setTicker(account.name);
516
517 final long when;
518
519 final long oldWhen =
520 NotificationActionUtils.sNotificationTimestamps.get(notificationId);
521 if (oldWhen != 0) {
522 when = oldWhen;
523 } else {
524 when = System.currentTimeMillis();
525 }
526
527 notification.setWhen(when);
528
529 // The timestamp is now stored in the notification, so we can remove it from here
530 NotificationActionUtils.sNotificationTimestamps.delete(notificationId);
531
532 // Dispatch a CLEAR_NEW_MAIL_NOTIFICATIONS intent if the user taps the "X" next to a
533 // notification. Also this intent gets fired when the user taps on a notification as
534 // the AutoCancel flag has been set
535 final Intent cancelNotificationIntent =
536 new Intent(MailIntentService.ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS);
537 cancelNotificationIntent.setPackage(context.getPackageName());
Scott Kennedy09400ef2013-03-07 11:09:47 -0800538 cancelNotificationIntent.setData(Utils.appendVersionQueryParameter(context,
539 folder.uri));
Scott Kennedy48cfe462013-04-10 11:32:02 -0700540 cancelNotificationIntent.putExtra(Utils.EXTRA_ACCOUNT, account);
541 cancelNotificationIntent.putExtra(Utils.EXTRA_FOLDER, folder);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800542
543 notification.setDeleteIntent(PendingIntent.getService(
544 context, notificationId, cancelNotificationIntent, 0));
545
546 // Ensure that the notification is cleared when the user selects it
547 notification.setAutoCancel(true);
548
549 boolean eventInfoConfigured = false;
550
551 final boolean isInbox = account.settings.defaultInbox.equals(folder.uri);
552 final FolderPreferences folderPreferences =
553 new FolderPreferences(context, account.name, folder, isInbox);
554
555 if (isInbox) {
556 final AccountPreferences accountPreferences =
557 new AccountPreferences(context, account.name);
558 moveNotificationSetting(accountPreferences, folderPreferences);
559 }
560
561 if (!folderPreferences.areNotificationsEnabled()) {
562 // Don't notify
563 return;
564 }
565
566 if (unreadCount > 0) {
567 // How can I order this properly?
568 if (cursor.moveToNext()) {
Scott Kennedybd8acad2013-05-29 10:22:25 -0700569 final Intent notificationIntent;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800570
Scott Kennedybd8acad2013-05-29 10:22:25 -0700571 // Launch directly to the conversation, if there is only 1 unseen conversation
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800572 if (unseenCount == 1) {
Scott Kennedy09400ef2013-03-07 11:09:47 -0800573 notificationIntent = createViewConversationIntent(context, account, folder,
574 cursor);
Scott Kennedybd8acad2013-05-29 10:22:25 -0700575 } else {
576 notificationIntent = createViewConversationIntent(context, account, folder,
577 null);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800578 }
579
580 if (notificationIntent == null) {
Scott Kennedy13a73272013-05-09 09:45:03 -0700581 LogUtils.e(LOG_TAG, "NotificationUtils: "
582 + "Null intent when building notification");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800583 return;
584 }
585
586 clickIntent = PendingIntent.getActivity(context, -1, notificationIntent, 0);
587 configureLatestEventInfoFromConversation(context, account, folderPreferences,
588 notification, cursor, clickIntent, notificationIntent,
589 account.name, unreadCount, unseenCount, folder, when);
590 eventInfoConfigured = true;
591 }
592 }
593
594 final boolean vibrate = folderPreferences.isNotificationVibrateEnabled();
595 final String ringtoneUri = folderPreferences.getNotificationRingtoneUri();
596 final boolean notifyOnce = !folderPreferences.isEveryMessageNotificationEnabled();
597
Scott Kennedyff8553f2013-04-05 20:57:44 -0700598 if (!ignoreUnobtrusiveSetting && notifyOnce) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800599 // If the user has "unobtrusive notifications" enabled, only alert the first time
600 // new mail is received in this account. This is the default behavior. See
601 // bugs 2412348 and 2413490.
602 notification.setOnlyAlertOnce(true);
603 }
604
Scott Kennedy13a73272013-05-09 09:45:03 -0700605 LogUtils.d(LOG_TAG, "NotificationUtils: Account: %s vibrate: %s", account.name,
Scott Kennedyff8553f2013-04-05 20:57:44 -0700606 Boolean.toString(folderPreferences.isNotificationVibrateEnabled()));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800607
608 int defaults = 0;
609
610 /*
611 * We do not want to notify if this is coming back from an Undo notification, hence the
612 * oldWhen check.
613 */
Scott Kennedyff8553f2013-04-05 20:57:44 -0700614 if (getAttention && oldWhen == 0) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800615 final AccountPreferences accountPreferences =
616 new AccountPreferences(context, account.name);
617 if (accountPreferences.areNotificationsEnabled()) {
618 if (vibrate) {
619 defaults |= Notification.DEFAULT_VIBRATE;
620 }
621
622 notification.setSound(TextUtils.isEmpty(ringtoneUri) ? null
623 : Uri.parse(ringtoneUri));
Scott Kennedy13a73272013-05-09 09:45:03 -0700624 LogUtils.d(LOG_TAG, "NotificationUtils: "
625 + "New email in %s vibrateWhen: %s, playing notification: %s",
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800626 account.name, vibrate, ringtoneUri);
627 }
628 }
629
630 if (eventInfoConfigured) {
631 defaults |= Notification.DEFAULT_LIGHTS;
632 notification.setDefaults(defaults);
633
634 if (oldWhen != 0) {
635 // We do not want to display the ticker again if we are re-displaying this
636 // notification (like from an Undo notification)
637 notification.setTicker(null);
638 }
639
640 nm.notify(notificationId, notification.build());
641 }
642 } finally {
643 if (cursor != null) {
644 cursor.close();
645 }
646 }
647 }
648
649 /**
650 * @return an {@link Intent} which, if launched, will display the corresponding conversation
651 */
Scott Kennedy09400ef2013-03-07 11:09:47 -0800652 private static Intent createViewConversationIntent(final Context context, final Account account,
653 final Folder folder, final Cursor cursor) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800654 if (folder == null || account == null) {
Scott Kennedy13a73272013-05-09 09:45:03 -0700655 LogUtils.e(LOG_TAG, "NotificationUtils#createViewConversationIntent(): "
656 + "Null account or folder. account: %s folder: %s", account, folder);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800657 return null;
658 }
659
660 final Intent intent;
661
662 if (cursor == null) {
Scott Kennedyb39aaf52013-03-06 19:17:22 -0800663 intent = Utils.createViewFolderIntent(context, folder.uri, account);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800664 } else {
665 // A conversation cursor has been specified, so this intent is intended to be go
666 // directly to the one new conversation
667
668 // Get the Conversation object
669 final Conversation conversation = new Conversation(cursor);
Scott Kennedyb39aaf52013-03-06 19:17:22 -0800670 intent = Utils.createViewConversationIntent(context, conversation, folder.uri,
671 account);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800672 }
673
674 return intent;
675 }
676
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800677 private static Bitmap getDefaultNotificationIcon(
678 final Context context, final Folder folder, final boolean multipleNew) {
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700679 final int resId;
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800680 if (folder.notificationIconResId != 0) {
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700681 resId = folder.notificationIconResId;
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800682 } else if (multipleNew) {
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700683 resId = R.drawable.ic_notification_multiple_mail_holo_dark;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800684 } else {
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700685 resId = R.drawable.ic_contact_picture;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800686 }
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700687
688 final Bitmap icon = getIcon(context, resId);
689
690 if (icon == null) {
691 LogUtils.e(LOG_TAG, "Couldn't decode notif icon res id %d", resId);
692 }
693
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800694 return icon;
695 }
696
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800697 private static Bitmap getIcon(final Context context, final int resId) {
698 final Bitmap cachedIcon = sNotificationIcons.get(resId);
699 if (cachedIcon != null) {
700 return cachedIcon;
701 }
702
703 final Bitmap icon = BitmapFactory.decodeResource(context.getResources(), resId);
704 sNotificationIcons.put(resId, icon);
705
706 return icon;
707 }
708
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800709 private static void configureLatestEventInfoFromConversation(final Context context,
710 final Account account, final FolderPreferences folderPreferences,
711 final NotificationCompat.Builder notification, final Cursor conversationCursor,
712 final PendingIntent clickIntent, final Intent notificationIntent,
713 final String notificationAccount, final int unreadCount, final int unseenCount,
714 final Folder folder, final long when) {
715 final Resources res = context.getResources();
716
Scott Kennedy13a73272013-05-09 09:45:03 -0700717 LogUtils.w(LOG_TAG, "NotificationUtils: Showing notification with unreadCount of %d and "
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800718 + "unseenCount of %d", unreadCount, unseenCount);
719
720 String notificationTicker = null;
721
722 // Boolean indicating that this notification is for a non-inbox label.
723 final boolean isInbox = account.settings.defaultInbox.equals(folder.uri);
724
725 // Notification label name for user label notifications.
726 final String notificationLabelName = isInbox ? null : folder.name;
727
728 if (unseenCount > 1) {
729 // Build the string that describes the number of new messages
730 final String newMessagesString = res.getString(R.string.new_messages, unseenCount);
731
732 // Use the default notification icon
733 notification.setLargeIcon(
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800734 getDefaultNotificationIcon(context, folder, true /* multiple new messages */));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800735
736 // The ticker initially start as the new messages string.
737 notificationTicker = newMessagesString;
738
739 // The title of the notification is the new messages string
740 notification.setContentTitle(newMessagesString);
741
Scott Kennedyc8bc5d12013-04-25 08:46:06 -0700742 // TODO(skennedy) Can we remove this check?
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800743 if (com.android.mail.utils.Utils.isRunningJellybeanOrLater()) {
744 // For a new-style notification
745 final int maxNumDigestItems = context.getResources().getInteger(
746 R.integer.max_num_notification_digest_items);
747
748 // The body of the notification is the account name, or the label name.
749 notification.setSubText(isInbox ? notificationAccount : notificationLabelName);
750
751 final NotificationCompat.InboxStyle digest =
752 new NotificationCompat.InboxStyle(notification);
753
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800754 int numDigestItems = 0;
755 do {
756 final Conversation conversation = new Conversation(conversationCursor);
757
758 if (!conversation.read) {
759 boolean multipleUnreadThread = false;
760 // TODO(cwren) extract this pattern into a helper
761
762 Cursor cursor = null;
763 MessageCursor messageCursor = null;
764 try {
765 final Uri.Builder uriBuilder = conversation.messageListUri.buildUpon();
766 uriBuilder.appendQueryParameter(
767 UIProvider.LABEL_QUERY_PARAMETER, notificationLabelName);
768 cursor = context.getContentResolver().query(uriBuilder.build(),
769 UIProvider.MESSAGE_PROJECTION, null, null, null);
770 messageCursor = new MessageCursor(cursor);
771
Yu Ping Hub18d75a2013-03-12 17:04:58 -0700772 String from = "";
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800773 String fromAddress = "";
774 if (messageCursor.moveToPosition(messageCursor.getCount() - 1)) {
775 final Message message = messageCursor.getMessage();
776 fromAddress = message.getFrom();
Yu Ping Hub18d75a2013-03-12 17:04:58 -0700777 if (fromAddress == null) {
778 fromAddress = "";
779 }
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800780 from = getDisplayableSender(fromAddress);
781 }
782 while (messageCursor.moveToPosition(messageCursor.getPosition() - 1)) {
783 final Message message = messageCursor.getMessage();
784 if (!message.read &&
785 !fromAddress.contentEquals(message.getFrom())) {
786 multipleUnreadThread = true;
787 break;
788 }
789 }
790 final SpannableStringBuilder sendersBuilder;
791 if (multipleUnreadThread) {
792 final int sendersLength =
793 res.getInteger(R.integer.swipe_senders_length);
794
795 sendersBuilder = getStyledSenders(context, conversationCursor,
796 sendersLength, notificationAccount);
797 } else {
Paul Westbrook3507af92013-03-30 17:06:12 -0700798 if (from == null) {
Scott Kennedy13a73272013-05-09 09:45:03 -0700799 LogUtils.e(LOG_TAG, "NotificationUtils: null from string in " +
Paul Westbrook3507af92013-03-30 17:06:12 -0700800 "configureLatestEventInfoFromConversation");
801 from = "";
802 }
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800803 sendersBuilder = new SpannableStringBuilder(from);
804 }
805 final CharSequence digestLine = getSingleMessageInboxLine(context,
806 sendersBuilder.toString(),
807 conversation.subject,
808 conversation.snippet);
809 digest.addLine(digestLine);
810 numDigestItems++;
811 } finally {
812 if (messageCursor != null) {
813 messageCursor.close();
814 }
815 if (cursor != null) {
816 cursor.close();
817 }
818 }
819 }
820 } while (numDigestItems <= maxNumDigestItems && conversationCursor.moveToNext());
821 } else {
822 // The body of the notification is the account name, or the label name.
823 notification.setContentText(
824 isInbox ? notificationAccount : notificationLabelName);
825 }
826 } else {
827 // For notifications for a single new conversation, we want to get the information from
828 // the conversation
829
830 // Move the cursor to the most recent unread conversation
831 seekToLatestUnreadConversation(conversationCursor);
832
833 final Conversation conversation = new Conversation(conversationCursor);
834
835 Cursor cursor = null;
836 MessageCursor messageCursor = null;
837 boolean multipleUnseenThread = false;
838 String from = null;
839 try {
Scott Kennedyffbf86f2013-03-08 11:48:15 -0800840 final Uri uri = conversation.messageListUri.buildUpon().appendQueryParameter(
841 UIProvider.LABEL_QUERY_PARAMETER, folder.persistentId).build();
842 cursor = context.getContentResolver().query(uri, UIProvider.MESSAGE_PROJECTION,
843 null, null, null);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800844 messageCursor = new MessageCursor(cursor);
845 // Use the information from the last sender in the conversation that triggered
846 // this notification.
847
848 String fromAddress = "";
849 if (messageCursor.moveToPosition(messageCursor.getCount() - 1)) {
850 final Message message = messageCursor.getMessage();
851 fromAddress = message.getFrom();
852 from = getDisplayableSender(fromAddress);
853 notification.setLargeIcon(
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700854 getContactIcon(context, from, getSenderAddress(fromAddress), folder));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800855 }
856
857 // Assume that the last message in this conversation is unread
858 int firstUnseenMessagePos = messageCursor.getPosition();
859 while (messageCursor.moveToPosition(messageCursor.getPosition() - 1)) {
860 final Message message = messageCursor.getMessage();
861 final boolean unseen = !message.seen;
862 if (unseen) {
863 firstUnseenMessagePos = messageCursor.getPosition();
864 if (!multipleUnseenThread
865 && !fromAddress.contentEquals(message.getFrom())) {
866 multipleUnseenThread = true;
867 }
868 }
869 }
870
Scott Kennedyc8bc5d12013-04-25 08:46:06 -0700871 // TODO(skennedy) Can we remove this check?
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800872 if (Utils.isRunningJellybeanOrLater()) {
873 // For a new-style notification
874
875 if (multipleUnseenThread) {
876 // The title of a single conversation is the list of senders.
877 int sendersLength = res.getInteger(R.integer.swipe_senders_length);
878
879 final SpannableStringBuilder sendersBuilder = getStyledSenders(
880 context, conversationCursor, sendersLength, notificationAccount);
881
882 notification.setContentTitle(sendersBuilder);
883 // For a single new conversation, the ticker is based on the sender's name.
884 notificationTicker = sendersBuilder.toString();
885 } else {
886 // The title of a single message the sender.
887 notification.setContentTitle(from);
888 // For a single new conversation, the ticker is based on the sender's name.
889 notificationTicker = from;
890 }
891
892 // The notification content will be the subject of the conversation.
893 notification.setContentText(
894 getSingleMessageLittleText(context, conversation.subject));
895
896 // The notification subtext will be the subject of the conversation for inbox
897 // notifications, or will based on the the label name for user label
898 // notifications.
899 notification.setSubText(isInbox ? notificationAccount : notificationLabelName);
900
901 if (multipleUnseenThread) {
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800902 notification.setLargeIcon(
903 getDefaultNotificationIcon(context, folder, true));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800904 }
905 final NotificationCompat.BigTextStyle bigText =
906 new NotificationCompat.BigTextStyle(notification);
907
908 // Seek the message cursor to the first unread message
Paul Westbrook92b21742013-03-11 17:36:40 -0700909 final Message message;
910 if (messageCursor.moveToPosition(firstUnseenMessagePos)) {
911 message = messageCursor.getMessage();
912 bigText.bigText(getSingleMessageBigText(context,
913 conversation.subject, message));
914 } else {
Scott Kennedy13a73272013-05-09 09:45:03 -0700915 LogUtils.e(LOG_TAG, "NotificationUtils: Failed to load message");
Paul Westbrook92b21742013-03-11 17:36:40 -0700916 message = null;
917 }
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800918
Paul Westbrook92b21742013-03-11 17:36:40 -0700919 if (message != null) {
920 final Set<String> notificationActions =
Scott Kennedycde6eb02013-03-21 18:49:32 -0700921 folderPreferences.getNotificationActions(account);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800922
Paul Westbrook92b21742013-03-11 17:36:40 -0700923 final int notificationId = getNotificationId(notificationAccount, folder);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800924
Paul Westbrook92b21742013-03-11 17:36:40 -0700925 NotificationActionUtils.addNotificationActions(context, notificationIntent,
926 notification, account, conversation, message, folder,
927 notificationId, when, notificationActions);
928 }
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800929 } else {
930 // For an old-style notification
931
932 // The title of a single conversation notification is built from both the sender
933 // and subject of the new message.
934 notification.setContentTitle(getSingleMessageNotificationTitle(context,
935 from, conversation.subject));
936
937 // The notification content will be the subject of the conversation for inbox
938 // notifications, or will based on the the label name for user label
939 // notifications.
940 notification.setContentText(
941 isInbox ? notificationAccount : notificationLabelName);
942
943 // For a single new conversation, the ticker is based on the sender's name.
944 notificationTicker = from;
945 }
946 } finally {
947 if (messageCursor != null) {
948 messageCursor.close();
949 }
950 if (cursor != null) {
951 cursor.close();
952 }
953 }
954 }
955
956 // Build the notification ticker
957 if (notificationLabelName != null && notificationTicker != null) {
958 // This is a per label notification, format the ticker with that information
959 notificationTicker = res.getString(R.string.label_notification_ticker,
960 notificationLabelName, notificationTicker);
961 }
962
963 if (notificationTicker != null) {
964 // If we didn't generate a notification ticker, it will default to account name
965 notification.setTicker(notificationTicker);
966 }
967
968 // Set the number in the notification
969 if (unreadCount > 1) {
970 notification.setNumber(unreadCount);
971 }
972
973 notification.setContentIntent(clickIntent);
974 }
975
976 private static SpannableStringBuilder getStyledSenders(final Context context,
977 final Cursor conversationCursor, final int maxLength, final String account) {
978 final Conversation conversation = new Conversation(conversationCursor);
979 final com.android.mail.providers.ConversationInfo conversationInfo =
980 conversation.conversationInfo;
981 final ArrayList<SpannableString> senders = new ArrayList<SpannableString>();
982 if (sNotificationUnreadStyleSpan == null) {
983 sNotificationUnreadStyleSpan = new TextAppearanceSpan(
984 context, R.style.NotificationSendersUnreadTextAppearance);
985 sNotificationReadStyleSpan =
986 new TextAppearanceSpan(context, R.style.NotificationSendersReadTextAppearance);
987 }
988 SendersView.format(context, conversationInfo, "", maxLength, senders, null, null, account,
989 sNotificationUnreadStyleSpan, sNotificationReadStyleSpan, false);
990
991 return ellipsizeStyledSenders(context, senders);
992 }
993
994 private static String sSendersSplitToken = null;
995 private static String sElidedPaddingToken = null;
996
997 private static SpannableStringBuilder ellipsizeStyledSenders(final Context context,
998 ArrayList<SpannableString> styledSenders) {
999 if (sSendersSplitToken == null) {
1000 sSendersSplitToken = context.getString(R.string.senders_split_token);
1001 sElidedPaddingToken = context.getString(R.string.elided_padding_token);
1002 }
1003
1004 SpannableStringBuilder builder = new SpannableStringBuilder();
1005 SpannableString prevSender = null;
1006 for (SpannableString sender : styledSenders) {
1007 if (sender == null) {
Scott Kennedy13a73272013-05-09 09:45:03 -07001008 LogUtils.e(LOG_TAG, "NotificationUtils: null sender iterating over styledSenders");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001009 continue;
1010 }
1011 CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class);
1012 if (SendersView.sElidedString.equals(sender.toString())) {
1013 prevSender = sender;
1014 sender = copyStyles(spans, sElidedPaddingToken + sender + sElidedPaddingToken);
1015 } else if (builder.length() > 0
1016 && (prevSender == null || !SendersView.sElidedString.equals(prevSender
1017 .toString()))) {
1018 prevSender = sender;
1019 sender = copyStyles(spans, sSendersSplitToken + sender);
1020 } else {
1021 prevSender = sender;
1022 }
1023 builder.append(sender);
1024 }
1025 return builder;
1026 }
1027
1028 private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) {
1029 SpannableString s = new SpannableString(newText);
1030 if (spans != null && spans.length > 0) {
1031 s.setSpan(spans[0], 0, s.length(), 0);
1032 }
1033 return s;
1034 }
1035
1036 /**
1037 * Seeks the cursor to the position of the most recent unread conversation. If no unread
1038 * conversation is found, the position of the cursor will be restored, and false will be
1039 * returned.
1040 */
1041 private static boolean seekToLatestUnreadConversation(final Cursor cursor) {
1042 final int initialPosition = cursor.getPosition();
1043 do {
1044 final Conversation conversation = new Conversation(cursor);
1045 if (!conversation.read) {
1046 return true;
1047 }
1048 } while (cursor.moveToNext());
1049
1050 // Didn't find an unread conversation, reset the position.
1051 cursor.moveToPosition(initialPosition);
1052 return false;
1053 }
1054
1055 /**
1056 * Sets the bigtext for a notification for a single new conversation
1057 *
1058 * @param context
1059 * @param senders Sender of the new message that triggered the notification.
1060 * @param subject Subject of the new message that triggered the notification
1061 * @param snippet Snippet of the new message that triggered the notification
1062 * @return a {@link CharSequence} suitable for use in
1063 * {@link android.support.v4.app.NotificationCompat.BigTextStyle}
1064 */
1065 private static CharSequence getSingleMessageInboxLine(Context context,
1066 String senders, String subject, String snippet) {
1067 // TODO(cwren) finish this step toward commmon code with getSingleMessageBigText
1068
1069 final String subjectSnippet = !TextUtils.isEmpty(subject) ? subject : snippet;
1070
1071 final TextAppearanceSpan notificationPrimarySpan =
1072 new TextAppearanceSpan(context, R.style.NotificationPrimaryText);
1073
1074 if (TextUtils.isEmpty(senders)) {
1075 // If the senders are empty, just use the subject/snippet.
1076 return subjectSnippet;
1077 } else if (TextUtils.isEmpty(subjectSnippet)) {
1078 // If the subject/snippet is empty, just use the senders.
1079 final SpannableString spannableString = new SpannableString(senders);
1080 spannableString.setSpan(notificationPrimarySpan, 0, senders.length(), 0);
1081
1082 return spannableString;
1083 } else {
1084 final String formatString = context.getResources().getString(
1085 R.string.multiple_new_message_notification_item);
1086 final TextAppearanceSpan notificationSecondarySpan =
1087 new TextAppearanceSpan(context, R.style.NotificationSecondaryText);
1088
Andrew Sappersteinf5810962013-06-27 10:41:33 -07001089 final String instantiatedString = String.format(formatString,
1090 sBidiFormatter.unicodeWrap(senders),
1091 sBidiFormatter.unicodeWrap(subjectSnippet));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001092
1093 final SpannableString spannableString = new SpannableString(instantiatedString);
1094
1095 final boolean isOrderReversed = formatString.indexOf("%2$s") <
1096 formatString.indexOf("%1$s");
1097 final int primaryOffset =
1098 (isOrderReversed ? instantiatedString.lastIndexOf(senders) :
1099 instantiatedString.indexOf(senders));
1100 final int secondaryOffset =
1101 (isOrderReversed ? instantiatedString.lastIndexOf(subjectSnippet) :
1102 instantiatedString.indexOf(subjectSnippet));
1103 spannableString.setSpan(notificationPrimarySpan,
1104 primaryOffset, primaryOffset + senders.length(), 0);
1105 spannableString.setSpan(notificationSecondarySpan,
1106 secondaryOffset, secondaryOffset + subjectSnippet.length(), 0);
1107 return spannableString;
1108 }
1109 }
1110
1111 /**
1112 * Sets the bigtext for a notification for a single new conversation
1113 * @param context
1114 * @param subject Subject of the new message that triggered the notification
Andrew Sappersteinf5810962013-06-27 10:41:33 -07001115 * @return a {@link CharSequence} suitable for use in
1116 * {@link NotificationCompat.Builder#setContentText}
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001117 */
1118 private static CharSequence getSingleMessageLittleText(Context context, String subject) {
1119 final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
1120 context, R.style.NotificationPrimaryText);
1121
1122 final SpannableString spannableString = new SpannableString(subject);
1123 spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0);
1124
1125 return spannableString;
1126 }
1127
1128 /**
1129 * Sets the bigtext for a notification for a single new conversation
1130 *
1131 * @param context
1132 * @param subject Subject of the new message that triggered the notification
1133 * @param message the {@link Message} to be displayed.
1134 * @return a {@link CharSequence} suitable for use in
1135 * {@link android.support.v4.app.NotificationCompat.BigTextStyle}
1136 */
1137 private static CharSequence getSingleMessageBigText(Context context, String subject,
1138 final Message message) {
1139
1140 final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
1141 context, R.style.NotificationPrimaryText);
1142
1143 final String snippet = getMessageBodyWithoutElidedText(message);
1144
1145 // Change multiple newlines (with potential white space between), into a single new line
1146 final String collapsedSnippet =
1147 !TextUtils.isEmpty(snippet) ? snippet.replaceAll("\\n\\s+", "\n") : "";
1148
1149 if (TextUtils.isEmpty(subject)) {
1150 // If the subject is empty, just use the snippet.
1151 return snippet;
1152 } else if (TextUtils.isEmpty(collapsedSnippet)) {
1153 // If the snippet is empty, just use the subject.
1154 final SpannableString spannableString = new SpannableString(subject);
1155 spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0);
1156
1157 return spannableString;
1158 } else {
1159 final String notificationBigTextFormat = context.getResources().getString(
1160 R.string.single_new_message_notification_big_text);
1161
1162 // Localizers may change the order of the parameters, look at how the format
1163 // string is structured.
1164 final boolean isSubjectFirst = notificationBigTextFormat.indexOf("%2$s") >
1165 notificationBigTextFormat.indexOf("%1$s");
1166 final String bigText =
1167 String.format(notificationBigTextFormat, subject, collapsedSnippet);
1168 final SpannableString spannableString = new SpannableString(bigText);
1169
1170 final int subjectOffset =
1171 (isSubjectFirst ? bigText.indexOf(subject) : bigText.lastIndexOf(subject));
1172 spannableString.setSpan(notificationSubjectSpan,
1173 subjectOffset, subjectOffset + subject.length(), 0);
1174
1175 return spannableString;
1176 }
1177 }
1178
1179 /**
1180 * Gets the title for a notification for a single new conversation
1181 * @param context
1182 * @param sender Sender of the new message that triggered the notification.
1183 * @param subject Subject of the new message that triggered the notification
1184 * @return a {@link CharSequence} suitable for use as a {@link Notification} title.
1185 */
1186 private static CharSequence getSingleMessageNotificationTitle(Context context,
1187 String sender, String subject) {
1188
1189 if (TextUtils.isEmpty(subject)) {
1190 // If the subject is empty, just set the title to the sender's information.
1191 return sender;
1192 } else {
1193 final String notificationTitleFormat = context.getResources().getString(
1194 R.string.single_new_message_notification_title);
1195
1196 // Localizers may change the order of the parameters, look at how the format
1197 // string is structured.
1198 final boolean isSubjectLast = notificationTitleFormat.indexOf("%2$s") >
1199 notificationTitleFormat.indexOf("%1$s");
1200 final String titleString = String.format(notificationTitleFormat, sender, subject);
1201
1202 // Format the string so the subject is using the secondaryText style
1203 final SpannableString titleSpannable = new SpannableString(titleString);
1204
1205 // Find the offset of the subject.
1206 final int subjectOffset =
1207 isSubjectLast ? titleString.lastIndexOf(subject) : titleString.indexOf(subject);
1208 final TextAppearanceSpan notificationSubjectSpan =
1209 new TextAppearanceSpan(context, R.style.NotificationSecondaryText);
1210 titleSpannable.setSpan(notificationSubjectSpan,
1211 subjectOffset, subjectOffset + subject.length(), 0);
1212 return titleSpannable;
1213 }
1214 }
1215
1216 /**
Scott Kennedy7f8aed62013-05-22 18:35:17 -07001217 * Clears the notifications for the specified account/folder.
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001218 */
Scott Kennedy7f8aed62013-05-22 18:35:17 -07001219 public static void clearFolderNotification(Context context, Account account, Folder folder,
1220 final boolean markSeen) {
Scott Kennedy13a73272013-05-09 09:45:03 -07001221 LogUtils.v(LOG_TAG, "NotificationUtils: Clearing all notifications for %s/%s", account.name,
1222 folder.name);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001223 final NotificationMap notificationMap = getNotificationMap(context);
1224 final NotificationKey key = new NotificationKey(account, folder);
1225 notificationMap.remove(key);
1226 notificationMap.saveNotificationMap(context);
1227
Scott Kennedy7f8aed62013-05-22 18:35:17 -07001228 final NotificationManager notificationManager =
1229 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
1230 notificationManager.cancel(getNotificationId(account.name, folder));
1231
1232 if (markSeen) {
1233 markSeen(context, folder);
1234 }
1235 }
1236
1237 /**
1238 * Clears all notifications for the specified account.
1239 */
1240 public static void clearAccountNotifications(final Context context, final String account) {
1241 LogUtils.v(LOG_TAG, "NotificationUtils: Clearing all notifications for %s", account);
1242 final NotificationMap notificationMap = getNotificationMap(context);
1243
1244 // Find all NotificationKeys for this account
1245 final ImmutableList.Builder<NotificationKey> keyBuilder = ImmutableList.builder();
1246
1247 for (final NotificationKey key : notificationMap.keySet()) {
1248 if (account.equals(key.account.name)) {
1249 keyBuilder.add(key);
1250 }
1251 }
1252
1253 final List<NotificationKey> notificationKeys = keyBuilder.build();
1254
1255 final NotificationManager notificationManager =
1256 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
1257
1258 for (final NotificationKey notificationKey : notificationKeys) {
1259 final Folder folder = notificationKey.folder;
1260 notificationManager.cancel(getNotificationId(account, folder));
1261 notificationMap.remove(notificationKey);
1262 }
1263
1264 notificationMap.saveNotificationMap(context);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001265 }
1266
1267 private static ArrayList<Long> findContacts(Context context, Collection<String> addresses) {
1268 ArrayList<String> whereArgs = new ArrayList<String>();
1269 StringBuilder whereBuilder = new StringBuilder();
1270 String[] questionMarks = new String[addresses.size()];
1271
1272 whereArgs.addAll(addresses);
1273 Arrays.fill(questionMarks, "?");
1274 whereBuilder.append(Email.DATA1 + " IN (").
1275 append(TextUtils.join(",", questionMarks)).
1276 append(")");
1277
1278 ContentResolver resolver = context.getContentResolver();
1279 Cursor c = resolver.query(Email.CONTENT_URI,
1280 new String[]{Email.CONTACT_ID}, whereBuilder.toString(),
1281 whereArgs.toArray(new String[0]), null);
1282
1283 ArrayList<Long> contactIds = new ArrayList<Long>();
1284 if (c == null) {
1285 return contactIds;
1286 }
1287 try {
1288 while (c.moveToNext()) {
1289 contactIds.add(c.getLong(0));
1290 }
1291 } finally {
1292 c.close();
1293 }
1294 return contactIds;
1295 }
1296
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001297 private static Bitmap getContactIcon(final Context context, final String displayName,
1298 final String senderAddress, final Folder folder) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001299 if (senderAddress == null) {
1300 return null;
1301 }
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001302
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001303 Bitmap icon = null;
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001304
1305 final List<Long> contactIds = findContacts( context, Arrays.asList(
1306 new String[] { senderAddress }));
1307
1308 // Get the ideal size for this icon.
1309 final Resources res = context.getResources();
1310 final int idealIconHeight =
1311 res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
1312 final int idealIconWidth =
1313 res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001314
1315 if (contactIds != null) {
Scott Kennedy4046d062013-06-27 14:46:09 -07001316 for (final long id : contactIds) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001317 final Uri contactUri =
1318 ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id);
1319 final Uri photoUri = Uri.withAppendedPath(contactUri, Photo.CONTENT_DIRECTORY);
1320 final Cursor cursor = context.getContentResolver().query(
1321 photoUri, new String[] { Photo.PHOTO }, null, null, null);
1322
1323 if (cursor != null) {
1324 try {
1325 if (cursor.moveToFirst()) {
Scott Kennedy4046d062013-06-27 14:46:09 -07001326 final byte[] data = cursor.getBlob(0);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001327 if (data != null) {
1328 icon = BitmapFactory.decodeStream(new ByteArrayInputStream(data));
1329 if (icon != null && icon.getHeight() < idealIconHeight) {
1330 // We should scale this image to fit the intended size
1331 icon = Bitmap.createScaledBitmap(
1332 icon, idealIconWidth, idealIconHeight, true);
1333 }
1334 if (icon != null) {
1335 break;
1336 }
1337 }
1338 }
1339 } finally {
1340 cursor.close();
1341 }
1342 }
1343 }
1344 }
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001345
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001346 if (icon == null) {
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001347 // Make a colorful tile!
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001348 final Dimensions dimensions = new Dimensions(idealIconWidth, idealIconHeight,
1349 Dimensions.SCALE_ONE);
1350
Scott Kennedyfe235122013-06-28 12:06:36 -07001351 icon = new LetterTileProvider(context).getLetterTile(dimensions,
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001352 displayName, senderAddress);
1353 }
1354
1355 if (icon == null) {
1356 // Icon should be the default mail icon.
Scott Kennedy61bd0e82012-12-10 18:18:17 -08001357 icon = getDefaultNotificationIcon(context, folder, false /* single new message */);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001358 }
1359 return icon;
1360 }
1361
1362 private static String getMessageBodyWithoutElidedText(final Message message) {
Scott Kennedy5e7e88b2013-03-07 16:42:12 -08001363 return getMessageBodyWithoutElidedText(message.getBodyAsHtml());
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001364 }
1365
1366 public static String getMessageBodyWithoutElidedText(String html) {
1367 if (TextUtils.isEmpty(html)) {
1368 return "";
1369 }
1370 // Get the html "tree" for this message body
1371 final HtmlTree htmlTree = com.android.mail.utils.Utils.getHtmlTree(html);
1372 htmlTree.setPlainTextConverterFactory(MESSAGE_CONVERTER_FACTORY);
1373
1374 return htmlTree.getPlainText();
1375 }
1376
1377 public static void markSeen(final Context context, final Folder folder) {
1378 final Uri uri = folder.uri;
1379
1380 final ContentValues values = new ContentValues(1);
1381 values.put(UIProvider.ConversationColumns.SEEN, 1);
1382
1383 context.getContentResolver().update(uri, values, null, null);
1384 }
1385
1386 /**
1387 * Returns a displayable string representing
1388 * the message sender. It has a preference toward showing the name,
1389 * but will fall back to the address if that is all that is available.
1390 */
1391 private static String getDisplayableSender(String sender) {
1392 final EmailAddress address = EmailAddress.getEmailAddress(sender);
1393
1394 String displayableSender = address.getName();
Scott Kennedy390cab92013-05-23 14:29:49 -07001395
1396 if (!TextUtils.isEmpty(displayableSender)) {
1397 return Address.decodeAddressName(displayableSender);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001398 }
Scott Kennedy390cab92013-05-23 14:29:49 -07001399
1400 // If that fails, default to the sender address.
1401 displayableSender = address.getAddress();
1402
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001403 // If we were unable to tokenize a name or address,
1404 // just use whatever was in the sender.
1405 if (TextUtils.isEmpty(displayableSender)) {
1406 displayableSender = sender;
1407 }
1408 return displayableSender;
1409 }
1410
1411 /**
1412 * Returns only the address portion of a message sender.
1413 */
1414 private static String getSenderAddress(String sender) {
1415 final EmailAddress address = EmailAddress.getEmailAddress(sender);
1416
1417 String tokenizedAddress = address.getAddress();
1418
1419 // If we were unable to tokenize a name or address,
1420 // just use whatever was in the sender.
1421 if (TextUtils.isEmpty(tokenizedAddress)) {
1422 tokenizedAddress = sender;
1423 }
1424 return tokenizedAddress;
1425 }
1426
1427 public static int getNotificationId(final String account, final Folder folder) {
1428 return 1 ^ account.hashCode() ^ folder.hashCode();
1429 }
1430
1431 private static class NotificationKey {
1432 public final Account account;
1433 public final Folder folder;
1434
1435 public NotificationKey(Account account, Folder folder) {
1436 this.account = account;
1437 this.folder = folder;
1438 }
1439
1440 @Override
1441 public boolean equals(Object other) {
1442 if (!(other instanceof NotificationKey)) {
1443 return false;
1444 }
1445 NotificationKey key = (NotificationKey) other;
1446 return account.equals(key.account) && folder.equals(key.folder);
1447 }
1448
1449 @Override
1450 public String toString() {
Scott Kennedye8a7c4c2013-05-09 11:56:50 -07001451 return account.name + " " + folder.name;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001452 }
1453
1454 @Override
1455 public int hashCode() {
1456 final int accountHashCode = account.hashCode();
1457 final int folderHashCode = folder.hashCode();
1458 return accountHashCode ^ folderHashCode;
1459 }
1460 }
1461
1462 /**
1463 * Contains the logic for converting the contents of one HtmlTree into
1464 * plaintext.
1465 */
1466 public static class MailMessagePlainTextConverter extends HtmlTree.DefaultPlainTextConverter {
1467 // Strings for parsing html message bodies
1468 private static final String ELIDED_TEXT_ELEMENT_NAME = "div";
1469 private static final String ELIDED_TEXT_ELEMENT_ATTRIBUTE_NAME = "class";
1470 private static final String ELIDED_TEXT_ELEMENT_ATTRIBUTE_CLASS_VALUE = "elided-text";
1471
1472 private static final HTML.Attribute ELIDED_TEXT_ATTRIBUTE =
1473 new HTML.Attribute(ELIDED_TEXT_ELEMENT_ATTRIBUTE_NAME, HTML.Attribute.NO_TYPE);
1474
1475 private static final HtmlDocument.Node ELIDED_TEXT_REPLACEMENT_NODE =
1476 HtmlDocument.createSelfTerminatingTag(HTML4.BR_ELEMENT, null, null, null);
1477
1478 private int mEndNodeElidedTextBlock = -1;
1479
1480 @Override
1481 public void addNode(HtmlDocument.Node n, int nodeNum, int endNum) {
1482 // If we are in the middle of an elided text block, don't add this node
1483 if (nodeNum < mEndNodeElidedTextBlock) {
1484 return;
1485 } else if (nodeNum == mEndNodeElidedTextBlock) {
1486 super.addNode(ELIDED_TEXT_REPLACEMENT_NODE, nodeNum, endNum);
1487 return;
1488 }
1489
1490 // If this tag starts another elided text block, we want to remember the end
1491 if (n instanceof HtmlDocument.Tag) {
1492 boolean foundElidedTextTag = false;
1493 final HtmlDocument.Tag htmlTag = (HtmlDocument.Tag)n;
1494 final HTML.Element htmlElement = htmlTag.getElement();
1495 if (ELIDED_TEXT_ELEMENT_NAME.equals(htmlElement.getName())) {
1496 // Make sure that the class is what is expected
1497 final List<HtmlDocument.TagAttribute> attributes =
1498 htmlTag.getAttributes(ELIDED_TEXT_ATTRIBUTE);
1499 for (HtmlDocument.TagAttribute attribute : attributes) {
1500 if (ELIDED_TEXT_ELEMENT_ATTRIBUTE_CLASS_VALUE.equals(
1501 attribute.getValue())) {
1502 // Found an "elided-text" div. Remember information about this tag
1503 mEndNodeElidedTextBlock = endNum;
1504 foundElidedTextTag = true;
1505 break;
1506 }
1507 }
1508 }
1509
1510 if (foundElidedTextTag) {
1511 return;
1512 }
1513 }
1514
Scott Kennedyc56b2332013-05-23 18:24:53 -07001515 super.addNode(n, nodeNum, endNum);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001516 }
1517 }
1518
1519 /**
1520 * During account setup in Email, we may not have an inbox yet, so the notification setting had
1521 * to be stored in {@link AccountPreferences}. If it is still there, we need to move it to the
1522 * {@link FolderPreferences} now.
1523 */
1524 public static void moveNotificationSetting(final AccountPreferences accountPreferences,
1525 final FolderPreferences folderPreferences) {
1526 if (accountPreferences.isDefaultInboxNotificationsEnabledSet()) {
1527 // If this setting has been changed some other way, don't overwrite it
1528 if (!folderPreferences.isNotificationsEnabledSet()) {
1529 final boolean notificationsEnabled =
1530 accountPreferences.getDefaultInboxNotificationsEnabled();
1531
1532 folderPreferences.setNotificationsEnabled(notificationsEnabled);
1533 }
1534
1535 accountPreferences.clearDefaultInboxNotificationsEnabled();
1536 }
1537 }
1538}