blob: 4889651ec544a09488787514789f649dee44eb13 [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 Kennedy13a73272013-05-09 09:45:03 -0700104 LogUtils.v(LOG_TAG, "NotificationUtils: 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 Kennedy93486c02013-04-17 12:15:43 -0700262 LogUtils.d(LOG_TAG, "NotificationUtils: 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 Kennedy4162f832013-05-06 17:37:55 -0700274 LogUtils.d(LOG_TAG, "NotificationUtils: 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 Kennedy2638b482013-05-06 16:05:11 -0700295 LogUtils.d(LOG_TAG, "NotificationUtils: resendNotifications ");
296
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800297 if (cancelExisting) {
Scott Kennedy93486c02013-04-17 12:15:43 -0700298 LogUtils.d(LOG_TAG, "NotificationUtils: 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 Kennedy2638b482013-05-06 16:05:11 -0700315 LogUtils.d(LOG_TAG, "NotificationUtils: resendNotifications - not resending %s / %s"
316 + " 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 Kennedy2638b482013-05-06 16:05:11 -0700321 LogUtils.d(LOG_TAG, "NotificationUtils: 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 Kennedy4162f832013-05-06 17:37:55 -0700340 LogUtils.d(LOG_TAG, "NotificationUtils: validateAccountNotifications - %s", account);
341
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 Kennedy93486c02013-04-17 12:15:43 -0700383 LogUtils.d(LOG_TAG,
384 "NotificationUtils: validateAccountNotifications - cancelling %s / %s",
385 notification.account.name, folder.persistentId);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800386 nm.cancel(notificationId);
387 notificationMap.remove(notification);
388 NotificationActionUtils.sUndoNotifications.remove(notificationId);
389 NotificationActionUtils.sNotificationTimestamps.delete(notificationId);
390 }
391 notificationMap.saveNotificationMap(context);
392 }
393 }
394
395 /**
396 * Display only one notification.
397 */
398 public static void setNewEmailIndicator(Context context, final int unreadCount,
399 final int unseenCount, final Account account, final Folder folder,
400 final boolean getAttention) {
Scott Kennedy4162f832013-05-06 17:37:55 -0700401 LogUtils.d(LOG_TAG, "NotificationUtils: setNewEmailIndicator unreadCount = %d, "
402 + "unseenCount = %d, account = %s, folder = %s, getAttention = %b", unreadCount,
Scott Kennedy259df5b2013-07-11 13:24:01 -0700403 unseenCount, account.name, folder.folderUri, getAttention);
Scott Kennedy4162f832013-05-06 17:37:55 -0700404
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800405 boolean ignoreUnobtrusiveSetting = false;
406
Scott Kennedydab8a942013-02-22 12:31:30 -0800407 final int notificationId = getNotificationId(account.name, folder);
408
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800409 // Update the notification map
410 final NotificationMap notificationMap = getNotificationMap(context);
411 final NotificationKey key = new NotificationKey(account, folder);
412 if (unreadCount == 0) {
Scott Kennedy93486c02013-04-17 12:15:43 -0700413 LogUtils.d(LOG_TAG,
414 "NotificationUtils: setNewEmailIndicator - cancelling %s / %s",
415 account.name, folder.persistentId);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800416 notificationMap.remove(key);
Scott Kennedydab8a942013-02-22 12:31:30 -0800417 ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
418 .cancel(notificationId);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800419 } else {
420 if (!notificationMap.containsKey(key)) {
421 // This account previously didn't have any unread mail; ignore the "unobtrusive
422 // notifications" setting and play sound and/or vibrate the device even if a
423 // notification already exists (bug 2412348).
424 ignoreUnobtrusiveSetting = true;
425 }
426 notificationMap.put(key, unreadCount, unseenCount);
427 }
428 notificationMap.saveNotificationMap(context);
429
430 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
Scott Kennedy13a73272013-05-09 09:45:03 -0700431 LogUtils.v(LOG_TAG, "NotificationUtils: New email: %s mapSize: %d getAttention: %b",
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800432 createNotificationString(notificationMap), notificationMap.size(),
433 getAttention);
434 }
435
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800436 if (NotificationActionUtils.sUndoNotifications.get(notificationId) == null) {
437 validateNotifications(context, folder, account, getAttention, ignoreUnobtrusiveSetting,
438 key);
439 }
440 }
441
442 /**
443 * Validate the notifications notification.
444 */
445 private static void validateNotifications(Context context, final Folder folder,
446 final Account account, boolean getAttention, boolean ignoreUnobtrusiveSetting,
447 NotificationKey key) {
448
449 NotificationManager nm = (NotificationManager)
450 context.getSystemService(Context.NOTIFICATION_SERVICE);
451
452 final NotificationMap notificationMap = getNotificationMap(context);
453 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
Scott Kennedy13a73272013-05-09 09:45:03 -0700454 LogUtils.v(LOG_TAG, "NotificationUtils: Validating Notification: %s mapSize: %d "
455 + "folder: %s getAttention: %b", createNotificationString(notificationMap),
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800456 notificationMap.size(), folder.name, getAttention);
457 }
458 // The number of unread messages for this account and label.
459 final Integer unread = notificationMap.getUnread(key);
460 final int unreadCount = unread != null ? unread.intValue() : 0;
461 final Integer unseen = notificationMap.getUnseen(key);
462 int unseenCount = unseen != null ? unseen.intValue() : 0;
463
464 Cursor cursor = null;
465
466 try {
467 final Uri.Builder uriBuilder = folder.conversationListUri.buildUpon();
468 uriBuilder.appendQueryParameter(
469 UIProvider.SEEN_QUERY_PARAMETER, Boolean.FALSE.toString());
Andy Huang0be92432013-03-22 18:31:44 -0700470 // Do not allow this quick check to disrupt any active network-enabled conversation
471 // cursor.
472 uriBuilder.appendQueryParameter(
473 UIProvider.ConversationListQueryParameters.USE_NETWORK,
474 Boolean.FALSE.toString());
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800475 cursor = context.getContentResolver().query(uriBuilder.build(),
476 UIProvider.CONVERSATION_PROJECTION, null, null, null);
Yu Ping Hu3a7b45b2013-06-18 18:10:50 -0700477 if (cursor == null) {
478 // This folder doesn't exist.
479 clearFolderNotification(context, account, folder, false);
480 return;
481 }
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800482 final int cursorUnseenCount = cursor.getCount();
483
484 // Make sure the unseen count matches the number of items in the cursor. But, we don't
485 // want to overwrite a 0 unseen count that was specified in the intent
486 if (unseenCount != 0 && unseenCount != cursorUnseenCount) {
Scott Kennedy13a73272013-05-09 09:45:03 -0700487 LogUtils.d(LOG_TAG, "NotificationUtils: "
488 + "Unseen count doesn't match cursor count. unseen: %d cursor count: %d",
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800489 unseenCount, cursorUnseenCount);
490 unseenCount = cursorUnseenCount;
491 }
492
493 // For the purpose of the notifications, the unseen count should be capped at the num of
494 // unread conversations.
495 if (unseenCount > unreadCount) {
496 unseenCount = unreadCount;
497 }
498
499 final int notificationId = getNotificationId(account.name, folder);
500
501 if (unseenCount == 0) {
Scott Kennedy93486c02013-04-17 12:15:43 -0700502 LogUtils.d(LOG_TAG,
503 "NotificationUtils: validateNotifications - cancelling %s / %s",
504 account.name, folder.persistentId);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800505 nm.cancel(notificationId);
506 return;
507 }
508
509 // We now have all we need to create the notification and the pending intent
510 PendingIntent clickIntent;
511
512 NotificationCompat.Builder notification = new NotificationCompat.Builder(context);
513 notification.setSmallIcon(R.drawable.stat_notify_email);
514 notification.setTicker(account.name);
515
516 final long when;
517
518 final long oldWhen =
519 NotificationActionUtils.sNotificationTimestamps.get(notificationId);
520 if (oldWhen != 0) {
521 when = oldWhen;
522 } else {
523 when = System.currentTimeMillis();
524 }
525
526 notification.setWhen(when);
527
528 // The timestamp is now stored in the notification, so we can remove it from here
529 NotificationActionUtils.sNotificationTimestamps.delete(notificationId);
530
531 // Dispatch a CLEAR_NEW_MAIL_NOTIFICATIONS intent if the user taps the "X" next to a
532 // notification. Also this intent gets fired when the user taps on a notification as
533 // the AutoCancel flag has been set
534 final Intent cancelNotificationIntent =
535 new Intent(MailIntentService.ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS);
536 cancelNotificationIntent.setPackage(context.getPackageName());
Scott Kennedy09400ef2013-03-07 11:09:47 -0800537 cancelNotificationIntent.setData(Utils.appendVersionQueryParameter(context,
Scott Kennedy259df5b2013-07-11 13:24:01 -0700538 folder.folderUri.fullUri));
Scott Kennedy48cfe462013-04-10 11:32:02 -0700539 cancelNotificationIntent.putExtra(Utils.EXTRA_ACCOUNT, account);
540 cancelNotificationIntent.putExtra(Utils.EXTRA_FOLDER, folder);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800541
542 notification.setDeleteIntent(PendingIntent.getService(
543 context, notificationId, cancelNotificationIntent, 0));
544
545 // Ensure that the notification is cleared when the user selects it
546 notification.setAutoCancel(true);
547
548 boolean eventInfoConfigured = false;
549
Scott Kennedy259df5b2013-07-11 13:24:01 -0700550 final boolean isInbox = folder.folderUri.equals(account.settings.defaultInbox);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800551 final FolderPreferences folderPreferences =
552 new FolderPreferences(context, account.name, folder, isInbox);
553
554 if (isInbox) {
555 final AccountPreferences accountPreferences =
556 new AccountPreferences(context, account.name);
557 moveNotificationSetting(accountPreferences, folderPreferences);
558 }
559
560 if (!folderPreferences.areNotificationsEnabled()) {
561 // Don't notify
562 return;
563 }
564
565 if (unreadCount > 0) {
566 // How can I order this properly?
567 if (cursor.moveToNext()) {
Scott Kennedybd8acad2013-05-29 10:22:25 -0700568 final Intent notificationIntent;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800569
Scott Kennedybd8acad2013-05-29 10:22:25 -0700570 // Launch directly to the conversation, if there is only 1 unseen conversation
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800571 if (unseenCount == 1) {
Scott Kennedy09400ef2013-03-07 11:09:47 -0800572 notificationIntent = createViewConversationIntent(context, account, folder,
573 cursor);
Scott Kennedybd8acad2013-05-29 10:22:25 -0700574 } else {
575 notificationIntent = createViewConversationIntent(context, account, folder,
576 null);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800577 }
578
579 if (notificationIntent == null) {
Scott Kennedy13a73272013-05-09 09:45:03 -0700580 LogUtils.e(LOG_TAG, "NotificationUtils: "
581 + "Null intent when building notification");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800582 return;
583 }
584
585 clickIntent = PendingIntent.getActivity(context, -1, notificationIntent, 0);
586 configureLatestEventInfoFromConversation(context, account, folderPreferences,
587 notification, cursor, clickIntent, notificationIntent,
588 account.name, unreadCount, unseenCount, folder, when);
589 eventInfoConfigured = true;
590 }
591 }
592
593 final boolean vibrate = folderPreferences.isNotificationVibrateEnabled();
594 final String ringtoneUri = folderPreferences.getNotificationRingtoneUri();
595 final boolean notifyOnce = !folderPreferences.isEveryMessageNotificationEnabled();
596
Scott Kennedyff8553f2013-04-05 20:57:44 -0700597 if (!ignoreUnobtrusiveSetting && notifyOnce) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800598 // If the user has "unobtrusive notifications" enabled, only alert the first time
599 // new mail is received in this account. This is the default behavior. See
600 // bugs 2412348 and 2413490.
601 notification.setOnlyAlertOnce(true);
602 }
603
Scott Kennedy13a73272013-05-09 09:45:03 -0700604 LogUtils.d(LOG_TAG, "NotificationUtils: Account: %s vibrate: %s", account.name,
Scott Kennedyff8553f2013-04-05 20:57:44 -0700605 Boolean.toString(folderPreferences.isNotificationVibrateEnabled()));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800606
607 int defaults = 0;
608
609 /*
610 * We do not want to notify if this is coming back from an Undo notification, hence the
611 * oldWhen check.
612 */
Scott Kennedyff8553f2013-04-05 20:57:44 -0700613 if (getAttention && oldWhen == 0) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800614 final AccountPreferences accountPreferences =
615 new AccountPreferences(context, account.name);
616 if (accountPreferences.areNotificationsEnabled()) {
617 if (vibrate) {
618 defaults |= Notification.DEFAULT_VIBRATE;
619 }
620
621 notification.setSound(TextUtils.isEmpty(ringtoneUri) ? null
622 : Uri.parse(ringtoneUri));
Scott Kennedy13a73272013-05-09 09:45:03 -0700623 LogUtils.d(LOG_TAG, "NotificationUtils: "
624 + "New email in %s vibrateWhen: %s, playing notification: %s",
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800625 account.name, vibrate, ringtoneUri);
626 }
627 }
628
629 if (eventInfoConfigured) {
630 defaults |= Notification.DEFAULT_LIGHTS;
631 notification.setDefaults(defaults);
632
633 if (oldWhen != 0) {
634 // We do not want to display the ticker again if we are re-displaying this
635 // notification (like from an Undo notification)
636 notification.setTicker(null);
637 }
638
639 nm.notify(notificationId, notification.build());
640 }
641 } finally {
642 if (cursor != null) {
643 cursor.close();
644 }
645 }
646 }
647
648 /**
649 * @return an {@link Intent} which, if launched, will display the corresponding conversation
650 */
Scott Kennedy09400ef2013-03-07 11:09:47 -0800651 private static Intent createViewConversationIntent(final Context context, final Account account,
652 final Folder folder, final Cursor cursor) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800653 if (folder == null || account == null) {
Scott Kennedy13a73272013-05-09 09:45:03 -0700654 LogUtils.e(LOG_TAG, "NotificationUtils#createViewConversationIntent(): "
655 + "Null account or folder. account: %s folder: %s", account, folder);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800656 return null;
657 }
658
659 final Intent intent;
660
661 if (cursor == null) {
Scott Kennedy259df5b2013-07-11 13:24:01 -0700662 intent = Utils.createViewFolderIntent(context, folder.folderUri.fullUri, account);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800663 } else {
664 // A conversation cursor has been specified, so this intent is intended to be go
665 // directly to the one new conversation
666
667 // Get the Conversation object
668 final Conversation conversation = new Conversation(cursor);
Scott Kennedy259df5b2013-07-11 13:24:01 -0700669 intent = Utils.createViewConversationIntent(context, conversation,
670 folder.folderUri.fullUri, account);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800671 }
672
673 return intent;
674 }
675
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800676 private static Bitmap getDefaultNotificationIcon(
677 final Context context, final Folder folder, final boolean multipleNew) {
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700678 final int resId;
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800679 if (folder.notificationIconResId != 0) {
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700680 resId = folder.notificationIconResId;
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800681 } else if (multipleNew) {
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700682 resId = R.drawable.ic_notification_multiple_mail_holo_dark;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800683 } else {
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700684 resId = R.drawable.ic_contact_picture;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800685 }
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700686
687 final Bitmap icon = getIcon(context, resId);
688
689 if (icon == null) {
690 LogUtils.e(LOG_TAG, "Couldn't decode notif icon res id %d", resId);
691 }
692
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800693 return icon;
694 }
695
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800696 private static Bitmap getIcon(final Context context, final int resId) {
697 final Bitmap cachedIcon = sNotificationIcons.get(resId);
698 if (cachedIcon != null) {
699 return cachedIcon;
700 }
701
702 final Bitmap icon = BitmapFactory.decodeResource(context.getResources(), resId);
703 sNotificationIcons.put(resId, icon);
704
705 return icon;
706 }
707
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800708 private static void configureLatestEventInfoFromConversation(final Context context,
709 final Account account, final FolderPreferences folderPreferences,
710 final NotificationCompat.Builder notification, final Cursor conversationCursor,
711 final PendingIntent clickIntent, final Intent notificationIntent,
712 final String notificationAccount, final int unreadCount, final int unseenCount,
713 final Folder folder, final long when) {
714 final Resources res = context.getResources();
715
Scott Kennedy13a73272013-05-09 09:45:03 -0700716 LogUtils.w(LOG_TAG, "NotificationUtils: Showing notification with unreadCount of %d and "
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800717 + "unseenCount of %d", unreadCount, unseenCount);
718
719 String notificationTicker = null;
720
721 // Boolean indicating that this notification is for a non-inbox label.
Scott Kennedy259df5b2013-07-11 13:24:01 -0700722 final boolean isInbox = folder.folderUri.fullUri.equals(account.settings.defaultInbox);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800723
724 // Notification label name for user label notifications.
725 final String notificationLabelName = isInbox ? null : folder.name;
726
727 if (unseenCount > 1) {
728 // Build the string that describes the number of new messages
729 final String newMessagesString = res.getString(R.string.new_messages, unseenCount);
730
731 // Use the default notification icon
732 notification.setLargeIcon(
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800733 getDefaultNotificationIcon(context, folder, true /* multiple new messages */));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800734
735 // The ticker initially start as the new messages string.
736 notificationTicker = newMessagesString;
737
738 // The title of the notification is the new messages string
739 notification.setContentTitle(newMessagesString);
740
Scott Kennedyc8bc5d12013-04-25 08:46:06 -0700741 // TODO(skennedy) Can we remove this check?
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800742 if (com.android.mail.utils.Utils.isRunningJellybeanOrLater()) {
743 // For a new-style notification
744 final int maxNumDigestItems = context.getResources().getInteger(
745 R.integer.max_num_notification_digest_items);
746
747 // The body of the notification is the account name, or the label name.
748 notification.setSubText(isInbox ? notificationAccount : notificationLabelName);
749
750 final NotificationCompat.InboxStyle digest =
751 new NotificationCompat.InboxStyle(notification);
752
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800753 int numDigestItems = 0;
754 do {
755 final Conversation conversation = new Conversation(conversationCursor);
756
757 if (!conversation.read) {
758 boolean multipleUnreadThread = false;
759 // TODO(cwren) extract this pattern into a helper
760
761 Cursor cursor = null;
762 MessageCursor messageCursor = null;
763 try {
764 final Uri.Builder uriBuilder = conversation.messageListUri.buildUpon();
765 uriBuilder.appendQueryParameter(
766 UIProvider.LABEL_QUERY_PARAMETER, notificationLabelName);
767 cursor = context.getContentResolver().query(uriBuilder.build(),
768 UIProvider.MESSAGE_PROJECTION, null, null, null);
769 messageCursor = new MessageCursor(cursor);
770
Yu Ping Hub18d75a2013-03-12 17:04:58 -0700771 String from = "";
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800772 String fromAddress = "";
773 if (messageCursor.moveToPosition(messageCursor.getCount() - 1)) {
774 final Message message = messageCursor.getMessage();
775 fromAddress = message.getFrom();
Yu Ping Hub18d75a2013-03-12 17:04:58 -0700776 if (fromAddress == null) {
777 fromAddress = "";
778 }
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800779 from = getDisplayableSender(fromAddress);
780 }
781 while (messageCursor.moveToPosition(messageCursor.getPosition() - 1)) {
782 final Message message = messageCursor.getMessage();
783 if (!message.read &&
784 !fromAddress.contentEquals(message.getFrom())) {
785 multipleUnreadThread = true;
786 break;
787 }
788 }
789 final SpannableStringBuilder sendersBuilder;
790 if (multipleUnreadThread) {
791 final int sendersLength =
792 res.getInteger(R.integer.swipe_senders_length);
793
794 sendersBuilder = getStyledSenders(context, conversationCursor,
795 sendersLength, notificationAccount);
796 } else {
Paul Westbrook3507af92013-03-30 17:06:12 -0700797 if (from == null) {
Scott Kennedy13a73272013-05-09 09:45:03 -0700798 LogUtils.e(LOG_TAG, "NotificationUtils: null from string in " +
Paul Westbrook3507af92013-03-30 17:06:12 -0700799 "configureLatestEventInfoFromConversation");
800 from = "";
801 }
Andrew Sapperstein8173cf42013-07-01 13:17:18 -0700802 sendersBuilder = new SpannableStringBuilder(sBidiFormatter.unicodeWrap(from));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800803 }
804 final CharSequence digestLine = getSingleMessageInboxLine(context,
805 sendersBuilder.toString(),
806 conversation.subject,
807 conversation.snippet);
808 digest.addLine(digestLine);
809 numDigestItems++;
810 } finally {
811 if (messageCursor != null) {
812 messageCursor.close();
813 }
814 if (cursor != null) {
815 cursor.close();
816 }
817 }
818 }
819 } while (numDigestItems <= maxNumDigestItems && conversationCursor.moveToNext());
820 } else {
821 // The body of the notification is the account name, or the label name.
822 notification.setContentText(
823 isInbox ? notificationAccount : notificationLabelName);
824 }
825 } else {
826 // For notifications for a single new conversation, we want to get the information from
827 // the conversation
828
829 // Move the cursor to the most recent unread conversation
830 seekToLatestUnreadConversation(conversationCursor);
831
832 final Conversation conversation = new Conversation(conversationCursor);
833
834 Cursor cursor = null;
835 MessageCursor messageCursor = null;
836 boolean multipleUnseenThread = false;
837 String from = null;
838 try {
Scott Kennedyffbf86f2013-03-08 11:48:15 -0800839 final Uri uri = conversation.messageListUri.buildUpon().appendQueryParameter(
840 UIProvider.LABEL_QUERY_PARAMETER, folder.persistentId).build();
841 cursor = context.getContentResolver().query(uri, UIProvider.MESSAGE_PROJECTION,
842 null, null, null);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800843 messageCursor = new MessageCursor(cursor);
844 // Use the information from the last sender in the conversation that triggered
845 // this notification.
846
847 String fromAddress = "";
848 if (messageCursor.moveToPosition(messageCursor.getCount() - 1)) {
849 final Message message = messageCursor.getMessage();
850 fromAddress = message.getFrom();
851 from = getDisplayableSender(fromAddress);
852 notification.setLargeIcon(
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700853 getContactIcon(context, from, getSenderAddress(fromAddress), folder));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800854 }
855
856 // Assume that the last message in this conversation is unread
857 int firstUnseenMessagePos = messageCursor.getPosition();
858 while (messageCursor.moveToPosition(messageCursor.getPosition() - 1)) {
859 final Message message = messageCursor.getMessage();
860 final boolean unseen = !message.seen;
861 if (unseen) {
862 firstUnseenMessagePos = messageCursor.getPosition();
863 if (!multipleUnseenThread
864 && !fromAddress.contentEquals(message.getFrom())) {
865 multipleUnseenThread = true;
866 }
867 }
868 }
869
Scott Kennedyc8bc5d12013-04-25 08:46:06 -0700870 // TODO(skennedy) Can we remove this check?
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800871 if (Utils.isRunningJellybeanOrLater()) {
872 // For a new-style notification
873
874 if (multipleUnseenThread) {
875 // The title of a single conversation is the list of senders.
876 int sendersLength = res.getInteger(R.integer.swipe_senders_length);
877
878 final SpannableStringBuilder sendersBuilder = getStyledSenders(
879 context, conversationCursor, sendersLength, notificationAccount);
880
881 notification.setContentTitle(sendersBuilder);
882 // For a single new conversation, the ticker is based on the sender's name.
883 notificationTicker = sendersBuilder.toString();
884 } else {
Andrew Sapperstein8173cf42013-07-01 13:17:18 -0700885 from = sBidiFormatter.unicodeWrap(from);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800886 // 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 Sapperstein8173cf42013-07-01 13:17:18 -07001089 // senders is already individually unicode wrapped so it does not need to be done here
Andrew Sappersteinf5810962013-06-27 10:41:33 -07001090 final String instantiatedString = String.format(formatString,
Andrew Sapperstein8173cf42013-07-01 13:17:18 -07001091 senders,
Andrew Sappersteinf5810962013-06-27 10:41:33 -07001092 sBidiFormatter.unicodeWrap(subjectSnippet));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001093
1094 final SpannableString spannableString = new SpannableString(instantiatedString);
1095
1096 final boolean isOrderReversed = formatString.indexOf("%2$s") <
1097 formatString.indexOf("%1$s");
1098 final int primaryOffset =
1099 (isOrderReversed ? instantiatedString.lastIndexOf(senders) :
1100 instantiatedString.indexOf(senders));
1101 final int secondaryOffset =
1102 (isOrderReversed ? instantiatedString.lastIndexOf(subjectSnippet) :
1103 instantiatedString.indexOf(subjectSnippet));
1104 spannableString.setSpan(notificationPrimarySpan,
1105 primaryOffset, primaryOffset + senders.length(), 0);
1106 spannableString.setSpan(notificationSecondarySpan,
1107 secondaryOffset, secondaryOffset + subjectSnippet.length(), 0);
1108 return spannableString;
1109 }
1110 }
1111
1112 /**
1113 * Sets the bigtext for a notification for a single new conversation
1114 * @param context
1115 * @param subject Subject of the new message that triggered the notification
Andrew Sappersteinf5810962013-06-27 10:41:33 -07001116 * @return a {@link CharSequence} suitable for use in
1117 * {@link NotificationCompat.Builder#setContentText}
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001118 */
1119 private static CharSequence getSingleMessageLittleText(Context context, String subject) {
1120 final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
1121 context, R.style.NotificationPrimaryText);
1122
1123 final SpannableString spannableString = new SpannableString(subject);
1124 spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0);
1125
1126 return spannableString;
1127 }
1128
1129 /**
1130 * Sets the bigtext for a notification for a single new conversation
1131 *
1132 * @param context
1133 * @param subject Subject of the new message that triggered the notification
1134 * @param message the {@link Message} to be displayed.
1135 * @return a {@link CharSequence} suitable for use in
1136 * {@link android.support.v4.app.NotificationCompat.BigTextStyle}
1137 */
1138 private static CharSequence getSingleMessageBigText(Context context, String subject,
1139 final Message message) {
1140
1141 final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
1142 context, R.style.NotificationPrimaryText);
1143
1144 final String snippet = getMessageBodyWithoutElidedText(message);
1145
1146 // Change multiple newlines (with potential white space between), into a single new line
1147 final String collapsedSnippet =
1148 !TextUtils.isEmpty(snippet) ? snippet.replaceAll("\\n\\s+", "\n") : "";
1149
1150 if (TextUtils.isEmpty(subject)) {
1151 // If the subject is empty, just use the snippet.
1152 return snippet;
1153 } else if (TextUtils.isEmpty(collapsedSnippet)) {
1154 // If the snippet is empty, just use the subject.
1155 final SpannableString spannableString = new SpannableString(subject);
1156 spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0);
1157
1158 return spannableString;
1159 } else {
1160 final String notificationBigTextFormat = context.getResources().getString(
1161 R.string.single_new_message_notification_big_text);
1162
1163 // Localizers may change the order of the parameters, look at how the format
1164 // string is structured.
1165 final boolean isSubjectFirst = notificationBigTextFormat.indexOf("%2$s") >
1166 notificationBigTextFormat.indexOf("%1$s");
1167 final String bigText =
1168 String.format(notificationBigTextFormat, subject, collapsedSnippet);
1169 final SpannableString spannableString = new SpannableString(bigText);
1170
1171 final int subjectOffset =
1172 (isSubjectFirst ? bigText.indexOf(subject) : bigText.lastIndexOf(subject));
1173 spannableString.setSpan(notificationSubjectSpan,
1174 subjectOffset, subjectOffset + subject.length(), 0);
1175
1176 return spannableString;
1177 }
1178 }
1179
1180 /**
1181 * Gets the title for a notification for a single new conversation
1182 * @param context
1183 * @param sender Sender of the new message that triggered the notification.
1184 * @param subject Subject of the new message that triggered the notification
1185 * @return a {@link CharSequence} suitable for use as a {@link Notification} title.
1186 */
1187 private static CharSequence getSingleMessageNotificationTitle(Context context,
1188 String sender, String subject) {
1189
1190 if (TextUtils.isEmpty(subject)) {
1191 // If the subject is empty, just set the title to the sender's information.
1192 return sender;
1193 } else {
1194 final String notificationTitleFormat = context.getResources().getString(
1195 R.string.single_new_message_notification_title);
1196
1197 // Localizers may change the order of the parameters, look at how the format
1198 // string is structured.
1199 final boolean isSubjectLast = notificationTitleFormat.indexOf("%2$s") >
1200 notificationTitleFormat.indexOf("%1$s");
1201 final String titleString = String.format(notificationTitleFormat, sender, subject);
1202
1203 // Format the string so the subject is using the secondaryText style
1204 final SpannableString titleSpannable = new SpannableString(titleString);
1205
1206 // Find the offset of the subject.
1207 final int subjectOffset =
1208 isSubjectLast ? titleString.lastIndexOf(subject) : titleString.indexOf(subject);
1209 final TextAppearanceSpan notificationSubjectSpan =
1210 new TextAppearanceSpan(context, R.style.NotificationSecondaryText);
1211 titleSpannable.setSpan(notificationSubjectSpan,
1212 subjectOffset, subjectOffset + subject.length(), 0);
1213 return titleSpannable;
1214 }
1215 }
1216
1217 /**
Scott Kennedy7f8aed62013-05-22 18:35:17 -07001218 * Clears the notifications for the specified account/folder.
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001219 */
Scott Kennedy7f8aed62013-05-22 18:35:17 -07001220 public static void clearFolderNotification(Context context, Account account, Folder folder,
1221 final boolean markSeen) {
Scott Kennedy13a73272013-05-09 09:45:03 -07001222 LogUtils.v(LOG_TAG, "NotificationUtils: Clearing all notifications for %s/%s", account.name,
1223 folder.name);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001224 final NotificationMap notificationMap = getNotificationMap(context);
1225 final NotificationKey key = new NotificationKey(account, folder);
1226 notificationMap.remove(key);
1227 notificationMap.saveNotificationMap(context);
1228
Scott Kennedy7f8aed62013-05-22 18:35:17 -07001229 final NotificationManager notificationManager =
1230 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
1231 notificationManager.cancel(getNotificationId(account.name, folder));
1232
1233 if (markSeen) {
1234 markSeen(context, folder);
1235 }
1236 }
1237
1238 /**
1239 * Clears all notifications for the specified account.
1240 */
1241 public static void clearAccountNotifications(final Context context, final String account) {
1242 LogUtils.v(LOG_TAG, "NotificationUtils: Clearing all notifications for %s", account);
1243 final NotificationMap notificationMap = getNotificationMap(context);
1244
1245 // Find all NotificationKeys for this account
1246 final ImmutableList.Builder<NotificationKey> keyBuilder = ImmutableList.builder();
1247
1248 for (final NotificationKey key : notificationMap.keySet()) {
1249 if (account.equals(key.account.name)) {
1250 keyBuilder.add(key);
1251 }
1252 }
1253
1254 final List<NotificationKey> notificationKeys = keyBuilder.build();
1255
1256 final NotificationManager notificationManager =
1257 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
1258
1259 for (final NotificationKey notificationKey : notificationKeys) {
1260 final Folder folder = notificationKey.folder;
1261 notificationManager.cancel(getNotificationId(account, folder));
1262 notificationMap.remove(notificationKey);
1263 }
1264
1265 notificationMap.saveNotificationMap(context);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001266 }
1267
1268 private static ArrayList<Long> findContacts(Context context, Collection<String> addresses) {
1269 ArrayList<String> whereArgs = new ArrayList<String>();
1270 StringBuilder whereBuilder = new StringBuilder();
1271 String[] questionMarks = new String[addresses.size()];
1272
1273 whereArgs.addAll(addresses);
1274 Arrays.fill(questionMarks, "?");
1275 whereBuilder.append(Email.DATA1 + " IN (").
1276 append(TextUtils.join(",", questionMarks)).
1277 append(")");
1278
1279 ContentResolver resolver = context.getContentResolver();
1280 Cursor c = resolver.query(Email.CONTENT_URI,
1281 new String[]{Email.CONTACT_ID}, whereBuilder.toString(),
1282 whereArgs.toArray(new String[0]), null);
1283
1284 ArrayList<Long> contactIds = new ArrayList<Long>();
1285 if (c == null) {
1286 return contactIds;
1287 }
1288 try {
1289 while (c.moveToNext()) {
1290 contactIds.add(c.getLong(0));
1291 }
1292 } finally {
1293 c.close();
1294 }
1295 return contactIds;
1296 }
1297
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001298 private static Bitmap getContactIcon(final Context context, final String displayName,
1299 final String senderAddress, final Folder folder) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001300 if (senderAddress == null) {
1301 return null;
1302 }
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001303
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001304 Bitmap icon = null;
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001305
1306 final List<Long> contactIds = findContacts( context, Arrays.asList(
1307 new String[] { senderAddress }));
1308
1309 // Get the ideal size for this icon.
1310 final Resources res = context.getResources();
1311 final int idealIconHeight =
1312 res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
1313 final int idealIconWidth =
1314 res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001315
1316 if (contactIds != null) {
Scott Kennedy4046d062013-06-27 14:46:09 -07001317 for (final long id : contactIds) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001318 final Uri contactUri =
1319 ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id);
1320 final Uri photoUri = Uri.withAppendedPath(contactUri, Photo.CONTENT_DIRECTORY);
1321 final Cursor cursor = context.getContentResolver().query(
1322 photoUri, new String[] { Photo.PHOTO }, null, null, null);
1323
1324 if (cursor != null) {
1325 try {
1326 if (cursor.moveToFirst()) {
Scott Kennedy4046d062013-06-27 14:46:09 -07001327 final byte[] data = cursor.getBlob(0);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001328 if (data != null) {
1329 icon = BitmapFactory.decodeStream(new ByteArrayInputStream(data));
1330 if (icon != null && icon.getHeight() < idealIconHeight) {
1331 // We should scale this image to fit the intended size
1332 icon = Bitmap.createScaledBitmap(
1333 icon, idealIconWidth, idealIconHeight, true);
1334 }
1335 if (icon != null) {
1336 break;
1337 }
1338 }
1339 }
1340 } finally {
1341 cursor.close();
1342 }
1343 }
1344 }
1345 }
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001346
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001347 if (icon == null) {
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001348 // Make a colorful tile!
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001349 final Dimensions dimensions = new Dimensions(idealIconWidth, idealIconHeight,
1350 Dimensions.SCALE_ONE);
1351
Scott Kennedyfe235122013-06-28 12:06:36 -07001352 icon = new LetterTileProvider(context).getLetterTile(dimensions,
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001353 displayName, senderAddress);
1354 }
1355
1356 if (icon == null) {
1357 // Icon should be the default mail icon.
Scott Kennedy61bd0e82012-12-10 18:18:17 -08001358 icon = getDefaultNotificationIcon(context, folder, false /* single new message */);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001359 }
1360 return icon;
1361 }
1362
1363 private static String getMessageBodyWithoutElidedText(final Message message) {
Scott Kennedy5e7e88b2013-03-07 16:42:12 -08001364 return getMessageBodyWithoutElidedText(message.getBodyAsHtml());
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001365 }
1366
1367 public static String getMessageBodyWithoutElidedText(String html) {
1368 if (TextUtils.isEmpty(html)) {
1369 return "";
1370 }
1371 // Get the html "tree" for this message body
1372 final HtmlTree htmlTree = com.android.mail.utils.Utils.getHtmlTree(html);
1373 htmlTree.setPlainTextConverterFactory(MESSAGE_CONVERTER_FACTORY);
1374
1375 return htmlTree.getPlainText();
1376 }
1377
1378 public static void markSeen(final Context context, final Folder folder) {
Scott Kennedy259df5b2013-07-11 13:24:01 -07001379 final Uri uri = folder.folderUri.fullUri;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001380
1381 final ContentValues values = new ContentValues(1);
1382 values.put(UIProvider.ConversationColumns.SEEN, 1);
1383
1384 context.getContentResolver().update(uri, values, null, null);
1385 }
1386
1387 /**
1388 * Returns a displayable string representing
1389 * the message sender. It has a preference toward showing the name,
1390 * but will fall back to the address if that is all that is available.
1391 */
1392 private static String getDisplayableSender(String sender) {
1393 final EmailAddress address = EmailAddress.getEmailAddress(sender);
1394
1395 String displayableSender = address.getName();
Scott Kennedy390cab92013-05-23 14:29:49 -07001396
1397 if (!TextUtils.isEmpty(displayableSender)) {
1398 return Address.decodeAddressName(displayableSender);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001399 }
Scott Kennedy390cab92013-05-23 14:29:49 -07001400
1401 // If that fails, default to the sender address.
1402 displayableSender = address.getAddress();
1403
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001404 // If we were unable to tokenize a name or address,
1405 // just use whatever was in the sender.
1406 if (TextUtils.isEmpty(displayableSender)) {
1407 displayableSender = sender;
1408 }
1409 return displayableSender;
1410 }
1411
1412 /**
1413 * Returns only the address portion of a message sender.
1414 */
1415 private static String getSenderAddress(String sender) {
1416 final EmailAddress address = EmailAddress.getEmailAddress(sender);
1417
1418 String tokenizedAddress = address.getAddress();
1419
1420 // If we were unable to tokenize a name or address,
1421 // just use whatever was in the sender.
1422 if (TextUtils.isEmpty(tokenizedAddress)) {
1423 tokenizedAddress = sender;
1424 }
1425 return tokenizedAddress;
1426 }
1427
1428 public static int getNotificationId(final String account, final Folder folder) {
1429 return 1 ^ account.hashCode() ^ folder.hashCode();
1430 }
1431
1432 private static class NotificationKey {
1433 public final Account account;
1434 public final Folder folder;
1435
1436 public NotificationKey(Account account, Folder folder) {
1437 this.account = account;
1438 this.folder = folder;
1439 }
1440
1441 @Override
1442 public boolean equals(Object other) {
1443 if (!(other instanceof NotificationKey)) {
1444 return false;
1445 }
1446 NotificationKey key = (NotificationKey) other;
1447 return account.equals(key.account) && folder.equals(key.folder);
1448 }
1449
1450 @Override
1451 public String toString() {
Scott Kennedye8a7c4c2013-05-09 11:56:50 -07001452 return account.name + " " + folder.name;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001453 }
1454
1455 @Override
1456 public int hashCode() {
1457 final int accountHashCode = account.hashCode();
1458 final int folderHashCode = folder.hashCode();
1459 return accountHashCode ^ folderHashCode;
1460 }
1461 }
1462
1463 /**
1464 * Contains the logic for converting the contents of one HtmlTree into
1465 * plaintext.
1466 */
1467 public static class MailMessagePlainTextConverter extends HtmlTree.DefaultPlainTextConverter {
1468 // Strings for parsing html message bodies
1469 private static final String ELIDED_TEXT_ELEMENT_NAME = "div";
1470 private static final String ELIDED_TEXT_ELEMENT_ATTRIBUTE_NAME = "class";
1471 private static final String ELIDED_TEXT_ELEMENT_ATTRIBUTE_CLASS_VALUE = "elided-text";
1472
1473 private static final HTML.Attribute ELIDED_TEXT_ATTRIBUTE =
1474 new HTML.Attribute(ELIDED_TEXT_ELEMENT_ATTRIBUTE_NAME, HTML.Attribute.NO_TYPE);
1475
1476 private static final HtmlDocument.Node ELIDED_TEXT_REPLACEMENT_NODE =
1477 HtmlDocument.createSelfTerminatingTag(HTML4.BR_ELEMENT, null, null, null);
1478
1479 private int mEndNodeElidedTextBlock = -1;
1480
1481 @Override
1482 public void addNode(HtmlDocument.Node n, int nodeNum, int endNum) {
1483 // If we are in the middle of an elided text block, don't add this node
1484 if (nodeNum < mEndNodeElidedTextBlock) {
1485 return;
1486 } else if (nodeNum == mEndNodeElidedTextBlock) {
1487 super.addNode(ELIDED_TEXT_REPLACEMENT_NODE, nodeNum, endNum);
1488 return;
1489 }
1490
1491 // If this tag starts another elided text block, we want to remember the end
1492 if (n instanceof HtmlDocument.Tag) {
1493 boolean foundElidedTextTag = false;
1494 final HtmlDocument.Tag htmlTag = (HtmlDocument.Tag)n;
1495 final HTML.Element htmlElement = htmlTag.getElement();
1496 if (ELIDED_TEXT_ELEMENT_NAME.equals(htmlElement.getName())) {
1497 // Make sure that the class is what is expected
1498 final List<HtmlDocument.TagAttribute> attributes =
1499 htmlTag.getAttributes(ELIDED_TEXT_ATTRIBUTE);
1500 for (HtmlDocument.TagAttribute attribute : attributes) {
1501 if (ELIDED_TEXT_ELEMENT_ATTRIBUTE_CLASS_VALUE.equals(
1502 attribute.getValue())) {
1503 // Found an "elided-text" div. Remember information about this tag
1504 mEndNodeElidedTextBlock = endNum;
1505 foundElidedTextTag = true;
1506 break;
1507 }
1508 }
1509 }
1510
1511 if (foundElidedTextTag) {
1512 return;
1513 }
1514 }
1515
Scott Kennedyc56b2332013-05-23 18:24:53 -07001516 super.addNode(n, nodeNum, endNum);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001517 }
1518 }
1519
1520 /**
1521 * During account setup in Email, we may not have an inbox yet, so the notification setting had
1522 * to be stored in {@link AccountPreferences}. If it is still there, we need to move it to the
1523 * {@link FolderPreferences} now.
1524 */
1525 public static void moveNotificationSetting(final AccountPreferences accountPreferences,
1526 final FolderPreferences folderPreferences) {
1527 if (accountPreferences.isDefaultInboxNotificationsEnabledSet()) {
1528 // If this setting has been changed some other way, don't overwrite it
1529 if (!folderPreferences.isNotificationsEnabledSet()) {
1530 final boolean notificationsEnabled =
1531 accountPreferences.getDefaultInboxNotificationsEnabled();
1532
1533 folderPreferences.setNotificationsEnabled(notificationsEnabled);
1534 }
1535
1536 accountPreferences.clearDefaultInboxNotificationsEnabled();
1537 }
1538 }
1539}