blob: 28b899f632adac5455977e67334a96d6ad07563f [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 Kennedyfe235122013-06-28 12:06:36 -070049import com.android.mail.photomanager.LetterTileProvider;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080050import com.android.mail.preferences.AccountPreferences;
51import com.android.mail.preferences.FolderPreferences;
52import com.android.mail.preferences.MailPrefs;
53import com.android.mail.providers.Account;
Scott Kennedy390cab92013-05-23 14:29:49 -070054import com.android.mail.providers.Address;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080055import com.android.mail.providers.Conversation;
56import com.android.mail.providers.Folder;
57import com.android.mail.providers.Message;
58import com.android.mail.providers.UIProvider;
Scott Kennedyc94c80c2013-06-27 14:42:41 -070059import com.android.mail.ui.ImageCanvas.Dimensions;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080060import com.android.mail.utils.NotificationActionUtils.NotificationAction;
Scott Kennedy09400ef2013-03-07 11:09:47 -080061import com.google.android.common.html.parser.HTML;
62import com.google.android.common.html.parser.HTML4;
63import com.google.android.common.html.parser.HtmlDocument;
64import com.google.android.common.html.parser.HtmlTree;
Andrew Sappersteinddd17bc2013-04-22 14:03:40 -070065import com.google.common.base.Objects;
Scott Kennedy7f8aed62013-05-22 18:35:17 -070066import com.google.common.collect.ImmutableList;
Scott Kennedy09400ef2013-03-07 11:09:47 -080067import com.google.common.collect.Lists;
Scott Kennedy09400ef2013-03-07 11:09:47 -080068import com.google.common.collect.Sets;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080069
70import java.io.ByteArrayInputStream;
71import java.util.ArrayList;
72import java.util.Arrays;
73import java.util.Collection;
74import java.util.List;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080075import java.util.Set;
76import java.util.concurrent.ConcurrentHashMap;
77
78public class NotificationUtils {
Scott Kennedyc7d75472013-07-08 18:21:28 -070079 public static final String LOG_TAG = "NotifUtils";
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080080
81 /** Contains a list of <(account, label), unread conversations> */
82 private static NotificationMap sActiveNotificationMap = null;
83
Scott Kennedy61bd0e82012-12-10 18:18:17 -080084 private static final SparseArray<Bitmap> sNotificationIcons = new SparseArray<Bitmap>();
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080085
86 private static TextAppearanceSpan sNotificationUnreadStyleSpan;
87 private static CharacterStyle sNotificationReadStyleSpan;
88
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080089 /** A factory that produces a plain text converter that removes elided text. */
90 private static final HtmlTree.PlainTextConverterFactory MESSAGE_CONVERTER_FACTORY =
91 new HtmlTree.PlainTextConverterFactory() {
92 @Override
93 public HtmlTree.PlainTextConverter createInstance() {
94 return new MailMessagePlainTextConverter();
95 }
96 };
97
Andrew Sappersteinf5810962013-06-27 10:41:33 -070098 private static final BidiFormatter sBidiFormatter = BidiFormatter.getInstance();
99
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800100 /**
101 * Clears all notifications in response to the user tapping "Clear" in the status bar.
102 */
103 public static void clearAllNotfications(Context context) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700104 LogUtils.v(LOG_TAG, "Clearing all notifications.");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800105 final NotificationMap notificationMap = getNotificationMap(context);
106 notificationMap.clear();
107 notificationMap.saveNotificationMap(context);
108 }
109
110 /**
111 * Returns the notification map, creating it if necessary.
112 */
113 private static synchronized NotificationMap getNotificationMap(Context context) {
114 if (sActiveNotificationMap == null) {
115 sActiveNotificationMap = new NotificationMap();
116
117 // populate the map from the cached data
118 sActiveNotificationMap.loadNotificationMap(context);
119 }
120 return sActiveNotificationMap;
121 }
122
123 /**
124 * Class representing the existing notifications, and the number of unread and
125 * unseen conversations that triggered each.
126 */
127 private static class NotificationMap
128 extends ConcurrentHashMap<NotificationKey, Pair<Integer, Integer>> {
129
130 private static final String NOTIFICATION_PART_SEPARATOR = " ";
131 private static final int NUM_NOTIFICATION_PARTS= 4;
132
133 /**
134 * Retuns the unread count for the given NotificationKey.
135 */
136 public Integer getUnread(NotificationKey key) {
137 final Pair<Integer, Integer> value = get(key);
138 return value != null ? value.first : null;
139 }
140
141 /**
142 * Retuns the unread unseen count for the given NotificationKey.
143 */
144 public Integer getUnseen(NotificationKey key) {
145 final Pair<Integer, Integer> value = get(key);
146 return value != null ? value.second : null;
147 }
148
149 /**
150 * Store the unread and unseen value for the given NotificationKey
151 */
152 public void put(NotificationKey key, int unread, int unseen) {
153 final Pair<Integer, Integer> value =
154 new Pair<Integer, Integer>(Integer.valueOf(unread), Integer.valueOf(unseen));
155 put(key, value);
156 }
157
158 /**
159 * Populates the notification map with previously cached data.
160 */
161 public synchronized void loadNotificationMap(final Context context) {
162 final MailPrefs mailPrefs = MailPrefs.get(context);
163 final Set<String> notificationSet = mailPrefs.getActiveNotificationSet();
164 if (notificationSet != null) {
165 for (String notificationEntry : notificationSet) {
166 // Get the parts of the string that make the notification entry
167 final String[] notificationParts =
168 TextUtils.split(notificationEntry, NOTIFICATION_PART_SEPARATOR);
169 if (notificationParts.length == NUM_NOTIFICATION_PARTS) {
170 final Uri accountUri = Uri.parse(notificationParts[0]);
171 final Cursor accountCursor = context.getContentResolver().query(
172 accountUri, UIProvider.ACCOUNTS_PROJECTION, null, null, null);
173 final Account account;
174 try {
175 if (accountCursor.moveToFirst()) {
176 account = new Account(accountCursor);
177 } else {
178 continue;
179 }
180 } finally {
181 accountCursor.close();
182 }
183
184 final Uri folderUri = Uri.parse(notificationParts[1]);
185 final Cursor folderCursor = context.getContentResolver().query(
186 folderUri, UIProvider.FOLDERS_PROJECTION, null, null, null);
187 final Folder folder;
188 try {
189 if (folderCursor.moveToFirst()) {
190 folder = new Folder(folderCursor);
191 } else {
192 continue;
193 }
194 } finally {
195 folderCursor.close();
196 }
197
198 final NotificationKey key = new NotificationKey(account, folder);
199 final Integer unreadValue = Integer.valueOf(notificationParts[2]);
200 final Integer unseenValue = Integer.valueOf(notificationParts[3]);
201 final Pair<Integer, Integer> unreadUnseenValue =
202 new Pair<Integer, Integer>(unreadValue, unseenValue);
203 put(key, unreadUnseenValue);
204 }
205 }
206 }
207 }
208
209 /**
210 * Cache the notification map.
211 */
212 public synchronized void saveNotificationMap(Context context) {
213 final Set<String> notificationSet = Sets.newHashSet();
214 final Set<NotificationKey> keys = keySet();
215 for (NotificationKey key : keys) {
216 final Pair<Integer, Integer> value = get(key);
217 final Integer unreadCount = value.first;
218 final Integer unseenCount = value.second;
Scott Kennedyff8553f2013-04-05 20:57:44 -0700219 if (unreadCount != null && unseenCount != null) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800220 final String[] partValues = new String[] {
Scott Kennedy259df5b2013-07-11 13:24:01 -0700221 key.account.uri.toString(), key.folder.folderUri.fullUri.toString(),
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800222 unreadCount.toString(), unseenCount.toString()};
223 notificationSet.add(TextUtils.join(NOTIFICATION_PART_SEPARATOR, partValues));
224 }
225 }
226 final MailPrefs mailPrefs = MailPrefs.get(context);
227 mailPrefs.cacheActiveNotificationSet(notificationSet);
228 }
229 }
230
231 /**
232 * @return the title of this notification with each account and the number of unread and unseen
233 * conversations for it. Also remove any account in the map that has 0 unread.
234 */
235 private static String createNotificationString(NotificationMap notifications) {
236 StringBuilder result = new StringBuilder();
237 int i = 0;
238 Set<NotificationKey> keysToRemove = Sets.newHashSet();
239 for (NotificationKey key : notifications.keySet()) {
240 Integer unread = notifications.getUnread(key);
241 Integer unseen = notifications.getUnseen(key);
242 if (unread == null || unread.intValue() == 0) {
243 keysToRemove.add(key);
244 } else {
245 if (i > 0) result.append(", ");
246 result.append(key.toString() + " (" + unread + ", " + unseen + ")");
247 i++;
248 }
249 }
250
251 for (NotificationKey key : keysToRemove) {
252 notifications.remove(key);
253 }
254
255 return result.toString();
256 }
257
258 /**
259 * Get all notifications for all accounts and cancel them.
260 **/
261 public static void cancelAllNotifications(Context context) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700262 LogUtils.d(LOG_TAG, "cancelAllNotifications - cancelling all");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800263 NotificationManager nm = (NotificationManager) context.getSystemService(
264 Context.NOTIFICATION_SERVICE);
265 nm.cancelAll();
266 clearAllNotfications(context);
267 }
268
269 /**
270 * Get all notifications for all accounts, cancel them, and repost.
271 * This happens when locale changes.
272 **/
273 public static void cancelAndResendNotifications(Context context) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700274 LogUtils.d(LOG_TAG, "cancelAndResendNotifications");
Andrew Sappersteinddd17bc2013-04-22 14:03:40 -0700275 resendNotifications(context, true, null, null);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800276 }
277
278 /**
279 * Get all notifications for all accounts, optionally cancel them, and repost.
Andrew Sappersteinddd17bc2013-04-22 14:03:40 -0700280 * This happens when locale changes. If you only want to resend messages from one
281 * account-folder pair, pass in the account and folder that should be resent.
282 * All other account-folder pairs will not have their notifications resent.
283 * All notifications will be resent if account or folder is null.
284 *
285 * @param context Current context.
286 * @param cancelExisting True, if all notifications should be canceled before resending.
287 * False, otherwise.
288 * @param accountUri The {@link Uri} of the {@link Account} of the notification
289 * upon which an action occurred.
290 * @param folderUri The {@link Uri} of the {@link Folder} of the notification
291 * upon which an action occurred.
292 */
293 public static void resendNotifications(Context context, final boolean cancelExisting,
Scott Kennedy259df5b2013-07-11 13:24:01 -0700294 final Uri accountUri, final FolderUri folderUri) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700295 LogUtils.d(LOG_TAG, "resendNotifications ");
Scott Kennedy2638b482013-05-06 16:05:11 -0700296
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800297 if (cancelExisting) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700298 LogUtils.d(LOG_TAG, "resendNotifications - cancelling all");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800299 NotificationManager nm =
300 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
301 nm.cancelAll();
302 }
303 // Re-validate the notifications.
304 final NotificationMap notificationMap = getNotificationMap(context);
305 final Set<NotificationKey> keys = notificationMap.keySet();
306 for (NotificationKey notification : keys) {
307 final Folder folder = notification.folder;
Andrew Sappersteinddd17bc2013-04-22 14:03:40 -0700308 final int notificationId = getNotificationId(notification.account.name,
309 folder);
310
311 // Only resend notifications if the notifications are from the same folder
312 // and same account as the undo notification that was previously displayed.
Scott Kennedy26b29ef2013-04-28 18:36:50 -0700313 if (accountUri != null && !Objects.equal(accountUri, notification.account.uri) &&
Scott Kennedy259df5b2013-07-11 13:24:01 -0700314 folderUri != null && !Objects.equal(folderUri, folder.folderUri)) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700315 LogUtils.d(LOG_TAG, "resendNotifications - not resending %s / %s"
Scott Kennedy2638b482013-05-06 16:05:11 -0700316 + " because it doesn't match %s / %s",
Scott Kennedy259df5b2013-07-11 13:24:01 -0700317 notification.account.uri, folder.folderUri, accountUri, folderUri);
Andrew Sappersteinddd17bc2013-04-22 14:03:40 -0700318 continue;
319 }
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800320
Scott Kennedy2f97af92013-07-30 10:42:34 -0700321 LogUtils.d(LOG_TAG, "resendNotifications - resending %s / %s",
Scott Kennedy259df5b2013-07-11 13:24:01 -0700322 notification.account.uri, folder.folderUri);
Scott Kennedy2638b482013-05-06 16:05:11 -0700323
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800324 final NotificationAction undoableAction =
325 NotificationActionUtils.sUndoNotifications.get(notificationId);
326 if (undoableAction == null) {
Andrew Sappersteinddd17bc2013-04-22 14:03:40 -0700327 validateNotifications(context, folder, notification.account, true,
328 false, notification);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800329 } else {
330 // Create an undo notification
331 NotificationActionUtils.createUndoNotification(context, undoableAction);
332 }
333 }
334 }
335
336 /**
337 * Validate the notifications for the specified account.
338 */
339 public static void validateAccountNotifications(Context context, String account) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700340 LogUtils.d(LOG_TAG, "validateAccountNotifications - %s", account);
Scott Kennedy4162f832013-05-06 17:37:55 -0700341
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800342 List<NotificationKey> notificationsToCancel = Lists.newArrayList();
343 // Iterate through the notification map to see if there are any entries that correspond to
344 // labels that are not in the sync set.
345 final NotificationMap notificationMap = getNotificationMap(context);
346 Set<NotificationKey> keys = notificationMap.keySet();
347 final AccountPreferences accountPreferences = new AccountPreferences(context, account);
348 final boolean enabled = accountPreferences.areNotificationsEnabled();
349 if (!enabled) {
350 // Cancel all notifications for this account
351 for (NotificationKey notification : keys) {
352 if (notification.account.name.equals(account)) {
353 notificationsToCancel.add(notification);
354 }
355 }
356 } else {
357 // Iterate through the notification map to see if there are any entries that
358 // correspond to labels that are not in the notification set.
359 for (NotificationKey notification : keys) {
360 if (notification.account.name.equals(account)) {
361 // If notification is not enabled for this label, remember this NotificationKey
362 // to later cancel the notification, and remove the entry from the map
363 final Folder folder = notification.folder;
Scott Kennedy259df5b2013-07-11 13:24:01 -0700364 final boolean isInbox = folder.folderUri.equals(
365 notification.account.settings.defaultInbox);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800366 final FolderPreferences folderPreferences = new FolderPreferences(
367 context, notification.account.name, folder, isInbox);
368
369 if (!folderPreferences.areNotificationsEnabled()) {
370 notificationsToCancel.add(notification);
371 }
372 }
373 }
374 }
375
376 // Cancel & remove the invalid notifications.
377 if (notificationsToCancel.size() > 0) {
378 NotificationManager nm = (NotificationManager) context.getSystemService(
379 Context.NOTIFICATION_SERVICE);
380 for (NotificationKey notification : notificationsToCancel) {
381 final Folder folder = notification.folder;
382 final int notificationId = getNotificationId(notification.account.name, folder);
Scott Kennedy2f97af92013-07-30 10:42:34 -0700383 LogUtils.d(LOG_TAG, "validateAccountNotifications - cancelling %s / %s",
Scott Kennedy93486c02013-04-17 12:15:43 -0700384 notification.account.name, folder.persistentId);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800385 nm.cancel(notificationId);
386 notificationMap.remove(notification);
387 NotificationActionUtils.sUndoNotifications.remove(notificationId);
388 NotificationActionUtils.sNotificationTimestamps.delete(notificationId);
389 }
390 notificationMap.saveNotificationMap(context);
391 }
392 }
393
394 /**
395 * Display only one notification.
396 */
397 public static void setNewEmailIndicator(Context context, final int unreadCount,
398 final int unseenCount, final Account account, final Folder folder,
399 final boolean getAttention) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700400 LogUtils.d(LOG_TAG, "setNewEmailIndicator unreadCount = %d, unseenCount = %d, account = %s,"
401 + " folder = %s, getAttention = %b", unreadCount, unseenCount, account.name,
402 folder.folderUri, getAttention);
Scott Kennedy4162f832013-05-06 17:37:55 -0700403
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800404 boolean ignoreUnobtrusiveSetting = false;
405
Scott Kennedydab8a942013-02-22 12:31:30 -0800406 final int notificationId = getNotificationId(account.name, folder);
407
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800408 // Update the notification map
409 final NotificationMap notificationMap = getNotificationMap(context);
410 final NotificationKey key = new NotificationKey(account, folder);
411 if (unreadCount == 0) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700412 LogUtils.d(LOG_TAG, "setNewEmailIndicator - cancelling %s / %s", account.name,
413 folder.persistentId);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800414 notificationMap.remove(key);
Scott Kennedydab8a942013-02-22 12:31:30 -0800415 ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
416 .cancel(notificationId);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800417 } else {
418 if (!notificationMap.containsKey(key)) {
419 // This account previously didn't have any unread mail; ignore the "unobtrusive
420 // notifications" setting and play sound and/or vibrate the device even if a
421 // notification already exists (bug 2412348).
422 ignoreUnobtrusiveSetting = true;
423 }
424 notificationMap.put(key, unreadCount, unseenCount);
425 }
426 notificationMap.saveNotificationMap(context);
427
428 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700429 LogUtils.v(LOG_TAG, "New email: %s mapSize: %d getAttention: %b",
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800430 createNotificationString(notificationMap), notificationMap.size(),
431 getAttention);
432 }
433
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800434 if (NotificationActionUtils.sUndoNotifications.get(notificationId) == null) {
435 validateNotifications(context, folder, account, getAttention, ignoreUnobtrusiveSetting,
436 key);
437 }
438 }
439
440 /**
441 * Validate the notifications notification.
442 */
443 private static void validateNotifications(Context context, final Folder folder,
444 final Account account, boolean getAttention, boolean ignoreUnobtrusiveSetting,
445 NotificationKey key) {
446
447 NotificationManager nm = (NotificationManager)
448 context.getSystemService(Context.NOTIFICATION_SERVICE);
449
450 final NotificationMap notificationMap = getNotificationMap(context);
451 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700452 LogUtils.i(LOG_TAG, "Validating Notification: %s mapSize: %d "
Scott Kennedy13a73272013-05-09 09:45:03 -0700453 + "folder: %s getAttention: %b", createNotificationString(notificationMap),
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800454 notificationMap.size(), folder.name, getAttention);
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700455 } else {
456 LogUtils.i(LOG_TAG, "Validating Notification, mapSize: %d "
457 + "getAttention: %b", notificationMap.size(), getAttention);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800458 }
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.
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700480 LogUtils.i(LOG_TAG,
481 "The cursor is null, so the specified folder probably does not exist");
Yu Ping Hu3a7b45b2013-06-18 18:10:50 -0700482 clearFolderNotification(context, account, folder, false);
483 return;
484 }
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800485 final int cursorUnseenCount = cursor.getCount();
486
487 // Make sure the unseen count matches the number of items in the cursor. But, we don't
488 // want to overwrite a 0 unseen count that was specified in the intent
489 if (unseenCount != 0 && unseenCount != cursorUnseenCount) {
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700490 LogUtils.i(LOG_TAG,
Scott Kennedy2f97af92013-07-30 10:42:34 -0700491 "Unseen count doesn't match cursor count. unseen: %d cursor count: %d",
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800492 unseenCount, cursorUnseenCount);
493 unseenCount = cursorUnseenCount;
494 }
495
496 // For the purpose of the notifications, the unseen count should be capped at the num of
497 // unread conversations.
498 if (unseenCount > unreadCount) {
499 unseenCount = unreadCount;
500 }
501
502 final int notificationId = getNotificationId(account.name, folder);
503
504 if (unseenCount == 0) {
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700505 LogUtils.i(LOG_TAG, "validateNotifications - cancelling account %s / folder %s",
506 LogUtils.sanitizeName(LOG_TAG, account.name),
507 LogUtils.sanitizeName(LOG_TAG, folder.persistentId));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800508 nm.cancel(notificationId);
509 return;
510 }
511
512 // We now have all we need to create the notification and the pending intent
513 PendingIntent clickIntent;
514
515 NotificationCompat.Builder notification = new NotificationCompat.Builder(context);
516 notification.setSmallIcon(R.drawable.stat_notify_email);
517 notification.setTicker(account.name);
518
519 final long when;
520
521 final long oldWhen =
522 NotificationActionUtils.sNotificationTimestamps.get(notificationId);
523 if (oldWhen != 0) {
524 when = oldWhen;
525 } else {
526 when = System.currentTimeMillis();
527 }
528
529 notification.setWhen(when);
530
531 // The timestamp is now stored in the notification, so we can remove it from here
532 NotificationActionUtils.sNotificationTimestamps.delete(notificationId);
533
534 // Dispatch a CLEAR_NEW_MAIL_NOTIFICATIONS intent if the user taps the "X" next to a
535 // notification. Also this intent gets fired when the user taps on a notification as
536 // the AutoCancel flag has been set
537 final Intent cancelNotificationIntent =
538 new Intent(MailIntentService.ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS);
539 cancelNotificationIntent.setPackage(context.getPackageName());
Scott Kennedy09400ef2013-03-07 11:09:47 -0800540 cancelNotificationIntent.setData(Utils.appendVersionQueryParameter(context,
Scott Kennedy259df5b2013-07-11 13:24:01 -0700541 folder.folderUri.fullUri));
Scott Kennedy48cfe462013-04-10 11:32:02 -0700542 cancelNotificationIntent.putExtra(Utils.EXTRA_ACCOUNT, account);
543 cancelNotificationIntent.putExtra(Utils.EXTRA_FOLDER, folder);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800544
545 notification.setDeleteIntent(PendingIntent.getService(
546 context, notificationId, cancelNotificationIntent, 0));
547
548 // Ensure that the notification is cleared when the user selects it
549 notification.setAutoCancel(true);
550
551 boolean eventInfoConfigured = false;
552
Scott Kennedy259df5b2013-07-11 13:24:01 -0700553 final boolean isInbox = folder.folderUri.equals(account.settings.defaultInbox);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800554 final FolderPreferences folderPreferences =
555 new FolderPreferences(context, account.name, folder, isInbox);
556
557 if (isInbox) {
558 final AccountPreferences accountPreferences =
559 new AccountPreferences(context, account.name);
560 moveNotificationSetting(accountPreferences, folderPreferences);
561 }
562
563 if (!folderPreferences.areNotificationsEnabled()) {
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700564 LogUtils.i(LOG_TAG, "Notifications are disabled for this folder; not notifying");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800565 // Don't notify
566 return;
567 }
568
569 if (unreadCount > 0) {
570 // How can I order this properly?
571 if (cursor.moveToNext()) {
Scott Kennedybd8acad2013-05-29 10:22:25 -0700572 final Intent notificationIntent;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800573
Scott Kennedybd8acad2013-05-29 10:22:25 -0700574 // Launch directly to the conversation, if there is only 1 unseen conversation
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800575 if (unseenCount == 1) {
Scott Kennedy09400ef2013-03-07 11:09:47 -0800576 notificationIntent = createViewConversationIntent(context, account, folder,
577 cursor);
Scott Kennedybd8acad2013-05-29 10:22:25 -0700578 } else {
579 notificationIntent = createViewConversationIntent(context, account, folder,
580 null);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800581 }
582
583 if (notificationIntent == null) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700584 LogUtils.e(LOG_TAG, "Null intent when building notification");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800585 return;
586 }
587
588 clickIntent = PendingIntent.getActivity(context, -1, notificationIntent, 0);
589 configureLatestEventInfoFromConversation(context, account, folderPreferences,
590 notification, cursor, clickIntent, notificationIntent,
591 account.name, unreadCount, unseenCount, folder, when);
592 eventInfoConfigured = true;
593 }
594 }
595
596 final boolean vibrate = folderPreferences.isNotificationVibrateEnabled();
597 final String ringtoneUri = folderPreferences.getNotificationRingtoneUri();
598 final boolean notifyOnce = !folderPreferences.isEveryMessageNotificationEnabled();
599
Scott Kennedyff8553f2013-04-05 20:57:44 -0700600 if (!ignoreUnobtrusiveSetting && notifyOnce) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800601 // If the user has "unobtrusive notifications" enabled, only alert the first time
602 // new mail is received in this account. This is the default behavior. See
603 // bugs 2412348 and 2413490.
604 notification.setOnlyAlertOnce(true);
605 }
606
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700607 LogUtils.i(LOG_TAG, "Account: %s vibrate: %s",
608 LogUtils.sanitizeName(LOG_TAG, account.name),
Scott Kennedyff8553f2013-04-05 20:57:44 -0700609 Boolean.toString(folderPreferences.isNotificationVibrateEnabled()));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800610
611 int defaults = 0;
612
613 /*
614 * We do not want to notify if this is coming back from an Undo notification, hence the
615 * oldWhen check.
616 */
Scott Kennedyff8553f2013-04-05 20:57:44 -0700617 if (getAttention && oldWhen == 0) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800618 final AccountPreferences accountPreferences =
619 new AccountPreferences(context, account.name);
620 if (accountPreferences.areNotificationsEnabled()) {
621 if (vibrate) {
622 defaults |= Notification.DEFAULT_VIBRATE;
623 }
624
625 notification.setSound(TextUtils.isEmpty(ringtoneUri) ? null
626 : Uri.parse(ringtoneUri));
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700627 LogUtils.i(LOG_TAG, "New email in %s vibrateWhen: %s, playing notification: %s",
628 LogUtils.sanitizeName(LOG_TAG, account.name), vibrate, ringtoneUri);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800629 }
630 }
631
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700632 // 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 -0800633 if (eventInfoConfigured) {
634 defaults |= Notification.DEFAULT_LIGHTS;
635 notification.setDefaults(defaults);
636
637 if (oldWhen != 0) {
638 // We do not want to display the ticker again if we are re-displaying this
639 // notification (like from an Undo notification)
640 notification.setTicker(null);
641 }
642
643 nm.notify(notificationId, notification.build());
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700644 } else {
645 LogUtils.i(LOG_TAG, "event info not configured - not notifying");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800646 }
647 } finally {
648 if (cursor != null) {
649 cursor.close();
650 }
651 }
652 }
653
654 /**
655 * @return an {@link Intent} which, if launched, will display the corresponding conversation
656 */
Scott Kennedy09400ef2013-03-07 11:09:47 -0800657 private static Intent createViewConversationIntent(final Context context, final Account account,
658 final Folder folder, final Cursor cursor) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800659 if (folder == null || account == null) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700660 LogUtils.e(LOG_TAG, "createViewConversationIntent(): "
Scott Kennedy13a73272013-05-09 09:45:03 -0700661 + "Null account or folder. account: %s folder: %s", account, folder);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800662 return null;
663 }
664
665 final Intent intent;
666
667 if (cursor == null) {
Scott Kennedy259df5b2013-07-11 13:24:01 -0700668 intent = Utils.createViewFolderIntent(context, folder.folderUri.fullUri, account);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800669 } else {
670 // A conversation cursor has been specified, so this intent is intended to be go
671 // directly to the one new conversation
672
673 // Get the Conversation object
674 final Conversation conversation = new Conversation(cursor);
Scott Kennedy259df5b2013-07-11 13:24:01 -0700675 intent = Utils.createViewConversationIntent(context, conversation,
676 folder.folderUri.fullUri, account);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800677 }
678
679 return intent;
680 }
681
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800682 private static Bitmap getDefaultNotificationIcon(
683 final Context context, final Folder folder, final boolean multipleNew) {
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700684 final int resId;
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800685 if (folder.notificationIconResId != 0) {
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700686 resId = folder.notificationIconResId;
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800687 } else if (multipleNew) {
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700688 resId = R.drawable.ic_notification_multiple_mail_holo_dark;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800689 } else {
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700690 resId = R.drawable.ic_contact_picture;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800691 }
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700692
693 final Bitmap icon = getIcon(context, resId);
694
695 if (icon == null) {
696 LogUtils.e(LOG_TAG, "Couldn't decode notif icon res id %d", resId);
697 }
698
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800699 return icon;
700 }
701
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800702 private static Bitmap getIcon(final Context context, final int resId) {
703 final Bitmap cachedIcon = sNotificationIcons.get(resId);
704 if (cachedIcon != null) {
705 return cachedIcon;
706 }
707
708 final Bitmap icon = BitmapFactory.decodeResource(context.getResources(), resId);
709 sNotificationIcons.put(resId, icon);
710
711 return icon;
712 }
713
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800714 private static void configureLatestEventInfoFromConversation(final Context context,
715 final Account account, final FolderPreferences folderPreferences,
716 final NotificationCompat.Builder notification, final Cursor conversationCursor,
717 final PendingIntent clickIntent, final Intent notificationIntent,
718 final String notificationAccount, final int unreadCount, final int unseenCount,
719 final Folder folder, final long when) {
720 final Resources res = context.getResources();
721
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700722 LogUtils.i(LOG_TAG, "Showing notification with unreadCount of %d and unseenCount of %d",
Scott Kennedy2f97af92013-07-30 10:42:34 -0700723 unreadCount, unseenCount);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800724
725 String notificationTicker = null;
726
727 // Boolean indicating that this notification is for a non-inbox label.
Scott Kennedy259df5b2013-07-11 13:24:01 -0700728 final boolean isInbox = folder.folderUri.fullUri.equals(account.settings.defaultInbox);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800729
730 // Notification label name for user label notifications.
731 final String notificationLabelName = isInbox ? null : folder.name;
732
733 if (unseenCount > 1) {
734 // Build the string that describes the number of new messages
735 final String newMessagesString = res.getString(R.string.new_messages, unseenCount);
736
737 // Use the default notification icon
738 notification.setLargeIcon(
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800739 getDefaultNotificationIcon(context, folder, true /* multiple new messages */));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800740
741 // The ticker initially start as the new messages string.
742 notificationTicker = newMessagesString;
743
744 // The title of the notification is the new messages string
745 notification.setContentTitle(newMessagesString);
746
Scott Kennedyc8bc5d12013-04-25 08:46:06 -0700747 // TODO(skennedy) Can we remove this check?
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800748 if (com.android.mail.utils.Utils.isRunningJellybeanOrLater()) {
749 // For a new-style notification
750 final int maxNumDigestItems = context.getResources().getInteger(
751 R.integer.max_num_notification_digest_items);
752
753 // The body of the notification is the account name, or the label name.
754 notification.setSubText(isInbox ? notificationAccount : notificationLabelName);
755
756 final NotificationCompat.InboxStyle digest =
757 new NotificationCompat.InboxStyle(notification);
758
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800759 int numDigestItems = 0;
760 do {
761 final Conversation conversation = new Conversation(conversationCursor);
762
763 if (!conversation.read) {
764 boolean multipleUnreadThread = false;
765 // TODO(cwren) extract this pattern into a helper
766
767 Cursor cursor = null;
768 MessageCursor messageCursor = null;
769 try {
770 final Uri.Builder uriBuilder = conversation.messageListUri.buildUpon();
771 uriBuilder.appendQueryParameter(
772 UIProvider.LABEL_QUERY_PARAMETER, notificationLabelName);
773 cursor = context.getContentResolver().query(uriBuilder.build(),
774 UIProvider.MESSAGE_PROJECTION, null, null, null);
775 messageCursor = new MessageCursor(cursor);
776
Yu Ping Hub18d75a2013-03-12 17:04:58 -0700777 String from = "";
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800778 String fromAddress = "";
779 if (messageCursor.moveToPosition(messageCursor.getCount() - 1)) {
780 final Message message = messageCursor.getMessage();
781 fromAddress = message.getFrom();
Yu Ping Hub18d75a2013-03-12 17:04:58 -0700782 if (fromAddress == null) {
783 fromAddress = "";
784 }
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800785 from = getDisplayableSender(fromAddress);
786 }
787 while (messageCursor.moveToPosition(messageCursor.getPosition() - 1)) {
788 final Message message = messageCursor.getMessage();
789 if (!message.read &&
790 !fromAddress.contentEquals(message.getFrom())) {
791 multipleUnreadThread = true;
792 break;
793 }
794 }
795 final SpannableStringBuilder sendersBuilder;
796 if (multipleUnreadThread) {
797 final int sendersLength =
798 res.getInteger(R.integer.swipe_senders_length);
799
800 sendersBuilder = getStyledSenders(context, conversationCursor,
801 sendersLength, notificationAccount);
802 } else {
Paul Westbrook3507af92013-03-30 17:06:12 -0700803 if (from == null) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700804 LogUtils.e(LOG_TAG, "null from string in " +
Paul Westbrook3507af92013-03-30 17:06:12 -0700805 "configureLatestEventInfoFromConversation");
806 from = "";
807 }
Scott Kennedy2f97af92013-07-30 10:42:34 -0700808 sendersBuilder = new SpannableStringBuilder(
809 sBidiFormatter.unicodeWrap(from));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800810 }
811 final CharSequence digestLine = getSingleMessageInboxLine(context,
812 sendersBuilder.toString(),
813 conversation.subject,
814 conversation.snippet);
815 digest.addLine(digestLine);
816 numDigestItems++;
817 } finally {
818 if (messageCursor != null) {
819 messageCursor.close();
820 }
821 if (cursor != null) {
822 cursor.close();
823 }
824 }
825 }
826 } while (numDigestItems <= maxNumDigestItems && conversationCursor.moveToNext());
827 } else {
828 // The body of the notification is the account name, or the label name.
829 notification.setContentText(
830 isInbox ? notificationAccount : notificationLabelName);
831 }
832 } else {
833 // For notifications for a single new conversation, we want to get the information from
834 // the conversation
835
836 // Move the cursor to the most recent unread conversation
837 seekToLatestUnreadConversation(conversationCursor);
838
839 final Conversation conversation = new Conversation(conversationCursor);
840
841 Cursor cursor = null;
842 MessageCursor messageCursor = null;
843 boolean multipleUnseenThread = false;
844 String from = null;
845 try {
Scott Kennedyffbf86f2013-03-08 11:48:15 -0800846 final Uri uri = conversation.messageListUri.buildUpon().appendQueryParameter(
847 UIProvider.LABEL_QUERY_PARAMETER, folder.persistentId).build();
848 cursor = context.getContentResolver().query(uri, UIProvider.MESSAGE_PROJECTION,
849 null, null, null);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800850 messageCursor = new MessageCursor(cursor);
851 // Use the information from the last sender in the conversation that triggered
852 // this notification.
853
854 String fromAddress = "";
855 if (messageCursor.moveToPosition(messageCursor.getCount() - 1)) {
856 final Message message = messageCursor.getMessage();
857 fromAddress = message.getFrom();
858 from = getDisplayableSender(fromAddress);
859 notification.setLargeIcon(
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700860 getContactIcon(context, from, getSenderAddress(fromAddress), folder));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800861 }
862
863 // Assume that the last message in this conversation is unread
864 int firstUnseenMessagePos = messageCursor.getPosition();
865 while (messageCursor.moveToPosition(messageCursor.getPosition() - 1)) {
866 final Message message = messageCursor.getMessage();
867 final boolean unseen = !message.seen;
868 if (unseen) {
869 firstUnseenMessagePos = messageCursor.getPosition();
870 if (!multipleUnseenThread
871 && !fromAddress.contentEquals(message.getFrom())) {
872 multipleUnseenThread = true;
873 }
874 }
875 }
876
Scott Kennedyc8bc5d12013-04-25 08:46:06 -0700877 // TODO(skennedy) Can we remove this check?
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800878 if (Utils.isRunningJellybeanOrLater()) {
879 // For a new-style notification
880
881 if (multipleUnseenThread) {
882 // The title of a single conversation is the list of senders.
883 int sendersLength = res.getInteger(R.integer.swipe_senders_length);
884
885 final SpannableStringBuilder sendersBuilder = getStyledSenders(
886 context, conversationCursor, sendersLength, notificationAccount);
887
888 notification.setContentTitle(sendersBuilder);
889 // For a single new conversation, the ticker is based on the sender's name.
890 notificationTicker = sendersBuilder.toString();
891 } else {
Andrew Sapperstein8173cf42013-07-01 13:17:18 -0700892 from = sBidiFormatter.unicodeWrap(from);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800893 // The title of a single message the sender.
894 notification.setContentTitle(from);
895 // For a single new conversation, the ticker is based on the sender's name.
896 notificationTicker = from;
897 }
898
899 // The notification content will be the subject of the conversation.
900 notification.setContentText(
901 getSingleMessageLittleText(context, conversation.subject));
902
903 // The notification subtext will be the subject of the conversation for inbox
904 // notifications, or will based on the the label name for user label
905 // notifications.
906 notification.setSubText(isInbox ? notificationAccount : notificationLabelName);
907
908 if (multipleUnseenThread) {
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800909 notification.setLargeIcon(
910 getDefaultNotificationIcon(context, folder, true));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800911 }
912 final NotificationCompat.BigTextStyle bigText =
913 new NotificationCompat.BigTextStyle(notification);
914
915 // Seek the message cursor to the first unread message
Paul Westbrook92b21742013-03-11 17:36:40 -0700916 final Message message;
917 if (messageCursor.moveToPosition(firstUnseenMessagePos)) {
918 message = messageCursor.getMessage();
919 bigText.bigText(getSingleMessageBigText(context,
920 conversation.subject, message));
921 } else {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700922 LogUtils.e(LOG_TAG, "Failed to load message");
Paul Westbrook92b21742013-03-11 17:36:40 -0700923 message = null;
924 }
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800925
Paul Westbrook92b21742013-03-11 17:36:40 -0700926 if (message != null) {
927 final Set<String> notificationActions =
Scott Kennedycde6eb02013-03-21 18:49:32 -0700928 folderPreferences.getNotificationActions(account);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800929
Paul Westbrook92b21742013-03-11 17:36:40 -0700930 final int notificationId = getNotificationId(notificationAccount, folder);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800931
Paul Westbrook92b21742013-03-11 17:36:40 -0700932 NotificationActionUtils.addNotificationActions(context, notificationIntent,
933 notification, account, conversation, message, folder,
934 notificationId, when, notificationActions);
935 }
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800936 } else {
937 // For an old-style notification
938
939 // The title of a single conversation notification is built from both the sender
940 // and subject of the new message.
941 notification.setContentTitle(getSingleMessageNotificationTitle(context,
942 from, conversation.subject));
943
944 // The notification content will be the subject of the conversation for inbox
945 // notifications, or will based on the the label name for user label
946 // notifications.
947 notification.setContentText(
948 isInbox ? notificationAccount : notificationLabelName);
949
950 // For a single new conversation, the ticker is based on the sender's name.
951 notificationTicker = from;
952 }
953 } finally {
954 if (messageCursor != null) {
955 messageCursor.close();
956 }
957 if (cursor != null) {
958 cursor.close();
959 }
960 }
961 }
962
963 // Build the notification ticker
964 if (notificationLabelName != null && notificationTicker != null) {
965 // This is a per label notification, format the ticker with that information
966 notificationTicker = res.getString(R.string.label_notification_ticker,
967 notificationLabelName, notificationTicker);
968 }
969
970 if (notificationTicker != null) {
971 // If we didn't generate a notification ticker, it will default to account name
972 notification.setTicker(notificationTicker);
973 }
974
975 // Set the number in the notification
976 if (unreadCount > 1) {
977 notification.setNumber(unreadCount);
978 }
979
980 notification.setContentIntent(clickIntent);
981 }
982
983 private static SpannableStringBuilder getStyledSenders(final Context context,
984 final Cursor conversationCursor, final int maxLength, final String account) {
985 final Conversation conversation = new Conversation(conversationCursor);
986 final com.android.mail.providers.ConversationInfo conversationInfo =
987 conversation.conversationInfo;
988 final ArrayList<SpannableString> senders = new ArrayList<SpannableString>();
989 if (sNotificationUnreadStyleSpan == null) {
990 sNotificationUnreadStyleSpan = new TextAppearanceSpan(
991 context, R.style.NotificationSendersUnreadTextAppearance);
992 sNotificationReadStyleSpan =
993 new TextAppearanceSpan(context, R.style.NotificationSendersReadTextAppearance);
994 }
995 SendersView.format(context, conversationInfo, "", maxLength, senders, null, null, account,
996 sNotificationUnreadStyleSpan, sNotificationReadStyleSpan, false);
997
998 return ellipsizeStyledSenders(context, senders);
999 }
1000
1001 private static String sSendersSplitToken = null;
1002 private static String sElidedPaddingToken = null;
1003
1004 private static SpannableStringBuilder ellipsizeStyledSenders(final Context context,
1005 ArrayList<SpannableString> styledSenders) {
1006 if (sSendersSplitToken == null) {
1007 sSendersSplitToken = context.getString(R.string.senders_split_token);
1008 sElidedPaddingToken = context.getString(R.string.elided_padding_token);
1009 }
1010
1011 SpannableStringBuilder builder = new SpannableStringBuilder();
1012 SpannableString prevSender = null;
1013 for (SpannableString sender : styledSenders) {
1014 if (sender == null) {
Scott Kennedy2f97af92013-07-30 10:42:34 -07001015 LogUtils.e(LOG_TAG, "null sender iterating over styledSenders");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001016 continue;
1017 }
1018 CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class);
1019 if (SendersView.sElidedString.equals(sender.toString())) {
1020 prevSender = sender;
1021 sender = copyStyles(spans, sElidedPaddingToken + sender + sElidedPaddingToken);
1022 } else if (builder.length() > 0
1023 && (prevSender == null || !SendersView.sElidedString.equals(prevSender
1024 .toString()))) {
1025 prevSender = sender;
1026 sender = copyStyles(spans, sSendersSplitToken + sender);
1027 } else {
1028 prevSender = sender;
1029 }
1030 builder.append(sender);
1031 }
1032 return builder;
1033 }
1034
1035 private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) {
1036 SpannableString s = new SpannableString(newText);
1037 if (spans != null && spans.length > 0) {
1038 s.setSpan(spans[0], 0, s.length(), 0);
1039 }
1040 return s;
1041 }
1042
1043 /**
1044 * Seeks the cursor to the position of the most recent unread conversation. If no unread
1045 * conversation is found, the position of the cursor will be restored, and false will be
1046 * returned.
1047 */
1048 private static boolean seekToLatestUnreadConversation(final Cursor cursor) {
1049 final int initialPosition = cursor.getPosition();
1050 do {
1051 final Conversation conversation = new Conversation(cursor);
1052 if (!conversation.read) {
1053 return true;
1054 }
1055 } while (cursor.moveToNext());
1056
1057 // Didn't find an unread conversation, reset the position.
1058 cursor.moveToPosition(initialPosition);
1059 return false;
1060 }
1061
1062 /**
1063 * Sets the bigtext for a notification for a single new conversation
1064 *
1065 * @param context
1066 * @param senders Sender of the new message that triggered the notification.
1067 * @param subject Subject of the new message that triggered the notification
1068 * @param snippet Snippet of the new message that triggered the notification
1069 * @return a {@link CharSequence} suitable for use in
1070 * {@link android.support.v4.app.NotificationCompat.BigTextStyle}
1071 */
1072 private static CharSequence getSingleMessageInboxLine(Context context,
1073 String senders, String subject, String snippet) {
1074 // TODO(cwren) finish this step toward commmon code with getSingleMessageBigText
1075
1076 final String subjectSnippet = !TextUtils.isEmpty(subject) ? subject : snippet;
1077
1078 final TextAppearanceSpan notificationPrimarySpan =
1079 new TextAppearanceSpan(context, R.style.NotificationPrimaryText);
1080
1081 if (TextUtils.isEmpty(senders)) {
1082 // If the senders are empty, just use the subject/snippet.
1083 return subjectSnippet;
1084 } else if (TextUtils.isEmpty(subjectSnippet)) {
1085 // If the subject/snippet is empty, just use the senders.
1086 final SpannableString spannableString = new SpannableString(senders);
1087 spannableString.setSpan(notificationPrimarySpan, 0, senders.length(), 0);
1088
1089 return spannableString;
1090 } else {
1091 final String formatString = context.getResources().getString(
1092 R.string.multiple_new_message_notification_item);
1093 final TextAppearanceSpan notificationSecondarySpan =
1094 new TextAppearanceSpan(context, R.style.NotificationSecondaryText);
1095
Andrew Sapperstein8173cf42013-07-01 13:17:18 -07001096 // senders is already individually unicode wrapped so it does not need to be done here
Andrew Sappersteinf5810962013-06-27 10:41:33 -07001097 final String instantiatedString = String.format(formatString,
Andrew Sapperstein8173cf42013-07-01 13:17:18 -07001098 senders,
Andrew Sappersteinf5810962013-06-27 10:41:33 -07001099 sBidiFormatter.unicodeWrap(subjectSnippet));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001100
1101 final SpannableString spannableString = new SpannableString(instantiatedString);
1102
1103 final boolean isOrderReversed = formatString.indexOf("%2$s") <
1104 formatString.indexOf("%1$s");
1105 final int primaryOffset =
1106 (isOrderReversed ? instantiatedString.lastIndexOf(senders) :
1107 instantiatedString.indexOf(senders));
1108 final int secondaryOffset =
1109 (isOrderReversed ? instantiatedString.lastIndexOf(subjectSnippet) :
1110 instantiatedString.indexOf(subjectSnippet));
1111 spannableString.setSpan(notificationPrimarySpan,
1112 primaryOffset, primaryOffset + senders.length(), 0);
1113 spannableString.setSpan(notificationSecondarySpan,
1114 secondaryOffset, secondaryOffset + subjectSnippet.length(), 0);
1115 return spannableString;
1116 }
1117 }
1118
1119 /**
1120 * Sets the bigtext for a notification for a single new conversation
1121 * @param context
1122 * @param subject Subject of the new message that triggered the notification
Andrew Sappersteinf5810962013-06-27 10:41:33 -07001123 * @return a {@link CharSequence} suitable for use in
1124 * {@link NotificationCompat.Builder#setContentText}
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001125 */
1126 private static CharSequence getSingleMessageLittleText(Context context, String subject) {
1127 final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
1128 context, R.style.NotificationPrimaryText);
1129
1130 final SpannableString spannableString = new SpannableString(subject);
1131 spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0);
1132
1133 return spannableString;
1134 }
1135
1136 /**
1137 * Sets the bigtext for a notification for a single new conversation
1138 *
1139 * @param context
1140 * @param subject Subject of the new message that triggered the notification
1141 * @param message the {@link Message} to be displayed.
1142 * @return a {@link CharSequence} suitable for use in
1143 * {@link android.support.v4.app.NotificationCompat.BigTextStyle}
1144 */
1145 private static CharSequence getSingleMessageBigText(Context context, String subject,
1146 final Message message) {
1147
1148 final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
1149 context, R.style.NotificationPrimaryText);
1150
1151 final String snippet = getMessageBodyWithoutElidedText(message);
1152
1153 // Change multiple newlines (with potential white space between), into a single new line
1154 final String collapsedSnippet =
1155 !TextUtils.isEmpty(snippet) ? snippet.replaceAll("\\n\\s+", "\n") : "";
1156
1157 if (TextUtils.isEmpty(subject)) {
1158 // If the subject is empty, just use the snippet.
1159 return snippet;
1160 } else if (TextUtils.isEmpty(collapsedSnippet)) {
1161 // If the snippet is empty, just use the subject.
1162 final SpannableString spannableString = new SpannableString(subject);
1163 spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0);
1164
1165 return spannableString;
1166 } else {
1167 final String notificationBigTextFormat = context.getResources().getString(
1168 R.string.single_new_message_notification_big_text);
1169
1170 // Localizers may change the order of the parameters, look at how the format
1171 // string is structured.
1172 final boolean isSubjectFirst = notificationBigTextFormat.indexOf("%2$s") >
1173 notificationBigTextFormat.indexOf("%1$s");
1174 final String bigText =
1175 String.format(notificationBigTextFormat, subject, collapsedSnippet);
1176 final SpannableString spannableString = new SpannableString(bigText);
1177
1178 final int subjectOffset =
1179 (isSubjectFirst ? bigText.indexOf(subject) : bigText.lastIndexOf(subject));
1180 spannableString.setSpan(notificationSubjectSpan,
1181 subjectOffset, subjectOffset + subject.length(), 0);
1182
1183 return spannableString;
1184 }
1185 }
1186
1187 /**
1188 * Gets the title for a notification for a single new conversation
1189 * @param context
1190 * @param sender Sender of the new message that triggered the notification.
1191 * @param subject Subject of the new message that triggered the notification
1192 * @return a {@link CharSequence} suitable for use as a {@link Notification} title.
1193 */
1194 private static CharSequence getSingleMessageNotificationTitle(Context context,
1195 String sender, String subject) {
1196
1197 if (TextUtils.isEmpty(subject)) {
1198 // If the subject is empty, just set the title to the sender's information.
1199 return sender;
1200 } else {
1201 final String notificationTitleFormat = context.getResources().getString(
1202 R.string.single_new_message_notification_title);
1203
1204 // Localizers may change the order of the parameters, look at how the format
1205 // string is structured.
1206 final boolean isSubjectLast = notificationTitleFormat.indexOf("%2$s") >
1207 notificationTitleFormat.indexOf("%1$s");
1208 final String titleString = String.format(notificationTitleFormat, sender, subject);
1209
1210 // Format the string so the subject is using the secondaryText style
1211 final SpannableString titleSpannable = new SpannableString(titleString);
1212
1213 // Find the offset of the subject.
1214 final int subjectOffset =
1215 isSubjectLast ? titleString.lastIndexOf(subject) : titleString.indexOf(subject);
1216 final TextAppearanceSpan notificationSubjectSpan =
1217 new TextAppearanceSpan(context, R.style.NotificationSecondaryText);
1218 titleSpannable.setSpan(notificationSubjectSpan,
1219 subjectOffset, subjectOffset + subject.length(), 0);
1220 return titleSpannable;
1221 }
1222 }
1223
1224 /**
Scott Kennedy7f8aed62013-05-22 18:35:17 -07001225 * Clears the notifications for the specified account/folder.
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001226 */
Scott Kennedy7f8aed62013-05-22 18:35:17 -07001227 public static void clearFolderNotification(Context context, Account account, Folder folder,
1228 final boolean markSeen) {
Scott Kennedy2f97af92013-07-30 10:42:34 -07001229 LogUtils.v(LOG_TAG, "Clearing all notifications for %s/%s", account.name, folder.name);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001230 final NotificationMap notificationMap = getNotificationMap(context);
1231 final NotificationKey key = new NotificationKey(account, folder);
1232 notificationMap.remove(key);
1233 notificationMap.saveNotificationMap(context);
1234
Scott Kennedy7f8aed62013-05-22 18:35:17 -07001235 final NotificationManager notificationManager =
1236 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
1237 notificationManager.cancel(getNotificationId(account.name, folder));
1238
1239 if (markSeen) {
1240 markSeen(context, folder);
1241 }
1242 }
1243
1244 /**
1245 * Clears all notifications for the specified account.
1246 */
1247 public static void clearAccountNotifications(final Context context, final String account) {
Scott Kennedy2f97af92013-07-30 10:42:34 -07001248 LogUtils.v(LOG_TAG, "Clearing all notifications for %s", account);
Scott Kennedy7f8aed62013-05-22 18:35:17 -07001249 final NotificationMap notificationMap = getNotificationMap(context);
1250
1251 // Find all NotificationKeys for this account
1252 final ImmutableList.Builder<NotificationKey> keyBuilder = ImmutableList.builder();
1253
1254 for (final NotificationKey key : notificationMap.keySet()) {
1255 if (account.equals(key.account.name)) {
1256 keyBuilder.add(key);
1257 }
1258 }
1259
1260 final List<NotificationKey> notificationKeys = keyBuilder.build();
1261
1262 final NotificationManager notificationManager =
1263 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
1264
1265 for (final NotificationKey notificationKey : notificationKeys) {
1266 final Folder folder = notificationKey.folder;
1267 notificationManager.cancel(getNotificationId(account, folder));
1268 notificationMap.remove(notificationKey);
1269 }
1270
1271 notificationMap.saveNotificationMap(context);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001272 }
1273
1274 private static ArrayList<Long> findContacts(Context context, Collection<String> addresses) {
1275 ArrayList<String> whereArgs = new ArrayList<String>();
1276 StringBuilder whereBuilder = new StringBuilder();
1277 String[] questionMarks = new String[addresses.size()];
1278
1279 whereArgs.addAll(addresses);
1280 Arrays.fill(questionMarks, "?");
1281 whereBuilder.append(Email.DATA1 + " IN (").
1282 append(TextUtils.join(",", questionMarks)).
1283 append(")");
1284
1285 ContentResolver resolver = context.getContentResolver();
1286 Cursor c = resolver.query(Email.CONTENT_URI,
1287 new String[]{Email.CONTACT_ID}, whereBuilder.toString(),
1288 whereArgs.toArray(new String[0]), null);
1289
1290 ArrayList<Long> contactIds = new ArrayList<Long>();
1291 if (c == null) {
1292 return contactIds;
1293 }
1294 try {
1295 while (c.moveToNext()) {
1296 contactIds.add(c.getLong(0));
1297 }
1298 } finally {
1299 c.close();
1300 }
1301 return contactIds;
1302 }
1303
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001304 private static Bitmap getContactIcon(final Context context, final String displayName,
1305 final String senderAddress, final Folder folder) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001306 if (senderAddress == null) {
1307 return null;
1308 }
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001309
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001310 Bitmap icon = null;
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001311
1312 final List<Long> contactIds = findContacts( context, Arrays.asList(
1313 new String[] { senderAddress }));
1314
1315 // Get the ideal size for this icon.
1316 final Resources res = context.getResources();
1317 final int idealIconHeight =
1318 res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
1319 final int idealIconWidth =
1320 res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001321
1322 if (contactIds != null) {
Scott Kennedy4046d062013-06-27 14:46:09 -07001323 for (final long id : contactIds) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001324 final Uri contactUri =
1325 ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id);
1326 final Uri photoUri = Uri.withAppendedPath(contactUri, Photo.CONTENT_DIRECTORY);
1327 final Cursor cursor = context.getContentResolver().query(
1328 photoUri, new String[] { Photo.PHOTO }, null, null, null);
1329
1330 if (cursor != null) {
1331 try {
1332 if (cursor.moveToFirst()) {
Scott Kennedy4046d062013-06-27 14:46:09 -07001333 final byte[] data = cursor.getBlob(0);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001334 if (data != null) {
1335 icon = BitmapFactory.decodeStream(new ByteArrayInputStream(data));
1336 if (icon != null && icon.getHeight() < idealIconHeight) {
1337 // We should scale this image to fit the intended size
1338 icon = Bitmap.createScaledBitmap(
1339 icon, idealIconWidth, idealIconHeight, true);
1340 }
1341 if (icon != null) {
1342 break;
1343 }
1344 }
1345 }
1346 } finally {
1347 cursor.close();
1348 }
1349 }
1350 }
1351 }
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001352
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001353 if (icon == null) {
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001354 // Make a colorful tile!
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001355 final Dimensions dimensions = new Dimensions(idealIconWidth, idealIconHeight,
1356 Dimensions.SCALE_ONE);
1357
Scott Kennedyfe235122013-06-28 12:06:36 -07001358 icon = new LetterTileProvider(context).getLetterTile(dimensions,
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001359 displayName, senderAddress);
1360 }
1361
1362 if (icon == null) {
1363 // Icon should be the default mail icon.
Scott Kennedy61bd0e82012-12-10 18:18:17 -08001364 icon = getDefaultNotificationIcon(context, folder, false /* single new message */);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001365 }
1366 return icon;
1367 }
1368
1369 private static String getMessageBodyWithoutElidedText(final Message message) {
Scott Kennedy5e7e88b2013-03-07 16:42:12 -08001370 return getMessageBodyWithoutElidedText(message.getBodyAsHtml());
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001371 }
1372
1373 public static String getMessageBodyWithoutElidedText(String html) {
1374 if (TextUtils.isEmpty(html)) {
1375 return "";
1376 }
1377 // Get the html "tree" for this message body
1378 final HtmlTree htmlTree = com.android.mail.utils.Utils.getHtmlTree(html);
1379 htmlTree.setPlainTextConverterFactory(MESSAGE_CONVERTER_FACTORY);
1380
1381 return htmlTree.getPlainText();
1382 }
1383
1384 public static void markSeen(final Context context, final Folder folder) {
Scott Kennedy259df5b2013-07-11 13:24:01 -07001385 final Uri uri = folder.folderUri.fullUri;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001386
1387 final ContentValues values = new ContentValues(1);
1388 values.put(UIProvider.ConversationColumns.SEEN, 1);
1389
1390 context.getContentResolver().update(uri, values, null, null);
1391 }
1392
1393 /**
1394 * Returns a displayable string representing
1395 * the message sender. It has a preference toward showing the name,
1396 * but will fall back to the address if that is all that is available.
1397 */
1398 private static String getDisplayableSender(String sender) {
1399 final EmailAddress address = EmailAddress.getEmailAddress(sender);
1400
1401 String displayableSender = address.getName();
Scott Kennedy390cab92013-05-23 14:29:49 -07001402
1403 if (!TextUtils.isEmpty(displayableSender)) {
1404 return Address.decodeAddressName(displayableSender);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001405 }
Scott Kennedy390cab92013-05-23 14:29:49 -07001406
1407 // If that fails, default to the sender address.
1408 displayableSender = address.getAddress();
1409
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001410 // If we were unable to tokenize a name or address,
1411 // just use whatever was in the sender.
1412 if (TextUtils.isEmpty(displayableSender)) {
1413 displayableSender = sender;
1414 }
1415 return displayableSender;
1416 }
1417
1418 /**
1419 * Returns only the address portion of a message sender.
1420 */
1421 private static String getSenderAddress(String sender) {
1422 final EmailAddress address = EmailAddress.getEmailAddress(sender);
1423
1424 String tokenizedAddress = address.getAddress();
1425
1426 // If we were unable to tokenize a name or address,
1427 // just use whatever was in the sender.
1428 if (TextUtils.isEmpty(tokenizedAddress)) {
1429 tokenizedAddress = sender;
1430 }
1431 return tokenizedAddress;
1432 }
1433
1434 public static int getNotificationId(final String account, final Folder folder) {
1435 return 1 ^ account.hashCode() ^ folder.hashCode();
1436 }
1437
1438 private static class NotificationKey {
1439 public final Account account;
1440 public final Folder folder;
1441
1442 public NotificationKey(Account account, Folder folder) {
1443 this.account = account;
1444 this.folder = folder;
1445 }
1446
1447 @Override
1448 public boolean equals(Object other) {
1449 if (!(other instanceof NotificationKey)) {
1450 return false;
1451 }
1452 NotificationKey key = (NotificationKey) other;
1453 return account.equals(key.account) && folder.equals(key.folder);
1454 }
1455
1456 @Override
1457 public String toString() {
Scott Kennedye8a7c4c2013-05-09 11:56:50 -07001458 return account.name + " " + folder.name;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001459 }
1460
1461 @Override
1462 public int hashCode() {
1463 final int accountHashCode = account.hashCode();
1464 final int folderHashCode = folder.hashCode();
1465 return accountHashCode ^ folderHashCode;
1466 }
1467 }
1468
1469 /**
1470 * Contains the logic for converting the contents of one HtmlTree into
1471 * plaintext.
1472 */
1473 public static class MailMessagePlainTextConverter extends HtmlTree.DefaultPlainTextConverter {
1474 // Strings for parsing html message bodies
1475 private static final String ELIDED_TEXT_ELEMENT_NAME = "div";
1476 private static final String ELIDED_TEXT_ELEMENT_ATTRIBUTE_NAME = "class";
1477 private static final String ELIDED_TEXT_ELEMENT_ATTRIBUTE_CLASS_VALUE = "elided-text";
1478
1479 private static final HTML.Attribute ELIDED_TEXT_ATTRIBUTE =
1480 new HTML.Attribute(ELIDED_TEXT_ELEMENT_ATTRIBUTE_NAME, HTML.Attribute.NO_TYPE);
1481
1482 private static final HtmlDocument.Node ELIDED_TEXT_REPLACEMENT_NODE =
1483 HtmlDocument.createSelfTerminatingTag(HTML4.BR_ELEMENT, null, null, null);
1484
1485 private int mEndNodeElidedTextBlock = -1;
1486
1487 @Override
1488 public void addNode(HtmlDocument.Node n, int nodeNum, int endNum) {
1489 // If we are in the middle of an elided text block, don't add this node
1490 if (nodeNum < mEndNodeElidedTextBlock) {
1491 return;
1492 } else if (nodeNum == mEndNodeElidedTextBlock) {
1493 super.addNode(ELIDED_TEXT_REPLACEMENT_NODE, nodeNum, endNum);
1494 return;
1495 }
1496
1497 // If this tag starts another elided text block, we want to remember the end
1498 if (n instanceof HtmlDocument.Tag) {
1499 boolean foundElidedTextTag = false;
1500 final HtmlDocument.Tag htmlTag = (HtmlDocument.Tag)n;
1501 final HTML.Element htmlElement = htmlTag.getElement();
1502 if (ELIDED_TEXT_ELEMENT_NAME.equals(htmlElement.getName())) {
1503 // Make sure that the class is what is expected
1504 final List<HtmlDocument.TagAttribute> attributes =
1505 htmlTag.getAttributes(ELIDED_TEXT_ATTRIBUTE);
1506 for (HtmlDocument.TagAttribute attribute : attributes) {
1507 if (ELIDED_TEXT_ELEMENT_ATTRIBUTE_CLASS_VALUE.equals(
1508 attribute.getValue())) {
1509 // Found an "elided-text" div. Remember information about this tag
1510 mEndNodeElidedTextBlock = endNum;
1511 foundElidedTextTag = true;
1512 break;
1513 }
1514 }
1515 }
1516
1517 if (foundElidedTextTag) {
1518 return;
1519 }
1520 }
1521
Scott Kennedyc56b2332013-05-23 18:24:53 -07001522 super.addNode(n, nodeNum, endNum);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001523 }
1524 }
1525
1526 /**
1527 * During account setup in Email, we may not have an inbox yet, so the notification setting had
1528 * to be stored in {@link AccountPreferences}. If it is still there, we need to move it to the
1529 * {@link FolderPreferences} now.
1530 */
1531 public static void moveNotificationSetting(final AccountPreferences accountPreferences,
1532 final FolderPreferences folderPreferences) {
1533 if (accountPreferences.isDefaultInboxNotificationsEnabledSet()) {
1534 // If this setting has been changed some other way, don't overwrite it
1535 if (!folderPreferences.isNotificationsEnabledSet()) {
1536 final boolean notificationsEnabled =
1537 accountPreferences.getDefaultInboxNotificationsEnabled();
1538
1539 folderPreferences.setNotificationsEnabled(notificationsEnabled);
1540 }
1541
1542 accountPreferences.clearDefaultInboxNotificationsEnabled();
1543 }
1544 }
1545}