blob: eaf0213ad94c5e34d833d20d3c61fddcce48edb7 [file] [log] [blame]
Makoto Onuki899c5b82010-09-26 16:16:21 -07001/*
2 * Copyright (C) 2010 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 */
16
17package com.android.email;
18
Makoto Onuki899c5b82010-09-26 16:16:21 -070019import android.app.Notification;
Todd Kennedydfdc8b62011-05-11 13:40:52 -070020import android.app.Notification.Builder;
Makoto Onuki899c5b82010-09-26 16:16:21 -070021import android.app.NotificationManager;
22import android.app.PendingIntent;
Todd Kennedyc4cdb112011-05-03 14:42:26 -070023import android.content.ContentResolver;
Marc Blank6e418aa2011-06-18 18:03:11 -070024import android.content.ContentUris;
Marc Blankaca94262011-07-19 18:19:59 -070025import android.content.ContentValues;
Makoto Onuki899c5b82010-09-26 16:16:21 -070026import android.content.Context;
Marc Blankd3e4f3c2010-10-18 13:14:20 -070027import android.content.Intent;
Todd Kennedyc4cdb112011-05-03 14:42:26 -070028import android.database.ContentObserver;
Todd Kennedy83693a62011-05-10 11:36:57 -070029import android.database.Cursor;
Makoto Onuki899c5b82010-09-26 16:16:21 -070030import android.graphics.Bitmap;
Makoto Onuki74e09482010-12-03 14:44:47 -080031import android.graphics.BitmapFactory;
Makoto Onuki899c5b82010-09-26 16:16:21 -070032import android.media.AudioManager;
33import android.net.Uri;
Todd Kennedyc4cdb112011-05-03 14:42:26 -070034import android.os.Handler;
Todd Kennedy83693a62011-05-10 11:36:57 -070035import android.os.Looper;
36import android.os.Process;
Makoto Onuki74e09482010-12-03 14:44:47 -080037import android.text.SpannableString;
Makoto Onuki899c5b82010-09-26 16:16:21 -070038import android.text.TextUtils;
Todd Kennedy83693a62011-05-10 11:36:57 -070039import android.util.Log;
Makoto Onuki899c5b82010-09-26 16:16:21 -070040
Marc Blank6e418aa2011-06-18 18:03:11 -070041import com.android.email.activity.ContactStatusLoader;
42import com.android.email.activity.Welcome;
43import com.android.email.activity.setup.AccountSecurity;
44import com.android.email.activity.setup.AccountSettings;
45import com.android.emailcommon.Logging;
46import com.android.emailcommon.mail.Address;
47import com.android.emailcommon.provider.Account;
48import com.android.emailcommon.provider.EmailContent;
Marc Blankaca94262011-07-19 18:19:59 -070049import com.android.emailcommon.provider.EmailContent.AccountColumns;
Marc Blank6e418aa2011-06-18 18:03:11 -070050import com.android.emailcommon.provider.EmailContent.Attachment;
51import com.android.emailcommon.provider.EmailContent.MailboxColumns;
52import com.android.emailcommon.provider.EmailContent.Message;
53import com.android.emailcommon.provider.EmailContent.MessageColumns;
54import com.android.emailcommon.provider.Mailbox;
Marc Blank2736c1a2011-10-20 10:13:02 -070055import com.android.emailcommon.utility.EmailAsyncTask;
Marc Blank6e418aa2011-06-18 18:03:11 -070056import com.android.emailcommon.utility.Utility;
57import com.google.common.annotations.VisibleForTesting;
58
Todd Kennedyc4cdb112011-05-03 14:42:26 -070059import java.util.HashMap;
Todd Kennedye7fb4ac2011-05-11 15:29:24 -070060import java.util.HashSet;
Todd Kennedyc4cdb112011-05-03 14:42:26 -070061
Makoto Onuki899c5b82010-09-26 16:16:21 -070062/**
63 * Class that manages notifications.
Makoto Onuki899c5b82010-09-26 16:16:21 -070064 */
65public class NotificationController {
Todd Kennedy958b15e2011-04-28 11:09:41 -070066 /** Reserved for {@link com.android.exchange.CalendarSyncEnabler} */
67 @SuppressWarnings("unused")
68 private static final int NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED = 2;
Makoto Onuki308ce922011-03-21 17:08:16 -070069 private static final int NOTIFICATION_ID_ATTACHMENT_WARNING = 3;
70 private static final int NOTIFICATION_ID_PASSWORD_EXPIRING = 4;
71 private static final int NOTIFICATION_ID_PASSWORD_EXPIRED = 5;
Marc Blankd3e4f3c2010-10-18 13:14:20 -070072
Marc Blank2736c1a2011-10-20 10:13:02 -070073 private static final int NOTIFICATION_ID_BASE_MASK = 0xF0000000;
Marc Blankd3e4f3c2010-10-18 13:14:20 -070074 private static final int NOTIFICATION_ID_BASE_NEW_MESSAGES = 0x10000000;
75 private static final int NOTIFICATION_ID_BASE_LOGIN_WARNING = 0x20000000;
Marc Blank2736c1a2011-10-20 10:13:02 -070076 private static final int NOTIFICATION_ID_BASE_SECURITY_NEEDED = 0x30000000;
77 private static final int NOTIFICATION_ID_BASE_SECURITY_CHANGED = 0x40000000;
Makoto Onuki899c5b82010-09-26 16:16:21 -070078
Todd Kennedy83693a62011-05-10 11:36:57 -070079 /** Selection to retrieve accounts that should we notify user for changes */
80 private final static String NOTIFIED_ACCOUNT_SELECTION =
81 Account.FLAGS + "&" + Account.FLAGS_NOTIFY_NEW_MAIL + " != 0";
Todd Kennedy83693a62011-05-10 11:36:57 -070082
Todd Kennedye7fb4ac2011-05-11 15:29:24 -070083 private static NotificationThread sNotificationThread;
84 private static Handler sNotificationHandler;
Makoto Onuki899c5b82010-09-26 16:16:21 -070085 private static NotificationController sInstance;
86 private final Context mContext;
87 private final NotificationManager mNotificationManager;
88 private final AudioManager mAudioManager;
Andy Stadlerc1c3b6f2010-12-15 15:26:30 -080089 private final Bitmap mGenericSenderIcon;
Ben Komalof13fee52011-08-17 17:10:06 -070090 private final Bitmap mGenericMultipleSenderIcon;
Makoto Onuki74e09482010-12-03 14:44:47 -080091 private final Clock mClock;
Todd Kennedy83693a62011-05-10 11:36:57 -070092 // TODO We're maintaining all of our structures based upon the account ID. This is fine
93 // for now since the assumption is that we only ever look for changes in an account's
94 // INBOX. We should adjust our logic to use the mailbox ID instead.
Todd Kennedyc4cdb112011-05-03 14:42:26 -070095 /** Maps account id to the message data */
Marc Blankaca94262011-07-19 18:19:59 -070096 private final HashMap<Long, ContentObserver> mNotificationMap;
Todd Kennedye7fb4ac2011-05-11 15:29:24 -070097 private ContentObserver mAccountObserver;
Todd Kennedy5701e0a2011-05-12 10:13:45 -070098 /**
Makoto Onukib36ac012011-05-17 10:50:30 -070099 * Suspend notifications for this account. If {@link Account#NO_ACCOUNT}, no
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700100 * account notifications are suspended. If {@link Account#ACCOUNT_ID_COMBINED_VIEW},
101 * notifications for all accounts are suspended.
102 */
Makoto Onukib36ac012011-05-17 10:50:30 -0700103 private long mSuspendAccountId = Account.NO_ACCOUNT;
Makoto Onuki899c5b82010-09-26 16:16:21 -0700104
Ben Komalo23a4b152011-07-22 11:41:51 -0700105 /**
106 * Timestamp indicating when the last message notification sound was played.
107 * Used for throttling.
108 */
109 private long mLastMessageNotifyTime;
110
111 /**
112 * Minimum interval between notification sounds.
113 * Since a long sync (either on account setup or after a long period of being offline) can cause
114 * several notifications consecutively, it can be pretty overwhelming to get a barrage of
115 * notification sounds. Throttle them using this value.
116 */
Ben Komalo48a3a1c2011-07-22 13:29:48 -0700117 private static final long MIN_SOUND_INTERVAL_MS = 15 * 1000; // 15 seconds
Ben Komalo23a4b152011-07-22 11:41:51 -0700118
Makoto Onuki899c5b82010-09-26 16:16:21 -0700119 /** Constructor */
Todd Kennedy958b15e2011-04-28 11:09:41 -0700120 @VisibleForTesting
121 NotificationController(Context context, Clock clock) {
Makoto Onuki899c5b82010-09-26 16:16:21 -0700122 mContext = context.getApplicationContext();
123 mNotificationManager = (NotificationManager) context.getSystemService(
124 Context.NOTIFICATION_SERVICE);
125 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
Andy Stadlerc1c3b6f2010-12-15 15:26:30 -0800126 mGenericSenderIcon = BitmapFactory.decodeResource(mContext.getResources(),
127 R.drawable.ic_contact_picture);
Ben Komalof13fee52011-08-17 17:10:06 -0700128 mGenericMultipleSenderIcon = BitmapFactory.decodeResource(mContext.getResources(),
129 R.drawable.ic_notification_multiple_mail_holo_dark);
Makoto Onuki74e09482010-12-03 14:44:47 -0800130 mClock = clock;
Marc Blankaca94262011-07-19 18:19:59 -0700131 mNotificationMap = new HashMap<Long, ContentObserver>();
Makoto Onuki899c5b82010-09-26 16:16:21 -0700132 }
133
134 /** Singleton access */
135 public static synchronized NotificationController getInstance(Context context) {
136 if (sInstance == null) {
Makoto Onuki74e09482010-12-03 14:44:47 -0800137 sInstance = new NotificationController(context, Clock.INSTANCE);
Makoto Onuki899c5b82010-09-26 16:16:21 -0700138 }
139 return sInstance;
140 }
141
142 /**
Marc Blankd9b2a8f2011-07-28 13:55:10 -0700143 * Return whether or not a notification, based on the passed-in id, needs to be "ongoing"
144 * @param notificationId the notification id to check
145 * @return whether or not the notification must be "ongoing"
146 */
147 private boolean needsOngoingNotification(int notificationId) {
148 // "Security needed" must be ongoing so that the user doesn't close it; otherwise, sync will
149 // be prevented until a reboot. Consider also doing this for password expired.
Marc Blank2736c1a2011-10-20 10:13:02 -0700150 return (notificationId & NOTIFICATION_ID_BASE_MASK) == NOTIFICATION_ID_BASE_SECURITY_NEEDED;
Marc Blankd9b2a8f2011-07-28 13:55:10 -0700151 }
152
153 /**
Todd Kennedy958b15e2011-04-28 11:09:41 -0700154 * Returns a {@link Notification} for an event with the given account. The account contains
155 * specific rules on ring tone usage and these will be used to modify the notification
156 * behaviour.
Andy Stadler1ca111c2010-12-01 12:58:36 -0800157 *
Todd Kennedy958b15e2011-04-28 11:09:41 -0700158 * @param account The account this notification is being built for.
159 * @param ticker Text displayed when the notification is first shown. May be {@code null}.
160 * @param title The first line of text. May NOT be {@code null}.
161 * @param contentText The second line of text. May NOT be {@code null}.
162 * @param intent The intent to start if the user clicks on the notification.
163 * @param largeIcon A large icon. May be {@code null}
Todd Kennedydfdc8b62011-05-11 13:40:52 -0700164 * @param number A number to display using {@link Builder#setNumber(int)}. May
Todd Kennedy958b15e2011-04-28 11:09:41 -0700165 * be {@code null}.
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700166 * @param enableAudio If {@code false}, do not play any sound. Otherwise, play sound according
167 * to the settings for the given account.
Todd Kennedy958b15e2011-04-28 11:09:41 -0700168 * @return A {@link Notification} that can be sent to the notification service.
Andy Stadler1ca111c2010-12-01 12:58:36 -0800169 */
Todd Kennedy958b15e2011-04-28 11:09:41 -0700170 private Notification createAccountNotification(Account account, String ticker,
171 CharSequence title, String contentText, Intent intent, Bitmap largeIcon,
Marc Blankd9b2a8f2011-07-28 13:55:10 -0700172 Integer number, boolean enableAudio, boolean ongoing) {
Andy Stadler1ca111c2010-12-01 12:58:36 -0800173 // Pending Intent
Andy Stadlerc6d344a2011-02-16 16:38:18 -0800174 PendingIntent pending = null;
175 if (intent != null) {
Todd Kennedy958b15e2011-04-28 11:09:41 -0700176 pending = PendingIntent.getActivity(
177 mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
Andy Stadlerc6d344a2011-02-16 16:38:18 -0800178 }
Andy Stadler1ca111c2010-12-01 12:58:36 -0800179
Todd Kennedy958b15e2011-04-28 11:09:41 -0700180 // NOTE: the ticker is not shown for notifications in the Holo UX
181 Notification.Builder builder = new Notification.Builder(mContext)
182 .setContentTitle(title)
183 .setContentText(contentText)
184 .setContentIntent(pending)
185 .setLargeIcon(largeIcon)
186 .setNumber(number == null ? 0 : number)
187 .setSmallIcon(R.drawable.stat_notify_email_generic)
188 .setWhen(mClock.getTime())
Marc Blankd9b2a8f2011-07-28 13:55:10 -0700189 .setTicker(ticker)
190 .setOngoing(ongoing);
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700191
192 if (enableAudio) {
193 setupSoundAndVibration(builder, account);
194 }
Andy Stadler1ca111c2010-12-01 12:58:36 -0800195
Todd Kennedy958b15e2011-04-28 11:09:41 -0700196 Notification notification = builder.getNotification();
197 return notification;
198 }
Andy Stadler1ca111c2010-12-01 12:58:36 -0800199
Todd Kennedy958b15e2011-04-28 11:09:41 -0700200 /**
201 * Generic notifier for any account. Uses notification rules from account.
202 *
203 * @param account The account this notification is being built for.
204 * @param ticker Text displayed when the notification is first shown. May be {@code null}.
205 * @param title The first line of text. May NOT be {@code null}.
206 * @param contentText The second line of text. May NOT be {@code null}.
207 * @param intent The intent to start if the user clicks on the notification.
208 * @param notificationId The ID of the notification to register with the service.
209 */
210 private void showAccountNotification(Account account, String ticker, String title,
211 String contentText, Intent intent, int notificationId) {
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700212 Notification notification = createAccountNotification(account, ticker, title, contentText,
Marc Blankd9b2a8f2011-07-28 13:55:10 -0700213 intent, null, null, true, needsOngoingNotification(notificationId));
Andy Stadler1ca111c2010-12-01 12:58:36 -0800214 mNotificationManager.notify(notificationId, notification);
215 }
216
217 /**
Todd Kennedy958b15e2011-04-28 11:09:41 -0700218 * Returns a notification ID for new message notifications for the given account.
Makoto Onuki899c5b82010-09-26 16:16:21 -0700219 */
220 private int getNewMessageNotificationId(long accountId) {
Todd Kennedy958b15e2011-04-28 11:09:41 -0700221 // We assume accountId will always be less than 0x0FFFFFFF; is there a better way?
Marc Blankd3e4f3c2010-10-18 13:14:20 -0700222 return (int) (NOTIFICATION_ID_BASE_NEW_MESSAGES + accountId);
Makoto Onuki899c5b82010-09-26 16:16:21 -0700223 }
224
225 /**
Todd Kennedy83693a62011-05-10 11:36:57 -0700226 * Tells the notification controller if it should be watching for changes to the message table.
227 * This is the main life cycle method for message notifications. When we stop observing
228 * database changes, we save the state [e.g. message ID and count] of the most recent
229 * notification shown to the user. And, when we start observing database changes, we restore
230 * the saved state.
231 * @param watch If {@code true}, we register observers for all accounts whose settings have
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700232 * notifications enabled. Otherwise, all observers are unregistered.
Makoto Onuki899c5b82010-09-26 16:16:21 -0700233 */
Todd Kennedy83693a62011-05-10 11:36:57 -0700234 public void watchForMessages(final boolean watch) {
Ben Komaloeb9dcfa2011-07-21 11:02:51 -0700235 if (Email.DEBUG) {
236 Log.i(Logging.LOG_TAG, "Notifications being toggled: " + watch);
237 }
Todd Kennedy83693a62011-05-10 11:36:57 -0700238 // Don't create the thread if we're only going to stop watching
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700239 if (!watch && sNotificationThread == null) return;
Todd Kennedy83693a62011-05-10 11:36:57 -0700240
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700241 ensureHandlerExists();
Todd Kennedy83693a62011-05-10 11:36:57 -0700242 // Run this on the message notification handler
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700243 sNotificationHandler.post(new Runnable() {
Todd Kennedy83693a62011-05-10 11:36:57 -0700244 @Override
245 public void run() {
246 ContentResolver resolver = mContext.getContentResolver();
Todd Kennedy83693a62011-05-10 11:36:57 -0700247 if (!watch) {
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700248 unregisterMessageNotification(Account.ACCOUNT_ID_COMBINED_VIEW);
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700249 if (mAccountObserver != null) {
250 resolver.unregisterContentObserver(mAccountObserver);
251 mAccountObserver = null;
252 }
Todd Kennedy83693a62011-05-10 11:36:57 -0700253
254 // tear down the event loop
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700255 sNotificationThread.quit();
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700256 sNotificationThread = null;
Todd Kennedy83693a62011-05-10 11:36:57 -0700257 return;
258 }
259
260 // otherwise, start new observers for all notified accounts
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700261 registerMessageNotification(Account.ACCOUNT_ID_COMBINED_VIEW);
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700262 // If we're already observing account changes, don't do anything else
263 if (mAccountObserver == null) {
Ben Komaloeb9dcfa2011-07-21 11:02:51 -0700264 if (Email.DEBUG) {
265 Log.i(Logging.LOG_TAG, "Observing account changes for notifications");
266 }
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700267 mAccountObserver = new AccountContentObserver(sNotificationHandler, mContext);
268 resolver.registerContentObserver(Account.NOTIFIER_URI, true, mAccountObserver);
Todd Kennedy83693a62011-05-10 11:36:57 -0700269 }
Todd Kennedy83693a62011-05-10 11:36:57 -0700270 }
271 });
272 }
273
274 /**
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700275 * Temporarily suspend a single account from receiving notifications. NOTE: only a single
276 * account may ever be suspended at a time. So, if this method is invoked a second time,
277 * notifications for the previously suspended account will automatically be re-activated.
278 * @param suspend If {@code true}, suspend notifications for the given account. Otherwise,
279 * re-activate notifications for the previously suspended account.
280 * @param accountId The ID of the account. If this is the special account ID
281 * {@link Account#ACCOUNT_ID_COMBINED_VIEW}, notifications for all accounts are
282 * suspended. If {@code suspend} is {@code false}, the account ID is ignored.
283 */
284 public void suspendMessageNotification(boolean suspend, long accountId) {
Makoto Onukib36ac012011-05-17 10:50:30 -0700285 if (mSuspendAccountId != Account.NO_ACCOUNT) {
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700286 // we're already suspending an account; un-suspend it
Makoto Onukib36ac012011-05-17 10:50:30 -0700287 mSuspendAccountId = Account.NO_ACCOUNT;
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700288 }
Makoto Onukib36ac012011-05-17 10:50:30 -0700289 if (suspend && accountId != Account.NO_ACCOUNT && accountId > 0L) {
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700290 mSuspendAccountId = accountId;
291 if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
292 // Only go onto the notification handler if we really, absolutely need to
293 ensureHandlerExists();
294 sNotificationHandler.post(new Runnable() {
295 @Override
296 public void run() {
297 for (long accountId : mNotificationMap.keySet()) {
298 mNotificationManager.cancel(getNewMessageNotificationId(accountId));
299 }
300 }
301 });
302 } else {
303 mNotificationManager.cancel(getNewMessageNotificationId(accountId));
304 }
305 }
306 }
307
308 /**
309 * Ensures the notification handler exists and is ready to handle requests.
310 */
311 private static synchronized void ensureHandlerExists() {
312 if (sNotificationThread == null) {
313 sNotificationThread = new NotificationThread();
314 sNotificationHandler = new Handler(sNotificationThread.getLooper());
315 }
316 }
317
318 /**
Todd Kennedy83693a62011-05-10 11:36:57 -0700319 * Registers an observer for changes to the INBOX for the given account. Since accounts
320 * may only have a single INBOX, we will never have more than one observer for an account.
321 * NOTE: This must be called on the notification handler thread.
322 * @param accountId The ID of the account to register the observer for. May be
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700323 * {@link Account#ACCOUNT_ID_COMBINED_VIEW} to register observers for all
324 * accounts that allow for user notification.
Todd Kennedy83693a62011-05-10 11:36:57 -0700325 */
326 private void registerMessageNotification(long accountId) {
327 ContentResolver resolver = mContext.getContentResolver();
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700328 if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
Todd Kennedy83693a62011-05-10 11:36:57 -0700329 Cursor c = resolver.query(
330 Account.CONTENT_URI, EmailContent.ID_PROJECTION,
331 NOTIFIED_ACCOUNT_SELECTION, null, null);
332 try {
333 while (c.moveToNext()) {
334 long id = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
335 registerMessageNotification(id);
336 }
337 } finally {
338 c.close();
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700339 }
Makoto Onuki899c5b82010-09-26 16:16:21 -0700340 } else {
Marc Blankaca94262011-07-19 18:19:59 -0700341 ContentObserver obs = mNotificationMap.get(accountId);
342 if (obs != null) return; // we're already observing; nothing to do
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700343
Todd Kennedy83693a62011-05-10 11:36:57 -0700344 Mailbox mailbox = Mailbox.restoreMailboxOfType(mContext, accountId, Mailbox.TYPE_INBOX);
Todd Kennedydfdc8b62011-05-11 13:40:52 -0700345 if (mailbox == null) {
346 Log.w(Logging.LOG_TAG, "Could not load INBOX for account id: " + accountId);
347 return;
348 }
Ben Komaloeb9dcfa2011-07-21 11:02:51 -0700349 if (Email.DEBUG) {
350 Log.i(Logging.LOG_TAG, "Registering for notifications for account " + accountId);
351 }
Todd Kennedy83693a62011-05-10 11:36:57 -0700352 ContentObserver observer = new MessageContentObserver(
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700353 sNotificationHandler, mContext, mailbox.mId, accountId);
Todd Kennedy83693a62011-05-10 11:36:57 -0700354 resolver.registerContentObserver(Message.NOTIFIER_URI, true, observer);
Marc Blankaca94262011-07-19 18:19:59 -0700355 mNotificationMap.put(accountId, observer);
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700356 // Now, ping the observer for any initial notifications
Marc Blankaca94262011-07-19 18:19:59 -0700357 observer.onChange(true);
Makoto Onuki899c5b82010-09-26 16:16:21 -0700358 }
359 }
360
361 /**
Todd Kennedy83693a62011-05-10 11:36:57 -0700362 * Unregisters the observer for the given account. If the specified account does not have
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700363 * a registered observer, no action is performed. This will not clear any existing notification
364 * for the specified account. Use {@link NotificationManager#cancel(int)}.
Todd Kennedy83693a62011-05-10 11:36:57 -0700365 * NOTE: This must be called on the notification handler thread.
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700366 * @param accountId The ID of the account to unregister from. To unregister all accounts that
367 * have observers, specify an ID of {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
Makoto Onuki899c5b82010-09-26 16:16:21 -0700368 */
Todd Kennedy83693a62011-05-10 11:36:57 -0700369 private void unregisterMessageNotification(long accountId) {
370 ContentResolver resolver = mContext.getContentResolver();
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700371 if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
Ben Komaloeb9dcfa2011-07-21 11:02:51 -0700372 if (Email.DEBUG) {
373 Log.i(Logging.LOG_TAG, "Unregistering notifications for all accounts");
374 }
Todd Kennedy83693a62011-05-10 11:36:57 -0700375 // cancel all existing message observers
Marc Blankaca94262011-07-19 18:19:59 -0700376 for (ContentObserver observer : mNotificationMap.values()) {
Todd Kennedy83693a62011-05-10 11:36:57 -0700377 resolver.unregisterContentObserver(observer);
378 }
379 mNotificationMap.clear();
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700380 } else {
Ben Komaloeb9dcfa2011-07-21 11:02:51 -0700381 if (Email.DEBUG) {
382 Log.i(Logging.LOG_TAG, "Unregistering notifications for account " + accountId);
383 }
Marc Blankaca94262011-07-19 18:19:59 -0700384 ContentObserver observer = mNotificationMap.remove(accountId);
385 if (observer != null) {
Todd Kennedy83693a62011-05-10 11:36:57 -0700386 resolver.unregisterContentObserver(observer);
387 }
388 }
389 }
390
391 /**
Todd Kennedy958b15e2011-04-28 11:09:41 -0700392 * Returns a picture of the sender of the given message. If no picture is available, returns
393 * {@code null}.
Makoto Onuki899c5b82010-09-26 16:16:21 -0700394 *
Todd Kennedy958b15e2011-04-28 11:09:41 -0700395 * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
Makoto Onuki899c5b82010-09-26 16:16:21 -0700396 */
397 private Bitmap getSenderPhoto(Message message) {
398 Address sender = Address.unpackFirst(message.mFrom);
399 if (sender == null) {
400 return null;
401 }
402 String email = sender.getAddress();
403 if (TextUtils.isEmpty(email)) {
404 return null;
405 }
Todd Kennedy958b15e2011-04-28 11:09:41 -0700406 return ContactStatusLoader.getContactInfo(mContext, email).mPhoto;
Makoto Onuki899c5b82010-09-26 16:16:21 -0700407 }
408
Makoto Onukidd6e6b82011-03-08 16:36:47 -0800409 /**
Todd Kennedy958b15e2011-04-28 11:09:41 -0700410 * Returns a "new message" notification for the given account.
Makoto Onuki899c5b82010-09-26 16:16:21 -0700411 *
Todd Kennedy958b15e2011-04-28 11:09:41 -0700412 * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
Makoto Onuki899c5b82010-09-26 16:16:21 -0700413 */
Todd Kennedy958b15e2011-04-28 11:09:41 -0700414 @VisibleForTesting
Todd Kennedyf4378922011-05-13 08:41:48 -0700415 Notification createNewMessageNotification(long accountId, long mailboxId, long messageId,
Ben Komalo23a4b152011-07-22 11:41:51 -0700416 int unseenMessageCount, int unreadCount) {
Makoto Onuki899c5b82010-09-26 16:16:21 -0700417 final Account account = Account.restoreAccountWithId(mContext, accountId);
418 if (account == null) {
419 return null;
420 }
421 // Get the latest message
Todd Kennedy83693a62011-05-10 11:36:57 -0700422 final Message message = Message.restoreMessageWithId(mContext, messageId);
Makoto Onuki899c5b82010-09-26 16:16:21 -0700423 if (message == null) {
424 return null; // no message found???
425 }
426
Makoto Onuki43c455e2011-03-08 10:34:18 -0800427 String senderName = Address.toFriendly(Address.unpack(message.mFrom));
428 if (senderName == null) {
429 senderName = ""; // Happens when a message has no from.
430 }
Ben Komalo6f93d2e2011-06-23 17:48:25 -0700431 final boolean multipleUnseen = unseenMessageCount > 1;
Ben Komalof13fee52011-08-17 17:10:06 -0700432 final Bitmap senderPhoto = multipleUnseen
433 ? mGenericMultipleSenderIcon
434 : getSenderPhoto(message);
Ben Komalo6f93d2e2011-06-23 17:48:25 -0700435 final SpannableString title = getNewMessageTitle(senderName, unseenMessageCount);
436 // TODO: add in display name on the second line for the text, once framework supports
437 // multiline texts.
438 final String text = multipleUnseen
439 ? account.mDisplayName
440 : message.mSubject;
Todd Kennedy958b15e2011-04-28 11:09:41 -0700441 final Bitmap largeIcon = senderPhoto != null ? senderPhoto : mGenericSenderIcon;
Ben Komalo6f93d2e2011-06-23 17:48:25 -0700442 final Integer number = unreadCount > 1 ? unreadCount : null;
Todd Kennedyf4378922011-05-13 08:41:48 -0700443 final Intent intent;
Ben Komaloe9188302011-07-10 14:02:50 -0700444 if (unseenMessageCount > 1) {
Todd Kennedyf4378922011-05-13 08:41:48 -0700445 intent = Welcome.createOpenAccountInboxIntent(mContext, accountId);
Ben Komaloe9188302011-07-10 14:02:50 -0700446 } else {
447 intent = Welcome.createOpenMessageIntent(mContext, accountId, mailboxId, messageId);
Todd Kennedyf4378922011-05-13 08:41:48 -0700448 }
Makoto Onuki899c5b82010-09-26 16:16:21 -0700449
Ben Komalo23a4b152011-07-22 11:41:51 -0700450 long now = mClock.getTime();
Ben Komalo48a3a1c2011-07-22 13:29:48 -0700451 boolean enableAudio = (now - mLastMessageNotifyTime) > MIN_SOUND_INTERVAL_MS;
Ben Komalo23a4b152011-07-22 11:41:51 -0700452 Notification notification = createAccountNotification(
453 account, title.toString(), title, text,
Marc Blankd9b2a8f2011-07-28 13:55:10 -0700454 intent, largeIcon, number, enableAudio, false);
Ben Komalo23a4b152011-07-22 11:41:51 -0700455 mLastMessageNotifyTime = now;
Makoto Onuki899c5b82010-09-26 16:16:21 -0700456 return notification;
457 }
458
Makoto Onuki74e09482010-12-03 14:44:47 -0800459 /**
Ben Komalo6f93d2e2011-06-23 17:48:25 -0700460 * Creates a notification title for a new message. If there is only a single message,
461 * show the sender name. Otherwise, show "X new messages".
Makoto Onuki74e09482010-12-03 14:44:47 -0800462 */
Todd Kennedy958b15e2011-04-28 11:09:41 -0700463 @VisibleForTesting
Ben Komalo6f93d2e2011-06-23 17:48:25 -0700464 SpannableString getNewMessageTitle(String sender, int unseenCount) {
465 String title;
466 if (unseenCount > 1) {
467 title = String.format(
468 mContext.getString(R.string.notification_multiple_new_messages_fmt),
469 unseenCount);
Makoto Onuki74e09482010-12-03 14:44:47 -0800470 } else {
Ben Komalo6f93d2e2011-06-23 17:48:25 -0700471 title = sender;
Makoto Onuki74e09482010-12-03 14:44:47 -0800472 }
Ben Komalo6f93d2e2011-06-23 17:48:25 -0700473 return new SpannableString(title);
Makoto Onuki899c5b82010-09-26 16:16:21 -0700474 }
475
Todd Kennedy958b15e2011-04-28 11:09:41 -0700476 /** Returns the system's current ringer mode */
477 @VisibleForTesting
478 int getRingerMode() {
Makoto Onuki74e09482010-12-03 14:44:47 -0800479 return mAudioManager.getRingerMode();
480 }
481
Todd Kennedy958b15e2011-04-28 11:09:41 -0700482 /** Sets up the notification's sound and vibration based upon account details. */
483 @VisibleForTesting
484 void setupSoundAndVibration(Notification.Builder builder, Account account) {
Makoto Onuki899c5b82010-09-26 16:16:21 -0700485 final int flags = account.mFlags;
486 final String ringtoneUri = account.mRingtoneUri;
487 final boolean vibrate = (flags & Account.FLAGS_VIBRATE_ALWAYS) != 0;
488 final boolean vibrateWhenSilent = (flags & Account.FLAGS_VIBRATE_WHEN_SILENT) != 0;
Todd Kennedy958b15e2011-04-28 11:09:41 -0700489 final boolean isRingerSilent = getRingerMode() != AudioManager.RINGER_MODE_NORMAL;
Makoto Onuki899c5b82010-09-26 16:16:21 -0700490
Todd Kennedy958b15e2011-04-28 11:09:41 -0700491 int defaults = Notification.DEFAULT_LIGHTS;
492 if (vibrate || (vibrateWhenSilent && isRingerSilent)) {
493 defaults |= Notification.DEFAULT_VIBRATE;
Makoto Onuki899c5b82010-09-26 16:16:21 -0700494 }
495
Todd Kennedy958b15e2011-04-28 11:09:41 -0700496 builder.setSound((ringtoneUri == null) ? null : Uri.parse(ringtoneUri))
497 .setDefaults(defaults);
Makoto Onuki899c5b82010-09-26 16:16:21 -0700498 }
Marc Blankd3e4f3c2010-10-18 13:14:20 -0700499
500 /**
Todd Kennedy958b15e2011-04-28 11:09:41 -0700501 * Show (or update) a notification that the given attachment could not be forwarded. This
502 * is a very unusual case, and perhaps we shouldn't even send a notification. For now,
503 * it's helpful for debugging.
504 *
Andy Stadlerc6d344a2011-02-16 16:38:18 -0800505 * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
Marc Blankd3e4f3c2010-10-18 13:14:20 -0700506 */
Andy Stadlerc6d344a2011-02-16 16:38:18 -0800507 public void showDownloadForwardFailedNotification(Attachment attachment) {
508 final Account account = Account.restoreAccountWithId(mContext, attachment.mAccountKey);
509 if (account == null) return;
Makoto Onuki308ce922011-03-21 17:08:16 -0700510 showAccountNotification(account,
Marc Blankd3e4f3c2010-10-18 13:14:20 -0700511 mContext.getString(R.string.forward_download_failed_ticker),
Andy Stadlerc6d344a2011-02-16 16:38:18 -0800512 mContext.getString(R.string.forward_download_failed_title),
513 attachment.mFileName,
514 null,
515 NOTIFICATION_ID_ATTACHMENT_WARNING);
Marc Blankd3e4f3c2010-10-18 13:14:20 -0700516 }
517
518 /**
Todd Kennedy958b15e2011-04-28 11:09:41 -0700519 * Returns a notification ID for login failed notifications for the given account account.
Marc Blankd3e4f3c2010-10-18 13:14:20 -0700520 */
521 private int getLoginFailedNotificationId(long accountId) {
522 return NOTIFICATION_ID_BASE_LOGIN_WARNING + (int)accountId;
523 }
524
Andy Stadlerc6d344a2011-02-16 16:38:18 -0800525 /**
Todd Kennedy958b15e2011-04-28 11:09:41 -0700526 * Show (or update) a notification that there was a login failure for the given account.
527 *
Andy Stadlerc6d344a2011-02-16 16:38:18 -0800528 * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
529 */
Marc Blankd3e4f3c2010-10-18 13:14:20 -0700530 public void showLoginFailedNotification(long accountId) {
531 final Account account = Account.restoreAccountWithId(mContext, accountId);
532 if (account == null) return;
Makoto Onuki308ce922011-03-21 17:08:16 -0700533 showAccountNotification(account,
Marc Blankd3e4f3c2010-10-18 13:14:20 -0700534 mContext.getString(R.string.login_failed_ticker, account.mDisplayName),
Andy Stadlerc6d344a2011-02-16 16:38:18 -0800535 mContext.getString(R.string.login_failed_title),
536 account.getDisplayName(),
Ben Komalo28662842011-05-12 17:27:56 -0700537 AccountSettings.createAccountSettingsIntent(mContext, accountId,
Andy Stadlerf4894132011-02-18 18:23:18 -0800538 account.mDisplayName),
Andy Stadlerc6d344a2011-02-16 16:38:18 -0800539 getLoginFailedNotificationId(accountId));
Marc Blankd3e4f3c2010-10-18 13:14:20 -0700540 }
541
Todd Kennedy958b15e2011-04-28 11:09:41 -0700542 /**
543 * Cancels the login failed notification for the given account.
544 */
Marc Blankd3e4f3c2010-10-18 13:14:20 -0700545 public void cancelLoginFailedNotification(long accountId) {
546 mNotificationManager.cancel(getLoginFailedNotificationId(accountId));
547 }
Makoto Onuki308ce922011-03-21 17:08:16 -0700548
549 /**
Todd Kennedy958b15e2011-04-28 11:09:41 -0700550 * Show (or update) a notification that the user's password is expiring. The given account
551 * is used to update the display text, but, all accounts share the same notification ID.
Makoto Onuki308ce922011-03-21 17:08:16 -0700552 *
Todd Kennedy958b15e2011-04-28 11:09:41 -0700553 * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
Makoto Onuki308ce922011-03-21 17:08:16 -0700554 */
555 public void showPasswordExpiringNotification(long accountId) {
556 Account account = Account.restoreAccountWithId(mContext, accountId);
557 if (account == null) return;
Todd Kennedy958b15e2011-04-28 11:09:41 -0700558
Makoto Onuki308ce922011-03-21 17:08:16 -0700559 Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext,
560 accountId, false);
Todd Kennedy958b15e2011-04-28 11:09:41 -0700561 String accountName = account.getDisplayName();
562 String ticker =
563 mContext.getString(R.string.password_expire_warning_ticker_fmt, accountName);
564 String title = mContext.getString(R.string.password_expire_warning_content_title);
565 showAccountNotification(account, ticker, title, accountName, intent,
Makoto Onuki308ce922011-03-21 17:08:16 -0700566 NOTIFICATION_ID_PASSWORD_EXPIRING);
567 }
568
569 /**
Todd Kennedy958b15e2011-04-28 11:09:41 -0700570 * Show (or update) a notification that the user's password has expired. The given account
571 * is used to update the display text, but, all accounts share the same notification ID.
Makoto Onuki308ce922011-03-21 17:08:16 -0700572 *
Todd Kennedy958b15e2011-04-28 11:09:41 -0700573 * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
Makoto Onuki308ce922011-03-21 17:08:16 -0700574 */
575 public void showPasswordExpiredNotification(long accountId) {
576 Account account = Account.restoreAccountWithId(mContext, accountId);
577 if (account == null) return;
Todd Kennedy958b15e2011-04-28 11:09:41 -0700578
Makoto Onuki308ce922011-03-21 17:08:16 -0700579 Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext,
580 accountId, true);
Todd Kennedy958b15e2011-04-28 11:09:41 -0700581 String accountName = account.getDisplayName();
Makoto Onuki308ce922011-03-21 17:08:16 -0700582 String ticker = mContext.getString(R.string.password_expired_ticker);
Todd Kennedy958b15e2011-04-28 11:09:41 -0700583 String title = mContext.getString(R.string.password_expired_content_title);
584 showAccountNotification(account, ticker, title, accountName, intent,
585 NOTIFICATION_ID_PASSWORD_EXPIRED);
Makoto Onuki308ce922011-03-21 17:08:16 -0700586 }
587
588 /**
Todd Kennedy958b15e2011-04-28 11:09:41 -0700589 * Cancels any password expire notifications [both expired & expiring].
Makoto Onuki308ce922011-03-21 17:08:16 -0700590 */
591 public void cancelPasswordExpirationNotifications() {
Todd Kennedy83693a62011-05-10 11:36:57 -0700592 mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRING);
593 mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRED);
Makoto Onuki308ce922011-03-21 17:08:16 -0700594 }
595
596 /**
Marc Blank2736c1a2011-10-20 10:13:02 -0700597 * Show (or update) a security needed notification. If tapped, the user is taken to a
598 * dialog asking whether he wants to update his settings.
Makoto Onuki308ce922011-03-21 17:08:16 -0700599 */
600 public void showSecurityNeededNotification(Account account) {
Makoto Onuki308ce922011-03-21 17:08:16 -0700601 Intent intent = AccountSecurity.actionUpdateSecurityIntent(mContext, account.mId, true);
Todd Kennedy958b15e2011-04-28 11:09:41 -0700602 String accountName = account.getDisplayName();
603 String ticker =
Marc Blank2736c1a2011-10-20 10:13:02 -0700604 mContext.getString(R.string.security_needed_ticker_fmt, accountName);
605 String title = mContext.getString(R.string.security_notification_content_update_title);
Todd Kennedy958b15e2011-04-28 11:09:41 -0700606 showAccountNotification(account, ticker, title, accountName, intent,
Marc Blank2736c1a2011-10-20 10:13:02 -0700607 (int)(NOTIFICATION_ID_BASE_SECURITY_NEEDED + account.mId));
Makoto Onuki308ce922011-03-21 17:08:16 -0700608 }
609
610 /**
Marc Blank2736c1a2011-10-20 10:13:02 -0700611 * Show (or update) a security changed notification. If tapped, the user is taken to the
612 * account settings screen where he can view the list of enforced policies
613 */
614 public void showSecurityChangedNotification(Account account) {
615 Intent intent = AccountSettings.createAccountSettingsIntent(mContext, account.mId, null);
616 String accountName = account.getDisplayName();
617 String ticker =
618 mContext.getString(R.string.security_changed_ticker_fmt, accountName);
619 String title = mContext.getString(R.string.security_notification_content_change_title);
620 showAccountNotification(account, ticker, title, accountName, intent,
621 (int)(NOTIFICATION_ID_BASE_SECURITY_CHANGED + account.mId));
622 }
623
624 /**
625 * Show (or update) a security unsupported notification. If tapped, the user is taken to the
626 * account settings screen where he can view the list of unsupported policies
627 */
628 public void showSecurityUnsupportedNotification(Account account) {
629 Intent intent = AccountSettings.createAccountSettingsIntent(mContext, account.mId, null);
630 String accountName = account.getDisplayName();
631 String ticker =
632 mContext.getString(R.string.security_unsupported_ticker_fmt, accountName);
633 String title = mContext.getString(R.string.security_notification_content_unsupported_title);
634 showAccountNotification(account, ticker, title, accountName, intent,
635 (int)(NOTIFICATION_ID_BASE_SECURITY_NEEDED + account.mId));
636 }
637
638 /**
639 * Cancels all security needed notifications.
Makoto Onuki308ce922011-03-21 17:08:16 -0700640 */
641 public void cancelSecurityNeededNotification() {
Marc Blank2736c1a2011-10-20 10:13:02 -0700642 EmailAsyncTask.runAsyncParallel(new Runnable() {
643 @Override
644 public void run() {
645 Cursor c = mContext.getContentResolver().query(Account.CONTENT_URI,
646 Account.ID_PROJECTION, null, null, null);
647 try {
648 while (c.moveToNext()) {
649 long id = c.getLong(Account.ID_PROJECTION_COLUMN);
650 mNotificationManager.cancel(
651 (int)(NOTIFICATION_ID_BASE_SECURITY_NEEDED + id));
652 }
653 }
654 finally {
655 c.close();
656 }
657 }});
Makoto Onuki308ce922011-03-21 17:08:16 -0700658 }
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700659
660 /**
661 * Observer invoked whenever a message we're notifying the user about changes.
662 */
663 private static class MessageContentObserver extends ContentObserver {
Todd Kennedy83693a62011-05-10 11:36:57 -0700664 /** A selection to get messages the user hasn't seen before */
665 private final static String MESSAGE_SELECTION =
Ben Komalo48a3a1c2011-07-22 13:29:48 -0700666 MessageColumns.MAILBOX_KEY + "=? AND "
667 + MessageColumns.ID + ">? AND "
668 + MessageColumns.FLAG_READ + "=0 AND "
669 + Message.FLAG_LOADED_SELECTION;
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700670 private final Context mContext;
Todd Kennedy83693a62011-05-10 11:36:57 -0700671 private final long mMailboxId;
672 private final long mAccountId;
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700673
Todd Kennedy83693a62011-05-10 11:36:57 -0700674 public MessageContentObserver(
675 Handler handler, Context context, long mailboxId, long accountId) {
676 super(handler);
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700677 mContext = context;
Todd Kennedy83693a62011-05-10 11:36:57 -0700678 mMailboxId = mailboxId;
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700679 mAccountId = accountId;
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700680 }
681
682 @Override
683 public void onChange(boolean selfChange) {
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700684 if (mAccountId == sInstance.mSuspendAccountId
685 || sInstance.mSuspendAccountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
686 return;
687 }
Todd Kennedy83693a62011-05-10 11:36:57 -0700688
Marc Blankaca94262011-07-19 18:19:59 -0700689 ContentObserver observer = sInstance.mNotificationMap.get(mAccountId);
690 if (observer == null) {
Ben Komaloeb9dcfa2011-07-21 11:02:51 -0700691 // Notification for a mailbox that we aren't observing; account is probably
692 // being deleted.
693 Log.w(Logging.LOG_TAG, "Received notification when observer data was null");
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700694 return;
695 }
Marc Blankaca94262011-07-19 18:19:59 -0700696 Account account = Account.restoreAccountWithId(mContext, mAccountId);
Ben Komaloeb9dcfa2011-07-21 11:02:51 -0700697 if (account == null) {
698 Log.w(Logging.LOG_TAG, "Couldn't find account for changed message notification");
699 return;
700 }
Marc Blankaca94262011-07-19 18:19:59 -0700701 long oldMessageId = account.mNotifiedMessageId;
702 int oldMessageCount = account.mNotifiedMessageCount;
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700703
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700704 ContentResolver resolver = mContext.getContentResolver();
Ben Komaloeb9dcfa2011-07-21 11:02:51 -0700705 Long lastSeenMessageId = Utility.getFirstRowLong(
Marc Blank6e418aa2011-06-18 18:03:11 -0700706 mContext, ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailboxId),
Todd Kennedy83693a62011-05-10 11:36:57 -0700707 new String[] { MailboxColumns.LAST_SEEN_MESSAGE_KEY },
Ben Komaloeb9dcfa2011-07-21 11:02:51 -0700708 null, null, null, 0);
709 if (lastSeenMessageId == null) {
710 // Mailbox got nuked. Could be that the account is in the process of being deleted
711 Log.w(Logging.LOG_TAG, "Couldn't find mailbox for changed message notification");
712 return;
713 }
714
Todd Kennedy83693a62011-05-10 11:36:57 -0700715 Cursor c = resolver.query(
716 Message.CONTENT_URI, EmailContent.ID_PROJECTION,
717 MESSAGE_SELECTION,
718 new String[] { Long.toString(mMailboxId), Long.toString(lastSeenMessageId) },
719 MessageColumns.ID + " DESC");
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700720 if (c == null) {
Ben Komaloeb9dcfa2011-07-21 11:02:51 -0700721 // Couldn't find message info - things may be getting deleted in bulk.
722 Log.w(Logging.LOG_TAG, "#onChange(); NULL response for message id query");
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700723 return;
724 }
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700725 try {
Todd Kennedy83693a62011-05-10 11:36:57 -0700726 int newMessageCount = c.getCount();
727 long newMessageId = 0L;
728 if (c.moveToNext()) {
729 newMessageId = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700730 }
Todd Kennedy83693a62011-05-10 11:36:57 -0700731
732 if (newMessageCount == 0) {
733 // No messages to notify for; clear the notification
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700734 int notificationId = sInstance.getNewMessageNotificationId(mAccountId);
735 sInstance.mNotificationManager.cancel(notificationId);
Todd Kennedy83693a62011-05-10 11:36:57 -0700736 } else if (newMessageCount != oldMessageCount
737 || (newMessageId != 0 && newMessageId != oldMessageId)) {
738 // Either the count or last message has changed; update the notification
Ben Komaloeb9dcfa2011-07-21 11:02:51 -0700739 Integer unreadCount = Utility.getFirstRowInt(
Ben Komalo6f93d2e2011-06-23 17:48:25 -0700740 mContext, ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailboxId),
741 new String[] { MailboxColumns.UNREAD_COUNT },
Ben Komaloeb9dcfa2011-07-21 11:02:51 -0700742 null, null, null, 0);
743 if (unreadCount == null) {
744 Log.w(Logging.LOG_TAG, "Couldn't find unread count for mailbox");
745 return;
746 }
Ben Komalo6f93d2e2011-06-23 17:48:25 -0700747
Todd Kennedy83693a62011-05-10 11:36:57 -0700748 Notification n = sInstance.createNewMessageNotification(
Ben Komalo6f93d2e2011-06-23 17:48:25 -0700749 mAccountId, mMailboxId, newMessageId,
Ben Komalo23a4b152011-07-22 11:41:51 -0700750 newMessageCount, unreadCount);
Todd Kennedy83693a62011-05-10 11:36:57 -0700751 if (n != null) {
752 // Make the notification visible
753 sInstance.mNotificationManager.notify(
754 sInstance.getNewMessageNotificationId(mAccountId), n);
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700755 }
Todd Kennedy83693a62011-05-10 11:36:57 -0700756 }
Marc Blankaca94262011-07-19 18:19:59 -0700757 // Save away the new values
758 ContentValues cv = new ContentValues();
759 cv.put(AccountColumns.NOTIFIED_MESSAGE_ID, newMessageId);
760 cv.put(AccountColumns.NOTIFIED_MESSAGE_COUNT, newMessageCount);
761 resolver.update(ContentUris.withAppendedId(Account.CONTENT_URI, mAccountId), cv,
762 null, null);
Todd Kennedy83693a62011-05-10 11:36:57 -0700763 } finally {
764 c.close();
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700765 }
766 }
767 }
768
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700769 /**
770 * Observer invoked whenever an account is modified. This could mean the user changed the
771 * notification settings.
772 */
773 private static class AccountContentObserver extends ContentObserver {
774 private final Context mContext;
775 public AccountContentObserver(Handler handler, Context context) {
776 super(handler);
777 mContext = context;
778 }
779
780 @Override
781 public void onChange(boolean selfChange) {
782 final ContentResolver resolver = mContext.getContentResolver();
783 final Cursor c = resolver.query(
784 Account.CONTENT_URI, EmailContent.ID_PROJECTION,
785 NOTIFIED_ACCOUNT_SELECTION, null, null);
786 final HashSet<Long> newAccountList = new HashSet<Long>();
787 final HashSet<Long> removedAccountList = new HashSet<Long>();
788 if (c == null) {
789 // Suspender time ... theoretically, this will never happen
790 Log.wtf(Logging.LOG_TAG, "#onChange(); NULL response for account id query");
791 return;
792 }
793 try {
794 while (c.moveToNext()) {
795 long accountId = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
796 newAccountList.add(accountId);
797 }
798 } finally {
799 if (c != null) {
800 c.close();
801 }
802 }
803 // NOTE: Looping over three lists is not necessarily the most efficient. However, the
804 // account lists are going to be very small, so, this will not be necessarily bad.
805 // Cycle through existing notification list and adjust as necessary
806 for (long accountId : sInstance.mNotificationMap.keySet()) {
807 if (!newAccountList.remove(accountId)) {
808 // account id not in the current set of notifiable accounts
809 removedAccountList.add(accountId);
810 }
811 }
812 // A new account was added to the notification list
813 for (long accountId : newAccountList) {
814 sInstance.registerMessageNotification(accountId);
815 }
816 // An account was removed from the notification list
817 for (long accountId : removedAccountList) {
818 sInstance.unregisterMessageNotification(accountId);
819 int notificationId = sInstance.getNewMessageNotificationId(accountId);
820 sInstance.mNotificationManager.cancel(notificationId);
821 }
822 }
823 }
824
Todd Kennedy83693a62011-05-10 11:36:57 -0700825 /**
826 * Thread to handle all notification actions through its own {@link Looper}.
827 */
828 private static class NotificationThread implements Runnable {
829 /** Lock to ensure proper initialization */
830 private final Object mLock = new Object();
831 /** The {@link Looper} that handles messages for this thread */
832 private Looper mLooper;
833
834 NotificationThread() {
835 new Thread(null, this, "EmailNotification").start();
836 synchronized (mLock) {
837 while (mLooper == null) {
838 try {
839 mLock.wait();
840 } catch (InterruptedException ex) {
841 }
842 }
843 }
844 }
845
846 @Override
847 public void run() {
848 synchronized (mLock) {
849 Looper.prepare();
850 mLooper = Looper.myLooper();
851 mLock.notifyAll();
852 }
853 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
854 Looper.loop();
855 }
856 void quit() {
857 mLooper.quit();
858 }
859 Looper getLooper() {
860 return mLooper;
861 }
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700862 }
Makoto Onuki899c5b82010-09-26 16:16:21 -0700863}