blob: 5ed3e012f2bdd9e03d6c684d36215867ccb77cd3 [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;
55import com.android.emailcommon.utility.Utility;
56import com.google.common.annotations.VisibleForTesting;
57
Todd Kennedyc4cdb112011-05-03 14:42:26 -070058import java.util.HashMap;
Todd Kennedye7fb4ac2011-05-11 15:29:24 -070059import java.util.HashSet;
Todd Kennedyc4cdb112011-05-03 14:42:26 -070060
Makoto Onuki899c5b82010-09-26 16:16:21 -070061/**
62 * Class that manages notifications.
Makoto Onuki899c5b82010-09-26 16:16:21 -070063 */
64public class NotificationController {
Makoto Onuki308ce922011-03-21 17:08:16 -070065 private static final int NOTIFICATION_ID_SECURITY_NEEDED = 1;
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
73 private static final int NOTIFICATION_ID_BASE_NEW_MESSAGES = 0x10000000;
74 private static final int NOTIFICATION_ID_BASE_LOGIN_WARNING = 0x20000000;
Makoto Onuki899c5b82010-09-26 16:16:21 -070075
Todd Kennedy83693a62011-05-10 11:36:57 -070076 /** Selection to retrieve accounts that should we notify user for changes */
77 private final static String NOTIFIED_ACCOUNT_SELECTION =
78 Account.FLAGS + "&" + Account.FLAGS_NOTIFY_NEW_MAIL + " != 0";
Todd Kennedy83693a62011-05-10 11:36:57 -070079
Todd Kennedye7fb4ac2011-05-11 15:29:24 -070080 private static NotificationThread sNotificationThread;
81 private static Handler sNotificationHandler;
Makoto Onuki899c5b82010-09-26 16:16:21 -070082 private static NotificationController sInstance;
83 private final Context mContext;
84 private final NotificationManager mNotificationManager;
85 private final AudioManager mAudioManager;
Andy Stadlerc1c3b6f2010-12-15 15:26:30 -080086 private final Bitmap mGenericSenderIcon;
Ben Komalof13fee52011-08-17 17:10:06 -070087 private final Bitmap mGenericMultipleSenderIcon;
Makoto Onuki74e09482010-12-03 14:44:47 -080088 private final Clock mClock;
Todd Kennedy83693a62011-05-10 11:36:57 -070089 // TODO We're maintaining all of our structures based upon the account ID. This is fine
90 // for now since the assumption is that we only ever look for changes in an account's
91 // INBOX. We should adjust our logic to use the mailbox ID instead.
Todd Kennedyc4cdb112011-05-03 14:42:26 -070092 /** Maps account id to the message data */
Marc Blankaca94262011-07-19 18:19:59 -070093 private final HashMap<Long, ContentObserver> mNotificationMap;
Todd Kennedye7fb4ac2011-05-11 15:29:24 -070094 private ContentObserver mAccountObserver;
Todd Kennedy5701e0a2011-05-12 10:13:45 -070095 /**
Makoto Onukib36ac012011-05-17 10:50:30 -070096 * Suspend notifications for this account. If {@link Account#NO_ACCOUNT}, no
Todd Kennedy5701e0a2011-05-12 10:13:45 -070097 * account notifications are suspended. If {@link Account#ACCOUNT_ID_COMBINED_VIEW},
98 * notifications for all accounts are suspended.
99 */
Makoto Onukib36ac012011-05-17 10:50:30 -0700100 private long mSuspendAccountId = Account.NO_ACCOUNT;
Makoto Onuki899c5b82010-09-26 16:16:21 -0700101
Ben Komalo23a4b152011-07-22 11:41:51 -0700102 /**
103 * Timestamp indicating when the last message notification sound was played.
104 * Used for throttling.
105 */
106 private long mLastMessageNotifyTime;
107
108 /**
109 * Minimum interval between notification sounds.
110 * Since a long sync (either on account setup or after a long period of being offline) can cause
111 * several notifications consecutively, it can be pretty overwhelming to get a barrage of
112 * notification sounds. Throttle them using this value.
113 */
Ben Komalo48a3a1c2011-07-22 13:29:48 -0700114 private static final long MIN_SOUND_INTERVAL_MS = 15 * 1000; // 15 seconds
Ben Komalo23a4b152011-07-22 11:41:51 -0700115
Makoto Onuki899c5b82010-09-26 16:16:21 -0700116 /** Constructor */
Todd Kennedy958b15e2011-04-28 11:09:41 -0700117 @VisibleForTesting
118 NotificationController(Context context, Clock clock) {
Makoto Onuki899c5b82010-09-26 16:16:21 -0700119 mContext = context.getApplicationContext();
120 mNotificationManager = (NotificationManager) context.getSystemService(
121 Context.NOTIFICATION_SERVICE);
122 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
Andy Stadlerc1c3b6f2010-12-15 15:26:30 -0800123 mGenericSenderIcon = BitmapFactory.decodeResource(mContext.getResources(),
124 R.drawable.ic_contact_picture);
Ben Komalof13fee52011-08-17 17:10:06 -0700125 mGenericMultipleSenderIcon = BitmapFactory.decodeResource(mContext.getResources(),
126 R.drawable.ic_notification_multiple_mail_holo_dark);
Makoto Onuki74e09482010-12-03 14:44:47 -0800127 mClock = clock;
Marc Blankaca94262011-07-19 18:19:59 -0700128 mNotificationMap = new HashMap<Long, ContentObserver>();
Makoto Onuki899c5b82010-09-26 16:16:21 -0700129 }
130
131 /** Singleton access */
132 public static synchronized NotificationController getInstance(Context context) {
133 if (sInstance == null) {
Makoto Onuki74e09482010-12-03 14:44:47 -0800134 sInstance = new NotificationController(context, Clock.INSTANCE);
Makoto Onuki899c5b82010-09-26 16:16:21 -0700135 }
136 return sInstance;
137 }
138
139 /**
Marc Blankd9b2a8f2011-07-28 13:55:10 -0700140 * Return whether or not a notification, based on the passed-in id, needs to be "ongoing"
141 * @param notificationId the notification id to check
142 * @return whether or not the notification must be "ongoing"
143 */
144 private boolean needsOngoingNotification(int notificationId) {
145 // "Security needed" must be ongoing so that the user doesn't close it; otherwise, sync will
146 // be prevented until a reboot. Consider also doing this for password expired.
147 return notificationId == NOTIFICATION_ID_SECURITY_NEEDED;
148 }
149
150 /**
Todd Kennedy958b15e2011-04-28 11:09:41 -0700151 * Returns a {@link Notification} for an event with the given account. The account contains
152 * specific rules on ring tone usage and these will be used to modify the notification
153 * behaviour.
Andy Stadler1ca111c2010-12-01 12:58:36 -0800154 *
Todd Kennedy958b15e2011-04-28 11:09:41 -0700155 * @param account The account this notification is being built for.
156 * @param ticker Text displayed when the notification is first shown. May be {@code null}.
157 * @param title The first line of text. May NOT be {@code null}.
158 * @param contentText The second line of text. May NOT be {@code null}.
159 * @param intent The intent to start if the user clicks on the notification.
160 * @param largeIcon A large icon. May be {@code null}
Todd Kennedydfdc8b62011-05-11 13:40:52 -0700161 * @param number A number to display using {@link Builder#setNumber(int)}. May
Todd Kennedy958b15e2011-04-28 11:09:41 -0700162 * be {@code null}.
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700163 * @param enableAudio If {@code false}, do not play any sound. Otherwise, play sound according
164 * to the settings for the given account.
Todd Kennedy958b15e2011-04-28 11:09:41 -0700165 * @return A {@link Notification} that can be sent to the notification service.
Andy Stadler1ca111c2010-12-01 12:58:36 -0800166 */
Todd Kennedy958b15e2011-04-28 11:09:41 -0700167 private Notification createAccountNotification(Account account, String ticker,
168 CharSequence title, String contentText, Intent intent, Bitmap largeIcon,
Marc Blankd9b2a8f2011-07-28 13:55:10 -0700169 Integer number, boolean enableAudio, boolean ongoing) {
Andy Stadler1ca111c2010-12-01 12:58:36 -0800170 // Pending Intent
Andy Stadlerc6d344a2011-02-16 16:38:18 -0800171 PendingIntent pending = null;
172 if (intent != null) {
Todd Kennedy958b15e2011-04-28 11:09:41 -0700173 pending = PendingIntent.getActivity(
174 mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
Andy Stadlerc6d344a2011-02-16 16:38:18 -0800175 }
Andy Stadler1ca111c2010-12-01 12:58:36 -0800176
Todd Kennedy958b15e2011-04-28 11:09:41 -0700177 // NOTE: the ticker is not shown for notifications in the Holo UX
178 Notification.Builder builder = new Notification.Builder(mContext)
179 .setContentTitle(title)
180 .setContentText(contentText)
181 .setContentIntent(pending)
182 .setLargeIcon(largeIcon)
183 .setNumber(number == null ? 0 : number)
184 .setSmallIcon(R.drawable.stat_notify_email_generic)
185 .setWhen(mClock.getTime())
Marc Blankd9b2a8f2011-07-28 13:55:10 -0700186 .setTicker(ticker)
187 .setOngoing(ongoing);
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700188
189 if (enableAudio) {
190 setupSoundAndVibration(builder, account);
191 }
Andy Stadler1ca111c2010-12-01 12:58:36 -0800192
Todd Kennedy958b15e2011-04-28 11:09:41 -0700193 Notification notification = builder.getNotification();
194 return notification;
195 }
Andy Stadler1ca111c2010-12-01 12:58:36 -0800196
Todd Kennedy958b15e2011-04-28 11:09:41 -0700197 /**
198 * Generic notifier for any account. Uses notification rules from account.
199 *
200 * @param account The account this notification is being built for.
201 * @param ticker Text displayed when the notification is first shown. May be {@code null}.
202 * @param title The first line of text. May NOT be {@code null}.
203 * @param contentText The second line of text. May NOT be {@code null}.
204 * @param intent The intent to start if the user clicks on the notification.
205 * @param notificationId The ID of the notification to register with the service.
206 */
207 private void showAccountNotification(Account account, String ticker, String title,
208 String contentText, Intent intent, int notificationId) {
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700209 Notification notification = createAccountNotification(account, ticker, title, contentText,
Marc Blankd9b2a8f2011-07-28 13:55:10 -0700210 intent, null, null, true, needsOngoingNotification(notificationId));
Andy Stadler1ca111c2010-12-01 12:58:36 -0800211 mNotificationManager.notify(notificationId, notification);
212 }
213
214 /**
Todd Kennedy958b15e2011-04-28 11:09:41 -0700215 * Returns a notification ID for new message notifications for the given account.
Makoto Onuki899c5b82010-09-26 16:16:21 -0700216 */
217 private int getNewMessageNotificationId(long accountId) {
Todd Kennedy958b15e2011-04-28 11:09:41 -0700218 // We assume accountId will always be less than 0x0FFFFFFF; is there a better way?
Marc Blankd3e4f3c2010-10-18 13:14:20 -0700219 return (int) (NOTIFICATION_ID_BASE_NEW_MESSAGES + accountId);
Makoto Onuki899c5b82010-09-26 16:16:21 -0700220 }
221
222 /**
Todd Kennedy83693a62011-05-10 11:36:57 -0700223 * Tells the notification controller if it should be watching for changes to the message table.
224 * This is the main life cycle method for message notifications. When we stop observing
225 * database changes, we save the state [e.g. message ID and count] of the most recent
226 * notification shown to the user. And, when we start observing database changes, we restore
227 * the saved state.
228 * @param watch If {@code true}, we register observers for all accounts whose settings have
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700229 * notifications enabled. Otherwise, all observers are unregistered.
Makoto Onuki899c5b82010-09-26 16:16:21 -0700230 */
Todd Kennedy83693a62011-05-10 11:36:57 -0700231 public void watchForMessages(final boolean watch) {
Ben Komaloeb9dcfa2011-07-21 11:02:51 -0700232 if (Email.DEBUG) {
233 Log.i(Logging.LOG_TAG, "Notifications being toggled: " + watch);
234 }
Todd Kennedy83693a62011-05-10 11:36:57 -0700235 // Don't create the thread if we're only going to stop watching
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700236 if (!watch && sNotificationThread == null) return;
Todd Kennedy83693a62011-05-10 11:36:57 -0700237
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700238 ensureHandlerExists();
Todd Kennedy83693a62011-05-10 11:36:57 -0700239 // Run this on the message notification handler
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700240 sNotificationHandler.post(new Runnable() {
Todd Kennedy83693a62011-05-10 11:36:57 -0700241 @Override
242 public void run() {
243 ContentResolver resolver = mContext.getContentResolver();
Todd Kennedy83693a62011-05-10 11:36:57 -0700244 if (!watch) {
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700245 unregisterMessageNotification(Account.ACCOUNT_ID_COMBINED_VIEW);
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700246 if (mAccountObserver != null) {
247 resolver.unregisterContentObserver(mAccountObserver);
248 mAccountObserver = null;
249 }
Todd Kennedy83693a62011-05-10 11:36:57 -0700250
251 // tear down the event loop
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700252 sNotificationThread.quit();
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700253 sNotificationThread = null;
Todd Kennedy83693a62011-05-10 11:36:57 -0700254 return;
255 }
256
257 // otherwise, start new observers for all notified accounts
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700258 registerMessageNotification(Account.ACCOUNT_ID_COMBINED_VIEW);
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700259 // If we're already observing account changes, don't do anything else
260 if (mAccountObserver == null) {
Ben Komaloeb9dcfa2011-07-21 11:02:51 -0700261 if (Email.DEBUG) {
262 Log.i(Logging.LOG_TAG, "Observing account changes for notifications");
263 }
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700264 mAccountObserver = new AccountContentObserver(sNotificationHandler, mContext);
265 resolver.registerContentObserver(Account.NOTIFIER_URI, true, mAccountObserver);
Todd Kennedy83693a62011-05-10 11:36:57 -0700266 }
Todd Kennedy83693a62011-05-10 11:36:57 -0700267 }
268 });
269 }
270
271 /**
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700272 * Temporarily suspend a single account from receiving notifications. NOTE: only a single
273 * account may ever be suspended at a time. So, if this method is invoked a second time,
274 * notifications for the previously suspended account will automatically be re-activated.
275 * @param suspend If {@code true}, suspend notifications for the given account. Otherwise,
276 * re-activate notifications for the previously suspended account.
277 * @param accountId The ID of the account. If this is the special account ID
278 * {@link Account#ACCOUNT_ID_COMBINED_VIEW}, notifications for all accounts are
279 * suspended. If {@code suspend} is {@code false}, the account ID is ignored.
280 */
281 public void suspendMessageNotification(boolean suspend, long accountId) {
Makoto Onukib36ac012011-05-17 10:50:30 -0700282 if (mSuspendAccountId != Account.NO_ACCOUNT) {
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700283 // we're already suspending an account; un-suspend it
Makoto Onukib36ac012011-05-17 10:50:30 -0700284 mSuspendAccountId = Account.NO_ACCOUNT;
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700285 }
Makoto Onukib36ac012011-05-17 10:50:30 -0700286 if (suspend && accountId != Account.NO_ACCOUNT && accountId > 0L) {
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700287 mSuspendAccountId = accountId;
288 if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
289 // Only go onto the notification handler if we really, absolutely need to
290 ensureHandlerExists();
291 sNotificationHandler.post(new Runnable() {
292 @Override
293 public void run() {
294 for (long accountId : mNotificationMap.keySet()) {
295 mNotificationManager.cancel(getNewMessageNotificationId(accountId));
296 }
297 }
298 });
299 } else {
300 mNotificationManager.cancel(getNewMessageNotificationId(accountId));
301 }
302 }
303 }
304
305 /**
306 * Ensures the notification handler exists and is ready to handle requests.
307 */
308 private static synchronized void ensureHandlerExists() {
309 if (sNotificationThread == null) {
310 sNotificationThread = new NotificationThread();
311 sNotificationHandler = new Handler(sNotificationThread.getLooper());
312 }
313 }
314
315 /**
Todd Kennedy83693a62011-05-10 11:36:57 -0700316 * Registers an observer for changes to the INBOX for the given account. Since accounts
317 * may only have a single INBOX, we will never have more than one observer for an account.
318 * NOTE: This must be called on the notification handler thread.
319 * @param accountId The ID of the account to register the observer for. May be
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700320 * {@link Account#ACCOUNT_ID_COMBINED_VIEW} to register observers for all
321 * accounts that allow for user notification.
Todd Kennedy83693a62011-05-10 11:36:57 -0700322 */
323 private void registerMessageNotification(long accountId) {
324 ContentResolver resolver = mContext.getContentResolver();
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700325 if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
Todd Kennedy83693a62011-05-10 11:36:57 -0700326 Cursor c = resolver.query(
327 Account.CONTENT_URI, EmailContent.ID_PROJECTION,
328 NOTIFIED_ACCOUNT_SELECTION, null, null);
329 try {
330 while (c.moveToNext()) {
331 long id = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
332 registerMessageNotification(id);
333 }
334 } finally {
335 c.close();
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700336 }
Makoto Onuki899c5b82010-09-26 16:16:21 -0700337 } else {
Marc Blankaca94262011-07-19 18:19:59 -0700338 ContentObserver obs = mNotificationMap.get(accountId);
339 if (obs != null) return; // we're already observing; nothing to do
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700340
Todd Kennedy83693a62011-05-10 11:36:57 -0700341 Mailbox mailbox = Mailbox.restoreMailboxOfType(mContext, accountId, Mailbox.TYPE_INBOX);
Todd Kennedydfdc8b62011-05-11 13:40:52 -0700342 if (mailbox == null) {
343 Log.w(Logging.LOG_TAG, "Could not load INBOX for account id: " + accountId);
344 return;
345 }
Ben Komaloeb9dcfa2011-07-21 11:02:51 -0700346 if (Email.DEBUG) {
347 Log.i(Logging.LOG_TAG, "Registering for notifications for account " + accountId);
348 }
Todd Kennedy83693a62011-05-10 11:36:57 -0700349 ContentObserver observer = new MessageContentObserver(
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700350 sNotificationHandler, mContext, mailbox.mId, accountId);
Todd Kennedy83693a62011-05-10 11:36:57 -0700351 resolver.registerContentObserver(Message.NOTIFIER_URI, true, observer);
Marc Blankaca94262011-07-19 18:19:59 -0700352 mNotificationMap.put(accountId, observer);
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700353 // Now, ping the observer for any initial notifications
Marc Blankaca94262011-07-19 18:19:59 -0700354 observer.onChange(true);
Makoto Onuki899c5b82010-09-26 16:16:21 -0700355 }
356 }
357
358 /**
Todd Kennedy83693a62011-05-10 11:36:57 -0700359 * Unregisters the observer for the given account. If the specified account does not have
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700360 * a registered observer, no action is performed. This will not clear any existing notification
361 * for the specified account. Use {@link NotificationManager#cancel(int)}.
Todd Kennedy83693a62011-05-10 11:36:57 -0700362 * NOTE: This must be called on the notification handler thread.
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700363 * @param accountId The ID of the account to unregister from. To unregister all accounts that
364 * have observers, specify an ID of {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
Makoto Onuki899c5b82010-09-26 16:16:21 -0700365 */
Todd Kennedy83693a62011-05-10 11:36:57 -0700366 private void unregisterMessageNotification(long accountId) {
367 ContentResolver resolver = mContext.getContentResolver();
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700368 if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
Ben Komaloeb9dcfa2011-07-21 11:02:51 -0700369 if (Email.DEBUG) {
370 Log.i(Logging.LOG_TAG, "Unregistering notifications for all accounts");
371 }
Todd Kennedy83693a62011-05-10 11:36:57 -0700372 // cancel all existing message observers
Marc Blankaca94262011-07-19 18:19:59 -0700373 for (ContentObserver observer : mNotificationMap.values()) {
Todd Kennedy83693a62011-05-10 11:36:57 -0700374 resolver.unregisterContentObserver(observer);
375 }
376 mNotificationMap.clear();
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700377 } else {
Ben Komaloeb9dcfa2011-07-21 11:02:51 -0700378 if (Email.DEBUG) {
379 Log.i(Logging.LOG_TAG, "Unregistering notifications for account " + accountId);
380 }
Marc Blankaca94262011-07-19 18:19:59 -0700381 ContentObserver observer = mNotificationMap.remove(accountId);
382 if (observer != null) {
Todd Kennedy83693a62011-05-10 11:36:57 -0700383 resolver.unregisterContentObserver(observer);
384 }
385 }
386 }
387
388 /**
Todd Kennedy958b15e2011-04-28 11:09:41 -0700389 * Returns a picture of the sender of the given message. If no picture is available, returns
390 * {@code null}.
Makoto Onuki899c5b82010-09-26 16:16:21 -0700391 *
Todd Kennedy958b15e2011-04-28 11:09:41 -0700392 * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
Makoto Onuki899c5b82010-09-26 16:16:21 -0700393 */
394 private Bitmap getSenderPhoto(Message message) {
395 Address sender = Address.unpackFirst(message.mFrom);
396 if (sender == null) {
397 return null;
398 }
399 String email = sender.getAddress();
400 if (TextUtils.isEmpty(email)) {
401 return null;
402 }
Todd Kennedy958b15e2011-04-28 11:09:41 -0700403 return ContactStatusLoader.getContactInfo(mContext, email).mPhoto;
Makoto Onuki899c5b82010-09-26 16:16:21 -0700404 }
405
Makoto Onukidd6e6b82011-03-08 16:36:47 -0800406 /**
Todd Kennedy958b15e2011-04-28 11:09:41 -0700407 * Returns a "new message" notification for the given account.
Makoto Onuki899c5b82010-09-26 16:16:21 -0700408 *
Todd Kennedy958b15e2011-04-28 11:09:41 -0700409 * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
Makoto Onuki899c5b82010-09-26 16:16:21 -0700410 */
Todd Kennedy958b15e2011-04-28 11:09:41 -0700411 @VisibleForTesting
Todd Kennedyf4378922011-05-13 08:41:48 -0700412 Notification createNewMessageNotification(long accountId, long mailboxId, long messageId,
Ben Komalo23a4b152011-07-22 11:41:51 -0700413 int unseenMessageCount, int unreadCount) {
Makoto Onuki899c5b82010-09-26 16:16:21 -0700414 final Account account = Account.restoreAccountWithId(mContext, accountId);
415 if (account == null) {
416 return null;
417 }
418 // Get the latest message
Todd Kennedy83693a62011-05-10 11:36:57 -0700419 final Message message = Message.restoreMessageWithId(mContext, messageId);
Makoto Onuki899c5b82010-09-26 16:16:21 -0700420 if (message == null) {
421 return null; // no message found???
422 }
423
Makoto Onuki43c455e2011-03-08 10:34:18 -0800424 String senderName = Address.toFriendly(Address.unpack(message.mFrom));
425 if (senderName == null) {
426 senderName = ""; // Happens when a message has no from.
427 }
Ben Komalo6f93d2e2011-06-23 17:48:25 -0700428 final boolean multipleUnseen = unseenMessageCount > 1;
Ben Komalof13fee52011-08-17 17:10:06 -0700429 final Bitmap senderPhoto = multipleUnseen
430 ? mGenericMultipleSenderIcon
431 : getSenderPhoto(message);
Ben Komalo6f93d2e2011-06-23 17:48:25 -0700432 final SpannableString title = getNewMessageTitle(senderName, unseenMessageCount);
433 // TODO: add in display name on the second line for the text, once framework supports
434 // multiline texts.
435 final String text = multipleUnseen
436 ? account.mDisplayName
437 : message.mSubject;
Todd Kennedy958b15e2011-04-28 11:09:41 -0700438 final Bitmap largeIcon = senderPhoto != null ? senderPhoto : mGenericSenderIcon;
Ben Komalo6f93d2e2011-06-23 17:48:25 -0700439 final Integer number = unreadCount > 1 ? unreadCount : null;
Todd Kennedyf4378922011-05-13 08:41:48 -0700440 final Intent intent;
Ben Komaloe9188302011-07-10 14:02:50 -0700441 if (unseenMessageCount > 1) {
Todd Kennedyf4378922011-05-13 08:41:48 -0700442 intent = Welcome.createOpenAccountInboxIntent(mContext, accountId);
Ben Komaloe9188302011-07-10 14:02:50 -0700443 } else {
444 intent = Welcome.createOpenMessageIntent(mContext, accountId, mailboxId, messageId);
Todd Kennedyf4378922011-05-13 08:41:48 -0700445 }
Makoto Onuki899c5b82010-09-26 16:16:21 -0700446
Ben Komalo23a4b152011-07-22 11:41:51 -0700447 long now = mClock.getTime();
Ben Komalo48a3a1c2011-07-22 13:29:48 -0700448 boolean enableAudio = (now - mLastMessageNotifyTime) > MIN_SOUND_INTERVAL_MS;
Ben Komalo23a4b152011-07-22 11:41:51 -0700449 Notification notification = createAccountNotification(
450 account, title.toString(), title, text,
Marc Blankd9b2a8f2011-07-28 13:55:10 -0700451 intent, largeIcon, number, enableAudio, false);
Ben Komalo23a4b152011-07-22 11:41:51 -0700452 mLastMessageNotifyTime = now;
Makoto Onuki899c5b82010-09-26 16:16:21 -0700453 return notification;
454 }
455
Makoto Onuki74e09482010-12-03 14:44:47 -0800456 /**
Ben Komalo6f93d2e2011-06-23 17:48:25 -0700457 * Creates a notification title for a new message. If there is only a single message,
458 * show the sender name. Otherwise, show "X new messages".
Makoto Onuki74e09482010-12-03 14:44:47 -0800459 */
Todd Kennedy958b15e2011-04-28 11:09:41 -0700460 @VisibleForTesting
Ben Komalo6f93d2e2011-06-23 17:48:25 -0700461 SpannableString getNewMessageTitle(String sender, int unseenCount) {
462 String title;
463 if (unseenCount > 1) {
464 title = String.format(
465 mContext.getString(R.string.notification_multiple_new_messages_fmt),
466 unseenCount);
Makoto Onuki74e09482010-12-03 14:44:47 -0800467 } else {
Ben Komalo6f93d2e2011-06-23 17:48:25 -0700468 title = sender;
Makoto Onuki74e09482010-12-03 14:44:47 -0800469 }
Ben Komalo6f93d2e2011-06-23 17:48:25 -0700470 return new SpannableString(title);
Makoto Onuki899c5b82010-09-26 16:16:21 -0700471 }
472
Todd Kennedy958b15e2011-04-28 11:09:41 -0700473 /** Returns the system's current ringer mode */
474 @VisibleForTesting
475 int getRingerMode() {
Makoto Onuki74e09482010-12-03 14:44:47 -0800476 return mAudioManager.getRingerMode();
477 }
478
Todd Kennedy958b15e2011-04-28 11:09:41 -0700479 /** Sets up the notification's sound and vibration based upon account details. */
480 @VisibleForTesting
481 void setupSoundAndVibration(Notification.Builder builder, Account account) {
Makoto Onuki899c5b82010-09-26 16:16:21 -0700482 final int flags = account.mFlags;
483 final String ringtoneUri = account.mRingtoneUri;
484 final boolean vibrate = (flags & Account.FLAGS_VIBRATE_ALWAYS) != 0;
485 final boolean vibrateWhenSilent = (flags & Account.FLAGS_VIBRATE_WHEN_SILENT) != 0;
Todd Kennedy958b15e2011-04-28 11:09:41 -0700486 final boolean isRingerSilent = getRingerMode() != AudioManager.RINGER_MODE_NORMAL;
Makoto Onuki899c5b82010-09-26 16:16:21 -0700487
Todd Kennedy958b15e2011-04-28 11:09:41 -0700488 int defaults = Notification.DEFAULT_LIGHTS;
489 if (vibrate || (vibrateWhenSilent && isRingerSilent)) {
490 defaults |= Notification.DEFAULT_VIBRATE;
Makoto Onuki899c5b82010-09-26 16:16:21 -0700491 }
492
Todd Kennedy958b15e2011-04-28 11:09:41 -0700493 builder.setSound((ringtoneUri == null) ? null : Uri.parse(ringtoneUri))
494 .setDefaults(defaults);
Makoto Onuki899c5b82010-09-26 16:16:21 -0700495 }
Marc Blankd3e4f3c2010-10-18 13:14:20 -0700496
497 /**
Todd Kennedy958b15e2011-04-28 11:09:41 -0700498 * Show (or update) a notification that the given attachment could not be forwarded. This
499 * is a very unusual case, and perhaps we shouldn't even send a notification. For now,
500 * it's helpful for debugging.
501 *
Andy Stadlerc6d344a2011-02-16 16:38:18 -0800502 * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
Marc Blankd3e4f3c2010-10-18 13:14:20 -0700503 */
Andy Stadlerc6d344a2011-02-16 16:38:18 -0800504 public void showDownloadForwardFailedNotification(Attachment attachment) {
505 final Account account = Account.restoreAccountWithId(mContext, attachment.mAccountKey);
506 if (account == null) return;
Makoto Onuki308ce922011-03-21 17:08:16 -0700507 showAccountNotification(account,
Marc Blankd3e4f3c2010-10-18 13:14:20 -0700508 mContext.getString(R.string.forward_download_failed_ticker),
Andy Stadlerc6d344a2011-02-16 16:38:18 -0800509 mContext.getString(R.string.forward_download_failed_title),
510 attachment.mFileName,
511 null,
512 NOTIFICATION_ID_ATTACHMENT_WARNING);
Marc Blankd3e4f3c2010-10-18 13:14:20 -0700513 }
514
515 /**
Todd Kennedy958b15e2011-04-28 11:09:41 -0700516 * Returns a notification ID for login failed notifications for the given account account.
Marc Blankd3e4f3c2010-10-18 13:14:20 -0700517 */
518 private int getLoginFailedNotificationId(long accountId) {
519 return NOTIFICATION_ID_BASE_LOGIN_WARNING + (int)accountId;
520 }
521
Andy Stadlerc6d344a2011-02-16 16:38:18 -0800522 /**
Todd Kennedy958b15e2011-04-28 11:09:41 -0700523 * Show (or update) a notification that there was a login failure for the given account.
524 *
Andy Stadlerc6d344a2011-02-16 16:38:18 -0800525 * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
526 */
Marc Blankd3e4f3c2010-10-18 13:14:20 -0700527 public void showLoginFailedNotification(long accountId) {
528 final Account account = Account.restoreAccountWithId(mContext, accountId);
529 if (account == null) return;
Makoto Onuki308ce922011-03-21 17:08:16 -0700530 showAccountNotification(account,
Marc Blankd3e4f3c2010-10-18 13:14:20 -0700531 mContext.getString(R.string.login_failed_ticker, account.mDisplayName),
Andy Stadlerc6d344a2011-02-16 16:38:18 -0800532 mContext.getString(R.string.login_failed_title),
533 account.getDisplayName(),
Ben Komalo28662842011-05-12 17:27:56 -0700534 AccountSettings.createAccountSettingsIntent(mContext, accountId,
Andy Stadlerf4894132011-02-18 18:23:18 -0800535 account.mDisplayName),
Andy Stadlerc6d344a2011-02-16 16:38:18 -0800536 getLoginFailedNotificationId(accountId));
Marc Blankd3e4f3c2010-10-18 13:14:20 -0700537 }
538
Todd Kennedy958b15e2011-04-28 11:09:41 -0700539 /**
540 * Cancels the login failed notification for the given account.
541 */
Marc Blankd3e4f3c2010-10-18 13:14:20 -0700542 public void cancelLoginFailedNotification(long accountId) {
543 mNotificationManager.cancel(getLoginFailedNotificationId(accountId));
544 }
Makoto Onuki308ce922011-03-21 17:08:16 -0700545
546 /**
Todd Kennedy958b15e2011-04-28 11:09:41 -0700547 * Show (or update) a notification that the user's password is expiring. The given account
548 * is used to update the display text, but, all accounts share the same notification ID.
Makoto Onuki308ce922011-03-21 17:08:16 -0700549 *
Todd Kennedy958b15e2011-04-28 11:09:41 -0700550 * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
Makoto Onuki308ce922011-03-21 17:08:16 -0700551 */
552 public void showPasswordExpiringNotification(long accountId) {
553 Account account = Account.restoreAccountWithId(mContext, accountId);
554 if (account == null) return;
Todd Kennedy958b15e2011-04-28 11:09:41 -0700555
Makoto Onuki308ce922011-03-21 17:08:16 -0700556 Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext,
557 accountId, false);
Todd Kennedy958b15e2011-04-28 11:09:41 -0700558 String accountName = account.getDisplayName();
559 String ticker =
560 mContext.getString(R.string.password_expire_warning_ticker_fmt, accountName);
561 String title = mContext.getString(R.string.password_expire_warning_content_title);
562 showAccountNotification(account, ticker, title, accountName, intent,
Makoto Onuki308ce922011-03-21 17:08:16 -0700563 NOTIFICATION_ID_PASSWORD_EXPIRING);
564 }
565
566 /**
Todd Kennedy958b15e2011-04-28 11:09:41 -0700567 * Show (or update) a notification that the user's password has expired. The given account
568 * is used to update the display text, but, all accounts share the same notification ID.
Makoto Onuki308ce922011-03-21 17:08:16 -0700569 *
Todd Kennedy958b15e2011-04-28 11:09:41 -0700570 * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
Makoto Onuki308ce922011-03-21 17:08:16 -0700571 */
572 public void showPasswordExpiredNotification(long accountId) {
573 Account account = Account.restoreAccountWithId(mContext, accountId);
574 if (account == null) return;
Todd Kennedy958b15e2011-04-28 11:09:41 -0700575
Makoto Onuki308ce922011-03-21 17:08:16 -0700576 Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext,
577 accountId, true);
Todd Kennedy958b15e2011-04-28 11:09:41 -0700578 String accountName = account.getDisplayName();
Makoto Onuki308ce922011-03-21 17:08:16 -0700579 String ticker = mContext.getString(R.string.password_expired_ticker);
Todd Kennedy958b15e2011-04-28 11:09:41 -0700580 String title = mContext.getString(R.string.password_expired_content_title);
581 showAccountNotification(account, ticker, title, accountName, intent,
582 NOTIFICATION_ID_PASSWORD_EXPIRED);
Makoto Onuki308ce922011-03-21 17:08:16 -0700583 }
584
585 /**
Todd Kennedy958b15e2011-04-28 11:09:41 -0700586 * Cancels any password expire notifications [both expired & expiring].
Makoto Onuki308ce922011-03-21 17:08:16 -0700587 */
588 public void cancelPasswordExpirationNotifications() {
Todd Kennedy83693a62011-05-10 11:36:57 -0700589 mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRING);
590 mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRED);
Makoto Onuki308ce922011-03-21 17:08:16 -0700591 }
592
593 /**
Todd Kennedy958b15e2011-04-28 11:09:41 -0700594 * Show (or update) a security needed notification. The given account is used to update
595 * the display text, but, all accounts share the same notification ID.
Makoto Onuki308ce922011-03-21 17:08:16 -0700596 */
597 public void showSecurityNeededNotification(Account account) {
Makoto Onuki308ce922011-03-21 17:08:16 -0700598 Intent intent = AccountSecurity.actionUpdateSecurityIntent(mContext, account.mId, true);
Todd Kennedy958b15e2011-04-28 11:09:41 -0700599 String accountName = account.getDisplayName();
600 String ticker =
601 mContext.getString(R.string.security_notification_ticker_fmt, accountName);
602 String title = mContext.getString(R.string.security_notification_content_title);
603 showAccountNotification(account, ticker, title, accountName, intent,
Makoto Onuki308ce922011-03-21 17:08:16 -0700604 NOTIFICATION_ID_SECURITY_NEEDED);
605 }
606
607 /**
Todd Kennedy958b15e2011-04-28 11:09:41 -0700608 * Cancels the security needed notification.
Makoto Onuki308ce922011-03-21 17:08:16 -0700609 */
610 public void cancelSecurityNeededNotification() {
Todd Kennedy83693a62011-05-10 11:36:57 -0700611 mNotificationManager.cancel(NOTIFICATION_ID_SECURITY_NEEDED);
Makoto Onuki308ce922011-03-21 17:08:16 -0700612 }
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700613
614 /**
615 * Observer invoked whenever a message we're notifying the user about changes.
616 */
617 private static class MessageContentObserver extends ContentObserver {
Todd Kennedy83693a62011-05-10 11:36:57 -0700618 /** A selection to get messages the user hasn't seen before */
619 private final static String MESSAGE_SELECTION =
Ben Komalo48a3a1c2011-07-22 13:29:48 -0700620 MessageColumns.MAILBOX_KEY + "=? AND "
621 + MessageColumns.ID + ">? AND "
622 + MessageColumns.FLAG_READ + "=0 AND "
623 + Message.FLAG_LOADED_SELECTION;
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700624 private final Context mContext;
Todd Kennedy83693a62011-05-10 11:36:57 -0700625 private final long mMailboxId;
626 private final long mAccountId;
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700627
Todd Kennedy83693a62011-05-10 11:36:57 -0700628 public MessageContentObserver(
629 Handler handler, Context context, long mailboxId, long accountId) {
630 super(handler);
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700631 mContext = context;
Todd Kennedy83693a62011-05-10 11:36:57 -0700632 mMailboxId = mailboxId;
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700633 mAccountId = accountId;
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700634 }
635
636 @Override
637 public void onChange(boolean selfChange) {
Todd Kennedy5701e0a2011-05-12 10:13:45 -0700638 if (mAccountId == sInstance.mSuspendAccountId
639 || sInstance.mSuspendAccountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
640 return;
641 }
Todd Kennedy83693a62011-05-10 11:36:57 -0700642
Marc Blankaca94262011-07-19 18:19:59 -0700643 ContentObserver observer = sInstance.mNotificationMap.get(mAccountId);
644 if (observer == null) {
Ben Komaloeb9dcfa2011-07-21 11:02:51 -0700645 // Notification for a mailbox that we aren't observing; account is probably
646 // being deleted.
647 Log.w(Logging.LOG_TAG, "Received notification when observer data was null");
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700648 return;
649 }
Marc Blankaca94262011-07-19 18:19:59 -0700650 Account account = Account.restoreAccountWithId(mContext, mAccountId);
Ben Komaloeb9dcfa2011-07-21 11:02:51 -0700651 if (account == null) {
652 Log.w(Logging.LOG_TAG, "Couldn't find account for changed message notification");
653 return;
654 }
Marc Blankaca94262011-07-19 18:19:59 -0700655 long oldMessageId = account.mNotifiedMessageId;
656 int oldMessageCount = account.mNotifiedMessageCount;
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700657
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700658 ContentResolver resolver = mContext.getContentResolver();
Ben Komaloeb9dcfa2011-07-21 11:02:51 -0700659 Long lastSeenMessageId = Utility.getFirstRowLong(
Marc Blank6e418aa2011-06-18 18:03:11 -0700660 mContext, ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailboxId),
Todd Kennedy83693a62011-05-10 11:36:57 -0700661 new String[] { MailboxColumns.LAST_SEEN_MESSAGE_KEY },
Ben Komaloeb9dcfa2011-07-21 11:02:51 -0700662 null, null, null, 0);
663 if (lastSeenMessageId == null) {
664 // Mailbox got nuked. Could be that the account is in the process of being deleted
665 Log.w(Logging.LOG_TAG, "Couldn't find mailbox for changed message notification");
666 return;
667 }
668
Todd Kennedy83693a62011-05-10 11:36:57 -0700669 Cursor c = resolver.query(
670 Message.CONTENT_URI, EmailContent.ID_PROJECTION,
671 MESSAGE_SELECTION,
672 new String[] { Long.toString(mMailboxId), Long.toString(lastSeenMessageId) },
673 MessageColumns.ID + " DESC");
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700674 if (c == null) {
Ben Komaloeb9dcfa2011-07-21 11:02:51 -0700675 // Couldn't find message info - things may be getting deleted in bulk.
676 Log.w(Logging.LOG_TAG, "#onChange(); NULL response for message id query");
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700677 return;
678 }
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700679 try {
Todd Kennedy83693a62011-05-10 11:36:57 -0700680 int newMessageCount = c.getCount();
681 long newMessageId = 0L;
682 if (c.moveToNext()) {
683 newMessageId = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700684 }
Todd Kennedy83693a62011-05-10 11:36:57 -0700685
686 if (newMessageCount == 0) {
687 // No messages to notify for; clear the notification
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700688 int notificationId = sInstance.getNewMessageNotificationId(mAccountId);
689 sInstance.mNotificationManager.cancel(notificationId);
Todd Kennedy83693a62011-05-10 11:36:57 -0700690 } else if (newMessageCount != oldMessageCount
691 || (newMessageId != 0 && newMessageId != oldMessageId)) {
692 // Either the count or last message has changed; update the notification
Ben Komaloeb9dcfa2011-07-21 11:02:51 -0700693 Integer unreadCount = Utility.getFirstRowInt(
Ben Komalo6f93d2e2011-06-23 17:48:25 -0700694 mContext, ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailboxId),
695 new String[] { MailboxColumns.UNREAD_COUNT },
Ben Komaloeb9dcfa2011-07-21 11:02:51 -0700696 null, null, null, 0);
697 if (unreadCount == null) {
698 Log.w(Logging.LOG_TAG, "Couldn't find unread count for mailbox");
699 return;
700 }
Ben Komalo6f93d2e2011-06-23 17:48:25 -0700701
Todd Kennedy83693a62011-05-10 11:36:57 -0700702 Notification n = sInstance.createNewMessageNotification(
Ben Komalo6f93d2e2011-06-23 17:48:25 -0700703 mAccountId, mMailboxId, newMessageId,
Ben Komalo23a4b152011-07-22 11:41:51 -0700704 newMessageCount, unreadCount);
Todd Kennedy83693a62011-05-10 11:36:57 -0700705 if (n != null) {
706 // Make the notification visible
707 sInstance.mNotificationManager.notify(
708 sInstance.getNewMessageNotificationId(mAccountId), n);
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700709 }
Todd Kennedy83693a62011-05-10 11:36:57 -0700710 }
Marc Blankaca94262011-07-19 18:19:59 -0700711 // Save away the new values
712 ContentValues cv = new ContentValues();
713 cv.put(AccountColumns.NOTIFIED_MESSAGE_ID, newMessageId);
714 cv.put(AccountColumns.NOTIFIED_MESSAGE_COUNT, newMessageCount);
715 resolver.update(ContentUris.withAppendedId(Account.CONTENT_URI, mAccountId), cv,
716 null, null);
Todd Kennedy83693a62011-05-10 11:36:57 -0700717 } finally {
718 c.close();
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700719 }
720 }
721 }
722
Todd Kennedye7fb4ac2011-05-11 15:29:24 -0700723 /**
724 * Observer invoked whenever an account is modified. This could mean the user changed the
725 * notification settings.
726 */
727 private static class AccountContentObserver extends ContentObserver {
728 private final Context mContext;
729 public AccountContentObserver(Handler handler, Context context) {
730 super(handler);
731 mContext = context;
732 }
733
734 @Override
735 public void onChange(boolean selfChange) {
736 final ContentResolver resolver = mContext.getContentResolver();
737 final Cursor c = resolver.query(
738 Account.CONTENT_URI, EmailContent.ID_PROJECTION,
739 NOTIFIED_ACCOUNT_SELECTION, null, null);
740 final HashSet<Long> newAccountList = new HashSet<Long>();
741 final HashSet<Long> removedAccountList = new HashSet<Long>();
742 if (c == null) {
743 // Suspender time ... theoretically, this will never happen
744 Log.wtf(Logging.LOG_TAG, "#onChange(); NULL response for account id query");
745 return;
746 }
747 try {
748 while (c.moveToNext()) {
749 long accountId = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
750 newAccountList.add(accountId);
751 }
752 } finally {
753 if (c != null) {
754 c.close();
755 }
756 }
757 // NOTE: Looping over three lists is not necessarily the most efficient. However, the
758 // account lists are going to be very small, so, this will not be necessarily bad.
759 // Cycle through existing notification list and adjust as necessary
760 for (long accountId : sInstance.mNotificationMap.keySet()) {
761 if (!newAccountList.remove(accountId)) {
762 // account id not in the current set of notifiable accounts
763 removedAccountList.add(accountId);
764 }
765 }
766 // A new account was added to the notification list
767 for (long accountId : newAccountList) {
768 sInstance.registerMessageNotification(accountId);
769 }
770 // An account was removed from the notification list
771 for (long accountId : removedAccountList) {
772 sInstance.unregisterMessageNotification(accountId);
773 int notificationId = sInstance.getNewMessageNotificationId(accountId);
774 sInstance.mNotificationManager.cancel(notificationId);
775 }
776 }
777 }
778
Todd Kennedy83693a62011-05-10 11:36:57 -0700779 /**
780 * Thread to handle all notification actions through its own {@link Looper}.
781 */
782 private static class NotificationThread implements Runnable {
783 /** Lock to ensure proper initialization */
784 private final Object mLock = new Object();
785 /** The {@link Looper} that handles messages for this thread */
786 private Looper mLooper;
787
788 NotificationThread() {
789 new Thread(null, this, "EmailNotification").start();
790 synchronized (mLock) {
791 while (mLooper == null) {
792 try {
793 mLock.wait();
794 } catch (InterruptedException ex) {
795 }
796 }
797 }
798 }
799
800 @Override
801 public void run() {
802 synchronized (mLock) {
803 Looper.prepare();
804 mLooper = Looper.myLooper();
805 mLock.notifyAll();
806 }
807 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
808 Looper.loop();
809 }
810 void quit() {
811 mLooper.quit();
812 }
813 Looper getLooper() {
814 return mLooper;
815 }
Todd Kennedyc4cdb112011-05-03 14:42:26 -0700816 }
Makoto Onuki899c5b82010-09-26 16:16:21 -0700817}