blob: 4c32e4459197cc01c61c5985ed27d1c1f848054b [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;
Andy Huang4fe0af82013-08-20 17:24:51 -070047import com.android.mail.analytics.Analytics;
48import com.android.mail.analytics.AnalyticsUtils;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080049import com.android.mail.browse.MessageCursor;
50import com.android.mail.browse.SendersView;
Scott Kennedyfe235122013-06-28 12:06:36 -070051import com.android.mail.photomanager.LetterTileProvider;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080052import com.android.mail.preferences.AccountPreferences;
53import com.android.mail.preferences.FolderPreferences;
54import com.android.mail.preferences.MailPrefs;
55import com.android.mail.providers.Account;
Scott Kennedy390cab92013-05-23 14:29:49 -070056import com.android.mail.providers.Address;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080057import com.android.mail.providers.Conversation;
58import com.android.mail.providers.Folder;
59import com.android.mail.providers.Message;
60import com.android.mail.providers.UIProvider;
Scott Kennedyc94c80c2013-06-27 14:42:41 -070061import com.android.mail.ui.ImageCanvas.Dimensions;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080062import com.android.mail.utils.NotificationActionUtils.NotificationAction;
Scott Kennedy1bdbfef2013-08-01 21:48:26 -070063import com.google.android.mail.common.html.parser.HTML;
64import com.google.android.mail.common.html.parser.HTML4;
65import com.google.android.mail.common.html.parser.HtmlDocument;
66import com.google.android.mail.common.html.parser.HtmlTree;
Andrew Sappersteinddd17bc2013-04-22 14:03:40 -070067import com.google.common.base.Objects;
Scott Kennedy7f8aed62013-05-22 18:35:17 -070068import com.google.common.collect.ImmutableList;
Scott Kennedy09400ef2013-03-07 11:09:47 -080069import com.google.common.collect.Lists;
Scott Kennedy09400ef2013-03-07 11:09:47 -080070import com.google.common.collect.Sets;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080071
72import java.io.ByteArrayInputStream;
73import java.util.ArrayList;
74import java.util.Arrays;
75import java.util.Collection;
76import java.util.List;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080077import java.util.Set;
78import java.util.concurrent.ConcurrentHashMap;
79
80public class NotificationUtils {
Scott Kennedyc7d75472013-07-08 18:21:28 -070081 public static final String LOG_TAG = "NotifUtils";
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080082
83 /** Contains a list of <(account, label), unread conversations> */
84 private static NotificationMap sActiveNotificationMap = null;
85
Scott Kennedy61bd0e82012-12-10 18:18:17 -080086 private static final SparseArray<Bitmap> sNotificationIcons = new SparseArray<Bitmap>();
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080087
88 private static TextAppearanceSpan sNotificationUnreadStyleSpan;
89 private static CharacterStyle sNotificationReadStyleSpan;
90
Scott Kennedyd5edd2d2012-12-05 11:11:32 -080091 /** A factory that produces a plain text converter that removes elided text. */
92 private static final HtmlTree.PlainTextConverterFactory MESSAGE_CONVERTER_FACTORY =
93 new HtmlTree.PlainTextConverterFactory() {
94 @Override
95 public HtmlTree.PlainTextConverter createInstance() {
96 return new MailMessagePlainTextConverter();
97 }
98 };
99
Paul Westbrook12fb02b2013-08-27 14:34:49 -0700100 private static final BidiFormatter BIDI_FORMATTER = BidiFormatter.getInstance();
Andrew Sappersteinf5810962013-06-27 10:41:33 -0700101
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800102 /**
103 * Clears all notifications in response to the user tapping "Clear" in the status bar.
104 */
105 public static void clearAllNotfications(Context context) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700106 LogUtils.v(LOG_TAG, "Clearing all notifications.");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800107 final NotificationMap notificationMap = getNotificationMap(context);
108 notificationMap.clear();
109 notificationMap.saveNotificationMap(context);
110 }
111
112 /**
113 * Returns the notification map, creating it if necessary.
114 */
115 private static synchronized NotificationMap getNotificationMap(Context context) {
116 if (sActiveNotificationMap == null) {
117 sActiveNotificationMap = new NotificationMap();
118
119 // populate the map from the cached data
120 sActiveNotificationMap.loadNotificationMap(context);
121 }
122 return sActiveNotificationMap;
123 }
124
125 /**
126 * Class representing the existing notifications, and the number of unread and
127 * unseen conversations that triggered each.
128 */
129 private static class NotificationMap
130 extends ConcurrentHashMap<NotificationKey, Pair<Integer, Integer>> {
131
132 private static final String NOTIFICATION_PART_SEPARATOR = " ";
133 private static final int NUM_NOTIFICATION_PARTS= 4;
134
135 /**
136 * Retuns the unread count for the given NotificationKey.
137 */
138 public Integer getUnread(NotificationKey key) {
139 final Pair<Integer, Integer> value = get(key);
140 return value != null ? value.first : null;
141 }
142
143 /**
144 * Retuns the unread unseen count for the given NotificationKey.
145 */
146 public Integer getUnseen(NotificationKey key) {
147 final Pair<Integer, Integer> value = get(key);
148 return value != null ? value.second : null;
149 }
150
151 /**
152 * Store the unread and unseen value for the given NotificationKey
153 */
154 public void put(NotificationKey key, int unread, int unseen) {
155 final Pair<Integer, Integer> value =
156 new Pair<Integer, Integer>(Integer.valueOf(unread), Integer.valueOf(unseen));
157 put(key, value);
158 }
159
160 /**
161 * Populates the notification map with previously cached data.
162 */
163 public synchronized void loadNotificationMap(final Context context) {
164 final MailPrefs mailPrefs = MailPrefs.get(context);
165 final Set<String> notificationSet = mailPrefs.getActiveNotificationSet();
166 if (notificationSet != null) {
167 for (String notificationEntry : notificationSet) {
168 // Get the parts of the string that make the notification entry
169 final String[] notificationParts =
170 TextUtils.split(notificationEntry, NOTIFICATION_PART_SEPARATOR);
171 if (notificationParts.length == NUM_NOTIFICATION_PARTS) {
172 final Uri accountUri = Uri.parse(notificationParts[0]);
173 final Cursor accountCursor = context.getContentResolver().query(
174 accountUri, UIProvider.ACCOUNTS_PROJECTION, null, null, null);
175 final Account account;
176 try {
177 if (accountCursor.moveToFirst()) {
178 account = new Account(accountCursor);
179 } else {
180 continue;
181 }
182 } finally {
183 accountCursor.close();
184 }
185
186 final Uri folderUri = Uri.parse(notificationParts[1]);
187 final Cursor folderCursor = context.getContentResolver().query(
188 folderUri, UIProvider.FOLDERS_PROJECTION, null, null, null);
189 final Folder folder;
190 try {
191 if (folderCursor.moveToFirst()) {
192 folder = new Folder(folderCursor);
193 } else {
194 continue;
195 }
196 } finally {
197 folderCursor.close();
198 }
199
200 final NotificationKey key = new NotificationKey(account, folder);
201 final Integer unreadValue = Integer.valueOf(notificationParts[2]);
202 final Integer unseenValue = Integer.valueOf(notificationParts[3]);
203 final Pair<Integer, Integer> unreadUnseenValue =
204 new Pair<Integer, Integer>(unreadValue, unseenValue);
205 put(key, unreadUnseenValue);
206 }
207 }
208 }
209 }
210
211 /**
212 * Cache the notification map.
213 */
214 public synchronized void saveNotificationMap(Context context) {
215 final Set<String> notificationSet = Sets.newHashSet();
216 final Set<NotificationKey> keys = keySet();
217 for (NotificationKey key : keys) {
218 final Pair<Integer, Integer> value = get(key);
219 final Integer unreadCount = value.first;
220 final Integer unseenCount = value.second;
Scott Kennedyff8553f2013-04-05 20:57:44 -0700221 if (unreadCount != null && unseenCount != null) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800222 final String[] partValues = new String[] {
Scott Kennedy259df5b2013-07-11 13:24:01 -0700223 key.account.uri.toString(), key.folder.folderUri.fullUri.toString(),
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800224 unreadCount.toString(), unseenCount.toString()};
225 notificationSet.add(TextUtils.join(NOTIFICATION_PART_SEPARATOR, partValues));
226 }
227 }
228 final MailPrefs mailPrefs = MailPrefs.get(context);
229 mailPrefs.cacheActiveNotificationSet(notificationSet);
230 }
231 }
232
233 /**
234 * @return the title of this notification with each account and the number of unread and unseen
235 * conversations for it. Also remove any account in the map that has 0 unread.
236 */
237 private static String createNotificationString(NotificationMap notifications) {
238 StringBuilder result = new StringBuilder();
239 int i = 0;
240 Set<NotificationKey> keysToRemove = Sets.newHashSet();
241 for (NotificationKey key : notifications.keySet()) {
242 Integer unread = notifications.getUnread(key);
243 Integer unseen = notifications.getUnseen(key);
244 if (unread == null || unread.intValue() == 0) {
245 keysToRemove.add(key);
246 } else {
247 if (i > 0) result.append(", ");
248 result.append(key.toString() + " (" + unread + ", " + unseen + ")");
249 i++;
250 }
251 }
252
253 for (NotificationKey key : keysToRemove) {
254 notifications.remove(key);
255 }
256
257 return result.toString();
258 }
259
260 /**
261 * Get all notifications for all accounts and cancel them.
262 **/
263 public static void cancelAllNotifications(Context context) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700264 LogUtils.d(LOG_TAG, "cancelAllNotifications - cancelling all");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800265 NotificationManager nm = (NotificationManager) context.getSystemService(
266 Context.NOTIFICATION_SERVICE);
267 nm.cancelAll();
268 clearAllNotfications(context);
269 }
270
271 /**
272 * Get all notifications for all accounts, cancel them, and repost.
273 * This happens when locale changes.
274 **/
275 public static void cancelAndResendNotifications(Context context) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700276 LogUtils.d(LOG_TAG, "cancelAndResendNotifications");
Andrew Sappersteinddd17bc2013-04-22 14:03:40 -0700277 resendNotifications(context, true, null, null);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800278 }
279
280 /**
281 * Get all notifications for all accounts, optionally cancel them, and repost.
Andrew Sappersteinddd17bc2013-04-22 14:03:40 -0700282 * This happens when locale changes. If you only want to resend messages from one
283 * account-folder pair, pass in the account and folder that should be resent.
284 * All other account-folder pairs will not have their notifications resent.
285 * All notifications will be resent if account or folder is null.
286 *
287 * @param context Current context.
288 * @param cancelExisting True, if all notifications should be canceled before resending.
289 * False, otherwise.
290 * @param accountUri The {@link Uri} of the {@link Account} of the notification
291 * upon which an action occurred.
292 * @param folderUri The {@link Uri} of the {@link Folder} of the notification
293 * upon which an action occurred.
294 */
295 public static void resendNotifications(Context context, final boolean cancelExisting,
Scott Kennedy259df5b2013-07-11 13:24:01 -0700296 final Uri accountUri, final FolderUri folderUri) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700297 LogUtils.d(LOG_TAG, "resendNotifications ");
Scott Kennedy2638b482013-05-06 16:05:11 -0700298
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800299 if (cancelExisting) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700300 LogUtils.d(LOG_TAG, "resendNotifications - cancelling all");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800301 NotificationManager nm =
302 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
303 nm.cancelAll();
304 }
305 // Re-validate the notifications.
306 final NotificationMap notificationMap = getNotificationMap(context);
307 final Set<NotificationKey> keys = notificationMap.keySet();
308 for (NotificationKey notification : keys) {
309 final Folder folder = notification.folder;
Andrew Sappersteinddd17bc2013-04-22 14:03:40 -0700310 final int notificationId = getNotificationId(notification.account.name,
311 folder);
312
313 // Only resend notifications if the notifications are from the same folder
314 // and same account as the undo notification that was previously displayed.
Scott Kennedy26b29ef2013-04-28 18:36:50 -0700315 if (accountUri != null && !Objects.equal(accountUri, notification.account.uri) &&
Scott Kennedy259df5b2013-07-11 13:24:01 -0700316 folderUri != null && !Objects.equal(folderUri, folder.folderUri)) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700317 LogUtils.d(LOG_TAG, "resendNotifications - not resending %s / %s"
Scott Kennedy2638b482013-05-06 16:05:11 -0700318 + " because it doesn't match %s / %s",
Scott Kennedy259df5b2013-07-11 13:24:01 -0700319 notification.account.uri, folder.folderUri, accountUri, folderUri);
Andrew Sappersteinddd17bc2013-04-22 14:03:40 -0700320 continue;
321 }
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800322
Scott Kennedy2f97af92013-07-30 10:42:34 -0700323 LogUtils.d(LOG_TAG, "resendNotifications - resending %s / %s",
Scott Kennedy259df5b2013-07-11 13:24:01 -0700324 notification.account.uri, folder.folderUri);
Scott Kennedy2638b482013-05-06 16:05:11 -0700325
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800326 final NotificationAction undoableAction =
327 NotificationActionUtils.sUndoNotifications.get(notificationId);
328 if (undoableAction == null) {
Andrew Sappersteinddd17bc2013-04-22 14:03:40 -0700329 validateNotifications(context, folder, notification.account, true,
330 false, notification);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800331 } else {
332 // Create an undo notification
333 NotificationActionUtils.createUndoNotification(context, undoableAction);
334 }
335 }
336 }
337
338 /**
339 * Validate the notifications for the specified account.
340 */
341 public static void validateAccountNotifications(Context context, String account) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700342 LogUtils.d(LOG_TAG, "validateAccountNotifications - %s", account);
Scott Kennedy4162f832013-05-06 17:37:55 -0700343
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800344 List<NotificationKey> notificationsToCancel = Lists.newArrayList();
345 // Iterate through the notification map to see if there are any entries that correspond to
346 // labels that are not in the sync set.
347 final NotificationMap notificationMap = getNotificationMap(context);
348 Set<NotificationKey> keys = notificationMap.keySet();
349 final AccountPreferences accountPreferences = new AccountPreferences(context, account);
350 final boolean enabled = accountPreferences.areNotificationsEnabled();
351 if (!enabled) {
352 // Cancel all notifications for this account
353 for (NotificationKey notification : keys) {
354 if (notification.account.name.equals(account)) {
355 notificationsToCancel.add(notification);
356 }
357 }
358 } else {
359 // Iterate through the notification map to see if there are any entries that
360 // correspond to labels that are not in the notification set.
361 for (NotificationKey notification : keys) {
362 if (notification.account.name.equals(account)) {
363 // If notification is not enabled for this label, remember this NotificationKey
364 // to later cancel the notification, and remove the entry from the map
365 final Folder folder = notification.folder;
Scott Kennedy259df5b2013-07-11 13:24:01 -0700366 final boolean isInbox = folder.folderUri.equals(
367 notification.account.settings.defaultInbox);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800368 final FolderPreferences folderPreferences = new FolderPreferences(
369 context, notification.account.name, folder, isInbox);
370
371 if (!folderPreferences.areNotificationsEnabled()) {
372 notificationsToCancel.add(notification);
373 }
374 }
375 }
376 }
377
378 // Cancel & remove the invalid notifications.
379 if (notificationsToCancel.size() > 0) {
380 NotificationManager nm = (NotificationManager) context.getSystemService(
381 Context.NOTIFICATION_SERVICE);
382 for (NotificationKey notification : notificationsToCancel) {
383 final Folder folder = notification.folder;
384 final int notificationId = getNotificationId(notification.account.name, folder);
Scott Kennedy2f97af92013-07-30 10:42:34 -0700385 LogUtils.d(LOG_TAG, "validateAccountNotifications - cancelling %s / %s",
Scott Kennedy93486c02013-04-17 12:15:43 -0700386 notification.account.name, folder.persistentId);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800387 nm.cancel(notificationId);
388 notificationMap.remove(notification);
389 NotificationActionUtils.sUndoNotifications.remove(notificationId);
390 NotificationActionUtils.sNotificationTimestamps.delete(notificationId);
391 }
392 notificationMap.saveNotificationMap(context);
393 }
394 }
395
396 /**
397 * Display only one notification.
398 */
399 public static void setNewEmailIndicator(Context context, final int unreadCount,
400 final int unseenCount, final Account account, final Folder folder,
401 final boolean getAttention) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700402 LogUtils.d(LOG_TAG, "setNewEmailIndicator unreadCount = %d, unseenCount = %d, account = %s,"
403 + " folder = %s, getAttention = %b", unreadCount, unseenCount, account.name,
404 folder.folderUri, getAttention);
Scott Kennedy4162f832013-05-06 17:37:55 -0700405
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800406 boolean ignoreUnobtrusiveSetting = false;
407
Scott Kennedydab8a942013-02-22 12:31:30 -0800408 final int notificationId = getNotificationId(account.name, folder);
409
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800410 // Update the notification map
411 final NotificationMap notificationMap = getNotificationMap(context);
412 final NotificationKey key = new NotificationKey(account, folder);
413 if (unreadCount == 0) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700414 LogUtils.d(LOG_TAG, "setNewEmailIndicator - cancelling %s / %s", account.name,
415 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 {
Paul Westbrookb1b855b2013-09-27 14:08:18 -0700420 LogUtils.d(LOG_TAG, "setNewEmailIndicator - update count for: %s / %s " +
421 "to: unread: %d unseen %d", account.name, folder.persistentId,
422 unreadCount, unseenCount);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800423 if (!notificationMap.containsKey(key)) {
424 // This account previously didn't have any unread mail; ignore the "unobtrusive
425 // notifications" setting and play sound and/or vibrate the device even if a
426 // notification already exists (bug 2412348).
Paul Westbrookb1b855b2013-09-27 14:08:18 -0700427 LogUtils.d(LOG_TAG, "setNewEmailIndicator - ignoringUnobtrusiveSetting");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800428 ignoreUnobtrusiveSetting = true;
429 }
430 notificationMap.put(key, unreadCount, unseenCount);
431 }
432 notificationMap.saveNotificationMap(context);
433
434 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700435 LogUtils.v(LOG_TAG, "New email: %s mapSize: %d getAttention: %b",
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800436 createNotificationString(notificationMap), notificationMap.size(),
437 getAttention);
438 }
439
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800440 if (NotificationActionUtils.sUndoNotifications.get(notificationId) == null) {
441 validateNotifications(context, folder, account, getAttention, ignoreUnobtrusiveSetting,
442 key);
443 }
444 }
445
446 /**
447 * Validate the notifications notification.
448 */
449 private static void validateNotifications(Context context, final Folder folder,
450 final Account account, boolean getAttention, boolean ignoreUnobtrusiveSetting,
451 NotificationKey key) {
452
453 NotificationManager nm = (NotificationManager)
454 context.getSystemService(Context.NOTIFICATION_SERVICE);
455
456 final NotificationMap notificationMap = getNotificationMap(context);
457 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700458 LogUtils.i(LOG_TAG, "Validating Notification: %s mapSize: %d "
Paul Westbrookb1b855b2013-09-27 14:08:18 -0700459 + "folder: %s getAttention: %b ignoreUnobtrusive: %b",
460 createNotificationString(notificationMap),
461 notificationMap.size(), folder.name, getAttention, ignoreUnobtrusiveSetting);
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700462 } else {
463 LogUtils.i(LOG_TAG, "Validating Notification, mapSize: %d "
Paul Westbrookb1b855b2013-09-27 14:08:18 -0700464 + "getAttention: %b ignoreUnobtrusive: %b", notificationMap.size(),
465 getAttention, ignoreUnobtrusiveSetting);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800466 }
467 // The number of unread messages for this account and label.
468 final Integer unread = notificationMap.getUnread(key);
469 final int unreadCount = unread != null ? unread.intValue() : 0;
470 final Integer unseen = notificationMap.getUnseen(key);
471 int unseenCount = unseen != null ? unseen.intValue() : 0;
472
473 Cursor cursor = null;
474
475 try {
476 final Uri.Builder uriBuilder = folder.conversationListUri.buildUpon();
477 uriBuilder.appendQueryParameter(
478 UIProvider.SEEN_QUERY_PARAMETER, Boolean.FALSE.toString());
Andy Huang0be92432013-03-22 18:31:44 -0700479 // Do not allow this quick check to disrupt any active network-enabled conversation
480 // cursor.
481 uriBuilder.appendQueryParameter(
482 UIProvider.ConversationListQueryParameters.USE_NETWORK,
483 Boolean.FALSE.toString());
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800484 cursor = context.getContentResolver().query(uriBuilder.build(),
485 UIProvider.CONVERSATION_PROJECTION, null, null, null);
Yu Ping Hu3a7b45b2013-06-18 18:10:50 -0700486 if (cursor == null) {
487 // This folder doesn't exist.
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700488 LogUtils.i(LOG_TAG,
489 "The cursor is null, so the specified folder probably does not exist");
Yu Ping Hu3a7b45b2013-06-18 18:10:50 -0700490 clearFolderNotification(context, account, folder, false);
491 return;
492 }
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800493 final int cursorUnseenCount = cursor.getCount();
494
495 // Make sure the unseen count matches the number of items in the cursor. But, we don't
496 // want to overwrite a 0 unseen count that was specified in the intent
497 if (unseenCount != 0 && unseenCount != cursorUnseenCount) {
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700498 LogUtils.i(LOG_TAG,
Scott Kennedy2f97af92013-07-30 10:42:34 -0700499 "Unseen count doesn't match cursor count. unseen: %d cursor count: %d",
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800500 unseenCount, cursorUnseenCount);
501 unseenCount = cursorUnseenCount;
502 }
503
504 // For the purpose of the notifications, the unseen count should be capped at the num of
505 // unread conversations.
506 if (unseenCount > unreadCount) {
507 unseenCount = unreadCount;
508 }
509
510 final int notificationId = getNotificationId(account.name, folder);
511
512 if (unseenCount == 0) {
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700513 LogUtils.i(LOG_TAG, "validateNotifications - cancelling account %s / folder %s",
514 LogUtils.sanitizeName(LOG_TAG, account.name),
515 LogUtils.sanitizeName(LOG_TAG, folder.persistentId));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800516 nm.cancel(notificationId);
517 return;
518 }
519
520 // We now have all we need to create the notification and the pending intent
521 PendingIntent clickIntent;
522
523 NotificationCompat.Builder notification = new NotificationCompat.Builder(context);
524 notification.setSmallIcon(R.drawable.stat_notify_email);
525 notification.setTicker(account.name);
526
527 final long when;
528
529 final long oldWhen =
530 NotificationActionUtils.sNotificationTimestamps.get(notificationId);
531 if (oldWhen != 0) {
532 when = oldWhen;
533 } else {
534 when = System.currentTimeMillis();
535 }
536
537 notification.setWhen(when);
538
539 // The timestamp is now stored in the notification, so we can remove it from here
540 NotificationActionUtils.sNotificationTimestamps.delete(notificationId);
541
542 // Dispatch a CLEAR_NEW_MAIL_NOTIFICATIONS intent if the user taps the "X" next to a
543 // notification. Also this intent gets fired when the user taps on a notification as
544 // the AutoCancel flag has been set
545 final Intent cancelNotificationIntent =
546 new Intent(MailIntentService.ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS);
547 cancelNotificationIntent.setPackage(context.getPackageName());
Scott Kennedy09400ef2013-03-07 11:09:47 -0800548 cancelNotificationIntent.setData(Utils.appendVersionQueryParameter(context,
Scott Kennedy259df5b2013-07-11 13:24:01 -0700549 folder.folderUri.fullUri));
Scott Kennedy48cfe462013-04-10 11:32:02 -0700550 cancelNotificationIntent.putExtra(Utils.EXTRA_ACCOUNT, account);
551 cancelNotificationIntent.putExtra(Utils.EXTRA_FOLDER, folder);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800552
553 notification.setDeleteIntent(PendingIntent.getService(
554 context, notificationId, cancelNotificationIntent, 0));
555
556 // Ensure that the notification is cleared when the user selects it
557 notification.setAutoCancel(true);
558
559 boolean eventInfoConfigured = false;
560
Scott Kennedy259df5b2013-07-11 13:24:01 -0700561 final boolean isInbox = folder.folderUri.equals(account.settings.defaultInbox);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800562 final FolderPreferences folderPreferences =
563 new FolderPreferences(context, account.name, folder, isInbox);
564
565 if (isInbox) {
566 final AccountPreferences accountPreferences =
567 new AccountPreferences(context, account.name);
568 moveNotificationSetting(accountPreferences, folderPreferences);
569 }
570
571 if (!folderPreferences.areNotificationsEnabled()) {
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700572 LogUtils.i(LOG_TAG, "Notifications are disabled for this folder; not notifying");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800573 // Don't notify
574 return;
575 }
576
577 if (unreadCount > 0) {
578 // How can I order this properly?
579 if (cursor.moveToNext()) {
Scott Kennedybd8acad2013-05-29 10:22:25 -0700580 final Intent notificationIntent;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800581
Scott Kennedybd8acad2013-05-29 10:22:25 -0700582 // Launch directly to the conversation, if there is only 1 unseen conversation
Andy Huang4fe0af82013-08-20 17:24:51 -0700583 final boolean launchConversationMode = (unseenCount == 1);
584 if (launchConversationMode) {
Scott Kennedy09400ef2013-03-07 11:09:47 -0800585 notificationIntent = createViewConversationIntent(context, account, folder,
586 cursor);
Scott Kennedybd8acad2013-05-29 10:22:25 -0700587 } else {
588 notificationIntent = createViewConversationIntent(context, account, folder,
589 null);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800590 }
591
Andy Huang4fe0af82013-08-20 17:24:51 -0700592 Analytics.getInstance().sendEvent("notification_create",
593 launchConversationMode ? "conversation" : "conversation_list",
594 folder.getTypeDescription(), unseenCount);
595
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800596 if (notificationIntent == null) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700597 LogUtils.e(LOG_TAG, "Null intent when building notification");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800598 return;
599 }
600
Andy Huang4fe0af82013-08-20 17:24:51 -0700601 // Amend the click intent with a hint that its source was a notification,
602 // but remove the hint before it's used to generate notification action
603 // intents. This prevents the following sequence:
604 // 1. generate single notification
605 // 2. user clicks reply, then completes Compose activity
606 // 3. main activity launches, gets FROM_NOTIFICATION hint in intent
607 notificationIntent.putExtra(Utils.EXTRA_FROM_NOTIFICATION, true);
608 clickIntent = PendingIntent.getActivity(context, -1, notificationIntent,
609 PendingIntent.FLAG_UPDATE_CURRENT);
610 notificationIntent.removeExtra(Utils.EXTRA_FROM_NOTIFICATION);
611
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800612 configureLatestEventInfoFromConversation(context, account, folderPreferences,
613 notification, cursor, clickIntent, notificationIntent,
614 account.name, unreadCount, unseenCount, folder, when);
615 eventInfoConfigured = true;
616 }
617 }
618
619 final boolean vibrate = folderPreferences.isNotificationVibrateEnabled();
620 final String ringtoneUri = folderPreferences.getNotificationRingtoneUri();
621 final boolean notifyOnce = !folderPreferences.isEveryMessageNotificationEnabled();
622
Scott Kennedyff8553f2013-04-05 20:57:44 -0700623 if (!ignoreUnobtrusiveSetting && notifyOnce) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800624 // If the user has "unobtrusive notifications" enabled, only alert the first time
625 // new mail is received in this account. This is the default behavior. See
626 // bugs 2412348 and 2413490.
Paul Westbrookb1b855b2013-09-27 14:08:18 -0700627 LogUtils.d(LOG_TAG, "Setting Alert Once");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800628 notification.setOnlyAlertOnce(true);
629 }
630
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700631 LogUtils.i(LOG_TAG, "Account: %s vibrate: %s",
632 LogUtils.sanitizeName(LOG_TAG, account.name),
Scott Kennedyff8553f2013-04-05 20:57:44 -0700633 Boolean.toString(folderPreferences.isNotificationVibrateEnabled()));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800634
635 int defaults = 0;
636
637 /*
638 * We do not want to notify if this is coming back from an Undo notification, hence the
639 * oldWhen check.
640 */
Scott Kennedyff8553f2013-04-05 20:57:44 -0700641 if (getAttention && oldWhen == 0) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800642 final AccountPreferences accountPreferences =
643 new AccountPreferences(context, account.name);
644 if (accountPreferences.areNotificationsEnabled()) {
645 if (vibrate) {
646 defaults |= Notification.DEFAULT_VIBRATE;
647 }
648
649 notification.setSound(TextUtils.isEmpty(ringtoneUri) ? null
650 : Uri.parse(ringtoneUri));
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700651 LogUtils.i(LOG_TAG, "New email in %s vibrateWhen: %s, playing notification: %s",
652 LogUtils.sanitizeName(LOG_TAG, account.name), vibrate, ringtoneUri);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800653 }
654 }
655
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700656 // 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 -0800657 if (eventInfoConfigured) {
658 defaults |= Notification.DEFAULT_LIGHTS;
659 notification.setDefaults(defaults);
660
661 if (oldWhen != 0) {
662 // We do not want to display the ticker again if we are re-displaying this
663 // notification (like from an Undo notification)
664 notification.setTicker(null);
665 }
666
667 nm.notify(notificationId, notification.build());
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700668 } else {
669 LogUtils.i(LOG_TAG, "event info not configured - not notifying");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800670 }
671 } finally {
672 if (cursor != null) {
673 cursor.close();
674 }
675 }
676 }
677
678 /**
679 * @return an {@link Intent} which, if launched, will display the corresponding conversation
680 */
Scott Kennedy09400ef2013-03-07 11:09:47 -0800681 private static Intent createViewConversationIntent(final Context context, final Account account,
682 final Folder folder, final Cursor cursor) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800683 if (folder == null || account == null) {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700684 LogUtils.e(LOG_TAG, "createViewConversationIntent(): "
Scott Kennedy13a73272013-05-09 09:45:03 -0700685 + "Null account or folder. account: %s folder: %s", account, folder);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800686 return null;
687 }
688
689 final Intent intent;
690
691 if (cursor == null) {
Scott Kennedy259df5b2013-07-11 13:24:01 -0700692 intent = Utils.createViewFolderIntent(context, folder.folderUri.fullUri, account);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800693 } else {
694 // A conversation cursor has been specified, so this intent is intended to be go
695 // directly to the one new conversation
696
697 // Get the Conversation object
698 final Conversation conversation = new Conversation(cursor);
Scott Kennedy259df5b2013-07-11 13:24:01 -0700699 intent = Utils.createViewConversationIntent(context, conversation,
700 folder.folderUri.fullUri, account);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800701 }
702
703 return intent;
704 }
705
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800706 private static Bitmap getDefaultNotificationIcon(
707 final Context context, final Folder folder, final boolean multipleNew) {
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700708 final int resId;
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800709 if (folder.notificationIconResId != 0) {
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700710 resId = folder.notificationIconResId;
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800711 } else if (multipleNew) {
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700712 resId = R.drawable.ic_notification_multiple_mail_holo_dark;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800713 } else {
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700714 resId = R.drawable.ic_contact_picture;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800715 }
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700716
717 final Bitmap icon = getIcon(context, resId);
718
719 if (icon == null) {
720 LogUtils.e(LOG_TAG, "Couldn't decode notif icon res id %d", resId);
721 }
722
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800723 return icon;
724 }
725
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800726 private static Bitmap getIcon(final Context context, final int resId) {
727 final Bitmap cachedIcon = sNotificationIcons.get(resId);
728 if (cachedIcon != null) {
729 return cachedIcon;
730 }
731
732 final Bitmap icon = BitmapFactory.decodeResource(context.getResources(), resId);
733 sNotificationIcons.put(resId, icon);
734
735 return icon;
736 }
737
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800738 private static void configureLatestEventInfoFromConversation(final Context context,
739 final Account account, final FolderPreferences folderPreferences,
740 final NotificationCompat.Builder notification, final Cursor conversationCursor,
741 final PendingIntent clickIntent, final Intent notificationIntent,
742 final String notificationAccount, final int unreadCount, final int unseenCount,
743 final Folder folder, final long when) {
744 final Resources res = context.getResources();
745
Scott Kennedy32fc8e42013-07-30 11:50:42 -0700746 LogUtils.i(LOG_TAG, "Showing notification with unreadCount of %d and unseenCount of %d",
Scott Kennedy2f97af92013-07-30 10:42:34 -0700747 unreadCount, unseenCount);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800748
749 String notificationTicker = null;
750
751 // Boolean indicating that this notification is for a non-inbox label.
Scott Kennedy259df5b2013-07-11 13:24:01 -0700752 final boolean isInbox = folder.folderUri.fullUri.equals(account.settings.defaultInbox);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800753
754 // Notification label name for user label notifications.
755 final String notificationLabelName = isInbox ? null : folder.name;
756
757 if (unseenCount > 1) {
758 // Build the string that describes the number of new messages
759 final String newMessagesString = res.getString(R.string.new_messages, unseenCount);
760
761 // Use the default notification icon
762 notification.setLargeIcon(
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800763 getDefaultNotificationIcon(context, folder, true /* multiple new messages */));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800764
765 // The ticker initially start as the new messages string.
766 notificationTicker = newMessagesString;
767
768 // The title of the notification is the new messages string
769 notification.setContentTitle(newMessagesString);
770
Scott Kennedyc8bc5d12013-04-25 08:46:06 -0700771 // TODO(skennedy) Can we remove this check?
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800772 if (com.android.mail.utils.Utils.isRunningJellybeanOrLater()) {
773 // For a new-style notification
774 final int maxNumDigestItems = context.getResources().getInteger(
775 R.integer.max_num_notification_digest_items);
776
777 // The body of the notification is the account name, or the label name.
778 notification.setSubText(isInbox ? notificationAccount : notificationLabelName);
779
780 final NotificationCompat.InboxStyle digest =
781 new NotificationCompat.InboxStyle(notification);
782
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800783 int numDigestItems = 0;
784 do {
785 final Conversation conversation = new Conversation(conversationCursor);
786
787 if (!conversation.read) {
788 boolean multipleUnreadThread = false;
789 // TODO(cwren) extract this pattern into a helper
790
791 Cursor cursor = null;
792 MessageCursor messageCursor = null;
793 try {
794 final Uri.Builder uriBuilder = conversation.messageListUri.buildUpon();
795 uriBuilder.appendQueryParameter(
796 UIProvider.LABEL_QUERY_PARAMETER, notificationLabelName);
797 cursor = context.getContentResolver().query(uriBuilder.build(),
798 UIProvider.MESSAGE_PROJECTION, null, null, null);
799 messageCursor = new MessageCursor(cursor);
800
Yu Ping Hub18d75a2013-03-12 17:04:58 -0700801 String from = "";
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800802 String fromAddress = "";
803 if (messageCursor.moveToPosition(messageCursor.getCount() - 1)) {
804 final Message message = messageCursor.getMessage();
805 fromAddress = message.getFrom();
Yu Ping Hub18d75a2013-03-12 17:04:58 -0700806 if (fromAddress == null) {
807 fromAddress = "";
808 }
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800809 from = getDisplayableSender(fromAddress);
810 }
811 while (messageCursor.moveToPosition(messageCursor.getPosition() - 1)) {
812 final Message message = messageCursor.getMessage();
813 if (!message.read &&
814 !fromAddress.contentEquals(message.getFrom())) {
815 multipleUnreadThread = true;
816 break;
817 }
818 }
819 final SpannableStringBuilder sendersBuilder;
820 if (multipleUnreadThread) {
821 final int sendersLength =
822 res.getInteger(R.integer.swipe_senders_length);
823
824 sendersBuilder = getStyledSenders(context, conversationCursor,
825 sendersLength, notificationAccount);
826 } else {
Paul Westbrook12fb02b2013-08-27 14:34:49 -0700827 sendersBuilder =
828 new SpannableStringBuilder(getWrappedFromString(from));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800829 }
830 final CharSequence digestLine = getSingleMessageInboxLine(context,
831 sendersBuilder.toString(),
832 conversation.subject,
833 conversation.snippet);
834 digest.addLine(digestLine);
835 numDigestItems++;
836 } finally {
837 if (messageCursor != null) {
838 messageCursor.close();
839 }
840 if (cursor != null) {
841 cursor.close();
842 }
843 }
844 }
845 } while (numDigestItems <= maxNumDigestItems && conversationCursor.moveToNext());
846 } else {
847 // The body of the notification is the account name, or the label name.
848 notification.setContentText(
849 isInbox ? notificationAccount : notificationLabelName);
850 }
851 } else {
852 // For notifications for a single new conversation, we want to get the information from
853 // the conversation
854
855 // Move the cursor to the most recent unread conversation
856 seekToLatestUnreadConversation(conversationCursor);
857
858 final Conversation conversation = new Conversation(conversationCursor);
859
860 Cursor cursor = null;
861 MessageCursor messageCursor = null;
862 boolean multipleUnseenThread = false;
863 String from = null;
864 try {
Scott Kennedyffbf86f2013-03-08 11:48:15 -0800865 final Uri uri = conversation.messageListUri.buildUpon().appendQueryParameter(
866 UIProvider.LABEL_QUERY_PARAMETER, folder.persistentId).build();
867 cursor = context.getContentResolver().query(uri, UIProvider.MESSAGE_PROJECTION,
868 null, null, null);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800869 messageCursor = new MessageCursor(cursor);
870 // Use the information from the last sender in the conversation that triggered
871 // this notification.
872
873 String fromAddress = "";
874 if (messageCursor.moveToPosition(messageCursor.getCount() - 1)) {
875 final Message message = messageCursor.getMessage();
876 fromAddress = message.getFrom();
877 from = getDisplayableSender(fromAddress);
878 notification.setLargeIcon(
Scott Kennedyc94c80c2013-06-27 14:42:41 -0700879 getContactIcon(context, from, getSenderAddress(fromAddress), folder));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800880 }
881
882 // Assume that the last message in this conversation is unread
883 int firstUnseenMessagePos = messageCursor.getPosition();
884 while (messageCursor.moveToPosition(messageCursor.getPosition() - 1)) {
885 final Message message = messageCursor.getMessage();
886 final boolean unseen = !message.seen;
887 if (unseen) {
888 firstUnseenMessagePos = messageCursor.getPosition();
889 if (!multipleUnseenThread
890 && !fromAddress.contentEquals(message.getFrom())) {
891 multipleUnseenThread = true;
892 }
893 }
894 }
895
Scott Kennedyc8bc5d12013-04-25 08:46:06 -0700896 // TODO(skennedy) Can we remove this check?
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800897 if (Utils.isRunningJellybeanOrLater()) {
898 // For a new-style notification
899
900 if (multipleUnseenThread) {
901 // The title of a single conversation is the list of senders.
902 int sendersLength = res.getInteger(R.integer.swipe_senders_length);
903
904 final SpannableStringBuilder sendersBuilder = getStyledSenders(
905 context, conversationCursor, sendersLength, notificationAccount);
906
907 notification.setContentTitle(sendersBuilder);
908 // For a single new conversation, the ticker is based on the sender's name.
909 notificationTicker = sendersBuilder.toString();
910 } else {
Paul Westbrook12fb02b2013-08-27 14:34:49 -0700911 from = getWrappedFromString(from);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800912 // The title of a single message the sender.
913 notification.setContentTitle(from);
914 // For a single new conversation, the ticker is based on the sender's name.
915 notificationTicker = from;
916 }
917
918 // The notification content will be the subject of the conversation.
919 notification.setContentText(
920 getSingleMessageLittleText(context, conversation.subject));
921
922 // The notification subtext will be the subject of the conversation for inbox
923 // notifications, or will based on the the label name for user label
924 // notifications.
925 notification.setSubText(isInbox ? notificationAccount : notificationLabelName);
926
927 if (multipleUnseenThread) {
Scott Kennedy61bd0e82012-12-10 18:18:17 -0800928 notification.setLargeIcon(
929 getDefaultNotificationIcon(context, folder, true));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800930 }
931 final NotificationCompat.BigTextStyle bigText =
932 new NotificationCompat.BigTextStyle(notification);
933
934 // Seek the message cursor to the first unread message
Paul Westbrook92b21742013-03-11 17:36:40 -0700935 final Message message;
936 if (messageCursor.moveToPosition(firstUnseenMessagePos)) {
937 message = messageCursor.getMessage();
938 bigText.bigText(getSingleMessageBigText(context,
939 conversation.subject, message));
940 } else {
Scott Kennedy2f97af92013-07-30 10:42:34 -0700941 LogUtils.e(LOG_TAG, "Failed to load message");
Paul Westbrook92b21742013-03-11 17:36:40 -0700942 message = null;
943 }
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800944
Paul Westbrook92b21742013-03-11 17:36:40 -0700945 if (message != null) {
946 final Set<String> notificationActions =
Scott Kennedycde6eb02013-03-21 18:49:32 -0700947 folderPreferences.getNotificationActions(account);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800948
Paul Westbrook92b21742013-03-11 17:36:40 -0700949 final int notificationId = getNotificationId(notificationAccount, folder);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800950
Paul Westbrook92b21742013-03-11 17:36:40 -0700951 NotificationActionUtils.addNotificationActions(context, notificationIntent,
952 notification, account, conversation, message, folder,
953 notificationId, when, notificationActions);
954 }
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800955 } else {
956 // For an old-style notification
957
958 // The title of a single conversation notification is built from both the sender
959 // and subject of the new message.
960 notification.setContentTitle(getSingleMessageNotificationTitle(context,
961 from, conversation.subject));
962
963 // The notification content will be the subject of the conversation for inbox
964 // notifications, or will based on the the label name for user label
965 // notifications.
966 notification.setContentText(
967 isInbox ? notificationAccount : notificationLabelName);
968
969 // For a single new conversation, the ticker is based on the sender's name.
970 notificationTicker = from;
971 }
972 } finally {
973 if (messageCursor != null) {
974 messageCursor.close();
975 }
976 if (cursor != null) {
977 cursor.close();
978 }
979 }
980 }
981
982 // Build the notification ticker
983 if (notificationLabelName != null && notificationTicker != null) {
984 // This is a per label notification, format the ticker with that information
985 notificationTicker = res.getString(R.string.label_notification_ticker,
986 notificationLabelName, notificationTicker);
987 }
988
989 if (notificationTicker != null) {
990 // If we didn't generate a notification ticker, it will default to account name
991 notification.setTicker(notificationTicker);
992 }
993
994 // Set the number in the notification
995 if (unreadCount > 1) {
996 notification.setNumber(unreadCount);
997 }
998
999 notification.setContentIntent(clickIntent);
1000 }
1001
Paul Westbrook12fb02b2013-08-27 14:34:49 -07001002 private static String getWrappedFromString(String from) {
1003 if (from == null) {
1004 LogUtils.e(LOG_TAG, "null from string in getWrappedFromString");
1005 from = "";
1006 }
1007 from = BIDI_FORMATTER.unicodeWrap(from);
1008 return from;
1009 }
1010
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001011 private static SpannableStringBuilder getStyledSenders(final Context context,
1012 final Cursor conversationCursor, final int maxLength, final String account) {
1013 final Conversation conversation = new Conversation(conversationCursor);
1014 final com.android.mail.providers.ConversationInfo conversationInfo =
1015 conversation.conversationInfo;
1016 final ArrayList<SpannableString> senders = new ArrayList<SpannableString>();
1017 if (sNotificationUnreadStyleSpan == null) {
1018 sNotificationUnreadStyleSpan = new TextAppearanceSpan(
1019 context, R.style.NotificationSendersUnreadTextAppearance);
1020 sNotificationReadStyleSpan =
1021 new TextAppearanceSpan(context, R.style.NotificationSendersReadTextAppearance);
1022 }
1023 SendersView.format(context, conversationInfo, "", maxLength, senders, null, null, account,
1024 sNotificationUnreadStyleSpan, sNotificationReadStyleSpan, false);
1025
1026 return ellipsizeStyledSenders(context, senders);
1027 }
1028
1029 private static String sSendersSplitToken = null;
1030 private static String sElidedPaddingToken = null;
1031
1032 private static SpannableStringBuilder ellipsizeStyledSenders(final Context context,
1033 ArrayList<SpannableString> styledSenders) {
1034 if (sSendersSplitToken == null) {
1035 sSendersSplitToken = context.getString(R.string.senders_split_token);
1036 sElidedPaddingToken = context.getString(R.string.elided_padding_token);
1037 }
1038
1039 SpannableStringBuilder builder = new SpannableStringBuilder();
1040 SpannableString prevSender = null;
1041 for (SpannableString sender : styledSenders) {
1042 if (sender == null) {
Scott Kennedy2f97af92013-07-30 10:42:34 -07001043 LogUtils.e(LOG_TAG, "null sender iterating over styledSenders");
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001044 continue;
1045 }
1046 CharacterStyle[] spans = sender.getSpans(0, sender.length(), CharacterStyle.class);
1047 if (SendersView.sElidedString.equals(sender.toString())) {
1048 prevSender = sender;
1049 sender = copyStyles(spans, sElidedPaddingToken + sender + sElidedPaddingToken);
1050 } else if (builder.length() > 0
1051 && (prevSender == null || !SendersView.sElidedString.equals(prevSender
1052 .toString()))) {
1053 prevSender = sender;
1054 sender = copyStyles(spans, sSendersSplitToken + sender);
1055 } else {
1056 prevSender = sender;
1057 }
1058 builder.append(sender);
1059 }
1060 return builder;
1061 }
1062
1063 private static SpannableString copyStyles(CharacterStyle[] spans, CharSequence newText) {
1064 SpannableString s = new SpannableString(newText);
1065 if (spans != null && spans.length > 0) {
1066 s.setSpan(spans[0], 0, s.length(), 0);
1067 }
1068 return s;
1069 }
1070
1071 /**
1072 * Seeks the cursor to the position of the most recent unread conversation. If no unread
1073 * conversation is found, the position of the cursor will be restored, and false will be
1074 * returned.
1075 */
1076 private static boolean seekToLatestUnreadConversation(final Cursor cursor) {
1077 final int initialPosition = cursor.getPosition();
1078 do {
1079 final Conversation conversation = new Conversation(cursor);
1080 if (!conversation.read) {
1081 return true;
1082 }
1083 } while (cursor.moveToNext());
1084
1085 // Didn't find an unread conversation, reset the position.
1086 cursor.moveToPosition(initialPosition);
1087 return false;
1088 }
1089
1090 /**
1091 * Sets the bigtext for a notification for a single new conversation
1092 *
1093 * @param context
1094 * @param senders Sender of the new message that triggered the notification.
1095 * @param subject Subject of the new message that triggered the notification
1096 * @param snippet Snippet of the new message that triggered the notification
1097 * @return a {@link CharSequence} suitable for use in
1098 * {@link android.support.v4.app.NotificationCompat.BigTextStyle}
1099 */
1100 private static CharSequence getSingleMessageInboxLine(Context context,
1101 String senders, String subject, String snippet) {
1102 // TODO(cwren) finish this step toward commmon code with getSingleMessageBigText
1103
1104 final String subjectSnippet = !TextUtils.isEmpty(subject) ? subject : snippet;
1105
1106 final TextAppearanceSpan notificationPrimarySpan =
1107 new TextAppearanceSpan(context, R.style.NotificationPrimaryText);
1108
1109 if (TextUtils.isEmpty(senders)) {
1110 // If the senders are empty, just use the subject/snippet.
1111 return subjectSnippet;
1112 } else if (TextUtils.isEmpty(subjectSnippet)) {
1113 // If the subject/snippet is empty, just use the senders.
1114 final SpannableString spannableString = new SpannableString(senders);
1115 spannableString.setSpan(notificationPrimarySpan, 0, senders.length(), 0);
1116
1117 return spannableString;
1118 } else {
1119 final String formatString = context.getResources().getString(
1120 R.string.multiple_new_message_notification_item);
1121 final TextAppearanceSpan notificationSecondarySpan =
1122 new TextAppearanceSpan(context, R.style.NotificationSecondaryText);
1123
Andrew Sapperstein8173cf42013-07-01 13:17:18 -07001124 // senders is already individually unicode wrapped so it does not need to be done here
Andrew Sappersteinf5810962013-06-27 10:41:33 -07001125 final String instantiatedString = String.format(formatString,
Andrew Sapperstein8173cf42013-07-01 13:17:18 -07001126 senders,
Paul Westbrook12fb02b2013-08-27 14:34:49 -07001127 BIDI_FORMATTER.unicodeWrap(subjectSnippet));
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001128
1129 final SpannableString spannableString = new SpannableString(instantiatedString);
1130
1131 final boolean isOrderReversed = formatString.indexOf("%2$s") <
1132 formatString.indexOf("%1$s");
1133 final int primaryOffset =
1134 (isOrderReversed ? instantiatedString.lastIndexOf(senders) :
1135 instantiatedString.indexOf(senders));
1136 final int secondaryOffset =
1137 (isOrderReversed ? instantiatedString.lastIndexOf(subjectSnippet) :
1138 instantiatedString.indexOf(subjectSnippet));
1139 spannableString.setSpan(notificationPrimarySpan,
1140 primaryOffset, primaryOffset + senders.length(), 0);
1141 spannableString.setSpan(notificationSecondarySpan,
1142 secondaryOffset, secondaryOffset + subjectSnippet.length(), 0);
1143 return spannableString;
1144 }
1145 }
1146
1147 /**
1148 * Sets the bigtext for a notification for a single new conversation
1149 * @param context
1150 * @param subject Subject of the new message that triggered the notification
Andrew Sappersteinf5810962013-06-27 10:41:33 -07001151 * @return a {@link CharSequence} suitable for use in
1152 * {@link NotificationCompat.Builder#setContentText}
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001153 */
1154 private static CharSequence getSingleMessageLittleText(Context context, String subject) {
1155 final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
1156 context, R.style.NotificationPrimaryText);
1157
1158 final SpannableString spannableString = new SpannableString(subject);
1159 spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0);
1160
1161 return spannableString;
1162 }
1163
1164 /**
1165 * Sets the bigtext for a notification for a single new conversation
1166 *
1167 * @param context
1168 * @param subject Subject of the new message that triggered the notification
1169 * @param message the {@link Message} to be displayed.
1170 * @return a {@link CharSequence} suitable for use in
1171 * {@link android.support.v4.app.NotificationCompat.BigTextStyle}
1172 */
1173 private static CharSequence getSingleMessageBigText(Context context, String subject,
1174 final Message message) {
1175
1176 final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
1177 context, R.style.NotificationPrimaryText);
1178
1179 final String snippet = getMessageBodyWithoutElidedText(message);
1180
1181 // Change multiple newlines (with potential white space between), into a single new line
1182 final String collapsedSnippet =
1183 !TextUtils.isEmpty(snippet) ? snippet.replaceAll("\\n\\s+", "\n") : "";
1184
1185 if (TextUtils.isEmpty(subject)) {
1186 // If the subject is empty, just use the snippet.
1187 return snippet;
1188 } else if (TextUtils.isEmpty(collapsedSnippet)) {
1189 // If the snippet is empty, just use the subject.
1190 final SpannableString spannableString = new SpannableString(subject);
1191 spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0);
1192
1193 return spannableString;
1194 } else {
1195 final String notificationBigTextFormat = context.getResources().getString(
1196 R.string.single_new_message_notification_big_text);
1197
1198 // Localizers may change the order of the parameters, look at how the format
1199 // string is structured.
1200 final boolean isSubjectFirst = notificationBigTextFormat.indexOf("%2$s") >
1201 notificationBigTextFormat.indexOf("%1$s");
1202 final String bigText =
1203 String.format(notificationBigTextFormat, subject, collapsedSnippet);
1204 final SpannableString spannableString = new SpannableString(bigText);
1205
1206 final int subjectOffset =
1207 (isSubjectFirst ? bigText.indexOf(subject) : bigText.lastIndexOf(subject));
1208 spannableString.setSpan(notificationSubjectSpan,
1209 subjectOffset, subjectOffset + subject.length(), 0);
1210
1211 return spannableString;
1212 }
1213 }
1214
1215 /**
1216 * Gets the title for a notification for a single new conversation
1217 * @param context
1218 * @param sender Sender of the new message that triggered the notification.
1219 * @param subject Subject of the new message that triggered the notification
1220 * @return a {@link CharSequence} suitable for use as a {@link Notification} title.
1221 */
1222 private static CharSequence getSingleMessageNotificationTitle(Context context,
1223 String sender, String subject) {
1224
1225 if (TextUtils.isEmpty(subject)) {
1226 // If the subject is empty, just set the title to the sender's information.
1227 return sender;
1228 } else {
1229 final String notificationTitleFormat = context.getResources().getString(
1230 R.string.single_new_message_notification_title);
1231
1232 // Localizers may change the order of the parameters, look at how the format
1233 // string is structured.
1234 final boolean isSubjectLast = notificationTitleFormat.indexOf("%2$s") >
1235 notificationTitleFormat.indexOf("%1$s");
1236 final String titleString = String.format(notificationTitleFormat, sender, subject);
1237
1238 // Format the string so the subject is using the secondaryText style
1239 final SpannableString titleSpannable = new SpannableString(titleString);
1240
1241 // Find the offset of the subject.
1242 final int subjectOffset =
1243 isSubjectLast ? titleString.lastIndexOf(subject) : titleString.indexOf(subject);
1244 final TextAppearanceSpan notificationSubjectSpan =
1245 new TextAppearanceSpan(context, R.style.NotificationSecondaryText);
1246 titleSpannable.setSpan(notificationSubjectSpan,
1247 subjectOffset, subjectOffset + subject.length(), 0);
1248 return titleSpannable;
1249 }
1250 }
1251
1252 /**
Scott Kennedy7f8aed62013-05-22 18:35:17 -07001253 * Clears the notifications for the specified account/folder.
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001254 */
Scott Kennedy7f8aed62013-05-22 18:35:17 -07001255 public static void clearFolderNotification(Context context, Account account, Folder folder,
1256 final boolean markSeen) {
Scott Kennedy2f97af92013-07-30 10:42:34 -07001257 LogUtils.v(LOG_TAG, "Clearing all notifications for %s/%s", account.name, folder.name);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001258 final NotificationMap notificationMap = getNotificationMap(context);
1259 final NotificationKey key = new NotificationKey(account, folder);
1260 notificationMap.remove(key);
1261 notificationMap.saveNotificationMap(context);
1262
Scott Kennedy7f8aed62013-05-22 18:35:17 -07001263 final NotificationManager notificationManager =
1264 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
1265 notificationManager.cancel(getNotificationId(account.name, folder));
1266
1267 if (markSeen) {
1268 markSeen(context, folder);
1269 }
1270 }
1271
1272 /**
1273 * Clears all notifications for the specified account.
1274 */
1275 public static void clearAccountNotifications(final Context context, final String account) {
Scott Kennedy2f97af92013-07-30 10:42:34 -07001276 LogUtils.v(LOG_TAG, "Clearing all notifications for %s", account);
Scott Kennedy7f8aed62013-05-22 18:35:17 -07001277 final NotificationMap notificationMap = getNotificationMap(context);
1278
1279 // Find all NotificationKeys for this account
1280 final ImmutableList.Builder<NotificationKey> keyBuilder = ImmutableList.builder();
1281
1282 for (final NotificationKey key : notificationMap.keySet()) {
1283 if (account.equals(key.account.name)) {
1284 keyBuilder.add(key);
1285 }
1286 }
1287
1288 final List<NotificationKey> notificationKeys = keyBuilder.build();
1289
1290 final NotificationManager notificationManager =
1291 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
1292
1293 for (final NotificationKey notificationKey : notificationKeys) {
1294 final Folder folder = notificationKey.folder;
1295 notificationManager.cancel(getNotificationId(account, folder));
1296 notificationMap.remove(notificationKey);
1297 }
1298
1299 notificationMap.saveNotificationMap(context);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001300 }
1301
1302 private static ArrayList<Long> findContacts(Context context, Collection<String> addresses) {
1303 ArrayList<String> whereArgs = new ArrayList<String>();
1304 StringBuilder whereBuilder = new StringBuilder();
1305 String[] questionMarks = new String[addresses.size()];
1306
1307 whereArgs.addAll(addresses);
1308 Arrays.fill(questionMarks, "?");
1309 whereBuilder.append(Email.DATA1 + " IN (").
1310 append(TextUtils.join(",", questionMarks)).
1311 append(")");
1312
1313 ContentResolver resolver = context.getContentResolver();
1314 Cursor c = resolver.query(Email.CONTENT_URI,
1315 new String[]{Email.CONTACT_ID}, whereBuilder.toString(),
1316 whereArgs.toArray(new String[0]), null);
1317
1318 ArrayList<Long> contactIds = new ArrayList<Long>();
1319 if (c == null) {
1320 return contactIds;
1321 }
1322 try {
1323 while (c.moveToNext()) {
1324 contactIds.add(c.getLong(0));
1325 }
1326 } finally {
1327 c.close();
1328 }
1329 return contactIds;
1330 }
1331
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001332 private static Bitmap getContactIcon(final Context context, final String displayName,
1333 final String senderAddress, final Folder folder) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001334 if (senderAddress == null) {
1335 return null;
1336 }
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001337
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001338 Bitmap icon = null;
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001339
1340 final List<Long> contactIds = findContacts( context, Arrays.asList(
1341 new String[] { senderAddress }));
1342
1343 // Get the ideal size for this icon.
1344 final Resources res = context.getResources();
1345 final int idealIconHeight =
1346 res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
1347 final int idealIconWidth =
1348 res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001349
1350 if (contactIds != null) {
Scott Kennedy4046d062013-06-27 14:46:09 -07001351 for (final long id : contactIds) {
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001352 final Uri contactUri =
1353 ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id);
1354 final Uri photoUri = Uri.withAppendedPath(contactUri, Photo.CONTENT_DIRECTORY);
1355 final Cursor cursor = context.getContentResolver().query(
1356 photoUri, new String[] { Photo.PHOTO }, null, null, null);
1357
1358 if (cursor != null) {
1359 try {
1360 if (cursor.moveToFirst()) {
Scott Kennedy4046d062013-06-27 14:46:09 -07001361 final byte[] data = cursor.getBlob(0);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001362 if (data != null) {
1363 icon = BitmapFactory.decodeStream(new ByteArrayInputStream(data));
1364 if (icon != null && icon.getHeight() < idealIconHeight) {
1365 // We should scale this image to fit the intended size
1366 icon = Bitmap.createScaledBitmap(
1367 icon, idealIconWidth, idealIconHeight, true);
1368 }
1369 if (icon != null) {
1370 break;
1371 }
1372 }
1373 }
1374 } finally {
1375 cursor.close();
1376 }
1377 }
1378 }
1379 }
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001380
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001381 if (icon == null) {
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001382 // Make a colorful tile!
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001383 final Dimensions dimensions = new Dimensions(idealIconWidth, idealIconHeight,
1384 Dimensions.SCALE_ONE);
1385
Scott Kennedyfe235122013-06-28 12:06:36 -07001386 icon = new LetterTileProvider(context).getLetterTile(dimensions,
Scott Kennedyc94c80c2013-06-27 14:42:41 -07001387 displayName, senderAddress);
1388 }
1389
1390 if (icon == null) {
1391 // Icon should be the default mail icon.
Scott Kennedy61bd0e82012-12-10 18:18:17 -08001392 icon = getDefaultNotificationIcon(context, folder, false /* single new message */);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001393 }
1394 return icon;
1395 }
1396
1397 private static String getMessageBodyWithoutElidedText(final Message message) {
Scott Kennedy5e7e88b2013-03-07 16:42:12 -08001398 return getMessageBodyWithoutElidedText(message.getBodyAsHtml());
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001399 }
1400
1401 public static String getMessageBodyWithoutElidedText(String html) {
1402 if (TextUtils.isEmpty(html)) {
1403 return "";
1404 }
1405 // Get the html "tree" for this message body
1406 final HtmlTree htmlTree = com.android.mail.utils.Utils.getHtmlTree(html);
1407 htmlTree.setPlainTextConverterFactory(MESSAGE_CONVERTER_FACTORY);
1408
1409 return htmlTree.getPlainText();
1410 }
1411
1412 public static void markSeen(final Context context, final Folder folder) {
Scott Kennedy259df5b2013-07-11 13:24:01 -07001413 final Uri uri = folder.folderUri.fullUri;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001414
1415 final ContentValues values = new ContentValues(1);
1416 values.put(UIProvider.ConversationColumns.SEEN, 1);
1417
1418 context.getContentResolver().update(uri, values, null, null);
1419 }
1420
1421 /**
1422 * Returns a displayable string representing
1423 * the message sender. It has a preference toward showing the name,
1424 * but will fall back to the address if that is all that is available.
1425 */
1426 private static String getDisplayableSender(String sender) {
1427 final EmailAddress address = EmailAddress.getEmailAddress(sender);
1428
1429 String displayableSender = address.getName();
Scott Kennedy390cab92013-05-23 14:29:49 -07001430
1431 if (!TextUtils.isEmpty(displayableSender)) {
1432 return Address.decodeAddressName(displayableSender);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001433 }
Scott Kennedy390cab92013-05-23 14:29:49 -07001434
1435 // If that fails, default to the sender address.
1436 displayableSender = address.getAddress();
1437
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001438 // If we were unable to tokenize a name or address,
1439 // just use whatever was in the sender.
1440 if (TextUtils.isEmpty(displayableSender)) {
1441 displayableSender = sender;
1442 }
1443 return displayableSender;
1444 }
1445
1446 /**
1447 * Returns only the address portion of a message sender.
1448 */
1449 private static String getSenderAddress(String sender) {
1450 final EmailAddress address = EmailAddress.getEmailAddress(sender);
1451
1452 String tokenizedAddress = address.getAddress();
1453
1454 // If we were unable to tokenize a name or address,
1455 // just use whatever was in the sender.
1456 if (TextUtils.isEmpty(tokenizedAddress)) {
1457 tokenizedAddress = sender;
1458 }
1459 return tokenizedAddress;
1460 }
1461
1462 public static int getNotificationId(final String account, final Folder folder) {
1463 return 1 ^ account.hashCode() ^ folder.hashCode();
1464 }
1465
1466 private static class NotificationKey {
1467 public final Account account;
1468 public final Folder folder;
1469
1470 public NotificationKey(Account account, Folder folder) {
1471 this.account = account;
1472 this.folder = folder;
1473 }
1474
1475 @Override
1476 public boolean equals(Object other) {
1477 if (!(other instanceof NotificationKey)) {
1478 return false;
1479 }
1480 NotificationKey key = (NotificationKey) other;
1481 return account.equals(key.account) && folder.equals(key.folder);
1482 }
1483
1484 @Override
1485 public String toString() {
Scott Kennedye8a7c4c2013-05-09 11:56:50 -07001486 return account.name + " " + folder.name;
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001487 }
1488
1489 @Override
1490 public int hashCode() {
1491 final int accountHashCode = account.hashCode();
1492 final int folderHashCode = folder.hashCode();
1493 return accountHashCode ^ folderHashCode;
1494 }
1495 }
1496
1497 /**
1498 * Contains the logic for converting the contents of one HtmlTree into
1499 * plaintext.
1500 */
1501 public static class MailMessagePlainTextConverter extends HtmlTree.DefaultPlainTextConverter {
1502 // Strings for parsing html message bodies
1503 private static final String ELIDED_TEXT_ELEMENT_NAME = "div";
1504 private static final String ELIDED_TEXT_ELEMENT_ATTRIBUTE_NAME = "class";
1505 private static final String ELIDED_TEXT_ELEMENT_ATTRIBUTE_CLASS_VALUE = "elided-text";
1506
1507 private static final HTML.Attribute ELIDED_TEXT_ATTRIBUTE =
1508 new HTML.Attribute(ELIDED_TEXT_ELEMENT_ATTRIBUTE_NAME, HTML.Attribute.NO_TYPE);
1509
1510 private static final HtmlDocument.Node ELIDED_TEXT_REPLACEMENT_NODE =
1511 HtmlDocument.createSelfTerminatingTag(HTML4.BR_ELEMENT, null, null, null);
1512
1513 private int mEndNodeElidedTextBlock = -1;
1514
1515 @Override
1516 public void addNode(HtmlDocument.Node n, int nodeNum, int endNum) {
1517 // If we are in the middle of an elided text block, don't add this node
1518 if (nodeNum < mEndNodeElidedTextBlock) {
1519 return;
1520 } else if (nodeNum == mEndNodeElidedTextBlock) {
1521 super.addNode(ELIDED_TEXT_REPLACEMENT_NODE, nodeNum, endNum);
1522 return;
1523 }
1524
1525 // If this tag starts another elided text block, we want to remember the end
1526 if (n instanceof HtmlDocument.Tag) {
1527 boolean foundElidedTextTag = false;
1528 final HtmlDocument.Tag htmlTag = (HtmlDocument.Tag)n;
1529 final HTML.Element htmlElement = htmlTag.getElement();
1530 if (ELIDED_TEXT_ELEMENT_NAME.equals(htmlElement.getName())) {
1531 // Make sure that the class is what is expected
1532 final List<HtmlDocument.TagAttribute> attributes =
1533 htmlTag.getAttributes(ELIDED_TEXT_ATTRIBUTE);
1534 for (HtmlDocument.TagAttribute attribute : attributes) {
1535 if (ELIDED_TEXT_ELEMENT_ATTRIBUTE_CLASS_VALUE.equals(
1536 attribute.getValue())) {
1537 // Found an "elided-text" div. Remember information about this tag
1538 mEndNodeElidedTextBlock = endNum;
1539 foundElidedTextTag = true;
1540 break;
1541 }
1542 }
1543 }
1544
1545 if (foundElidedTextTag) {
1546 return;
1547 }
1548 }
1549
Scott Kennedyc56b2332013-05-23 18:24:53 -07001550 super.addNode(n, nodeNum, endNum);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -08001551 }
1552 }
1553
1554 /**
1555 * During account setup in Email, we may not have an inbox yet, so the notification setting had
1556 * to be stored in {@link AccountPreferences}. If it is still there, we need to move it to the
1557 * {@link FolderPreferences} now.
1558 */
1559 public static void moveNotificationSetting(final AccountPreferences accountPreferences,
1560 final FolderPreferences folderPreferences) {
1561 if (accountPreferences.isDefaultInboxNotificationsEnabledSet()) {
1562 // If this setting has been changed some other way, don't overwrite it
1563 if (!folderPreferences.isNotificationsEnabledSet()) {
1564 final boolean notificationsEnabled =
1565 accountPreferences.getDefaultInboxNotificationsEnabled();
1566
1567 folderPreferences.setNotificationsEnabled(notificationsEnabled);
1568 }
1569
1570 accountPreferences.clearDefaultInboxNotificationsEnabled();
1571 }
1572 }
1573}