2010-09-26 23:16:21 +00:00
|
|
|
/*
|
|
|
|
* Copyright (C) 2010 The Android Open Source Project
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
package com.android.email;
|
|
|
|
|
|
|
|
import android.app.Notification;
|
2011-05-11 20:40:52 +00:00
|
|
|
import android.app.Notification.Builder;
|
2010-09-26 23:16:21 +00:00
|
|
|
import android.app.NotificationManager;
|
|
|
|
import android.app.PendingIntent;
|
2011-05-03 21:42:26 +00:00
|
|
|
import android.content.ContentResolver;
|
Improve EmailContent caching...
* Guarantee that up to 16 Account (with HostAuths), and Policy rows
are always cached. Also, 6 commonly used Mailboxes per Account
(Inbox, Outbox, Drafts, Sent, Trash, and Search)
* Precache these rows when EmailProvider starts up
* Ensure that newly added, precachable rows are cached when created
* Clean up some inefficient/wrong caching code
* Fix a commonly called method in NotificationManager in which we
load a single Mailbox row using selection vs withAppendedId
* Confirm that we don't read from the database in typical use and
heavy message loading
* Add a special URI for finding mailbox by type (using the cache)
* Add special-case code for EmailContent.count(Account.CONTENT_URI)
which is used in a number of places (including on the UI thread)
and whose value is easily determined
* Add a special URI to get the default account id
* Confirm that all unit tests work
The goal here is to be able to load all Account, HostAuth, Policy,
and Mailbox objects (by id) without worrying about disk access.
There will still be a single disk read for uncommon Mailbox reads,
but this should be considered acceptable.
Change-Id: Ibc9aa7acc73185e360b0b6f3053b90a985e97210
TODO: Unit tests
2011-06-19 01:03:11 +00:00
|
|
|
import android.content.ContentUris;
|
2011-07-20 01:19:59 +00:00
|
|
|
import android.content.ContentValues;
|
2010-09-26 23:16:21 +00:00
|
|
|
import android.content.Context;
|
2010-10-18 20:14:20 +00:00
|
|
|
import android.content.Intent;
|
2011-05-03 21:42:26 +00:00
|
|
|
import android.database.ContentObserver;
|
2011-05-10 18:36:57 +00:00
|
|
|
import android.database.Cursor;
|
2010-09-26 23:16:21 +00:00
|
|
|
import android.graphics.Bitmap;
|
2010-12-03 22:44:47 +00:00
|
|
|
import android.graphics.BitmapFactory;
|
2010-09-26 23:16:21 +00:00
|
|
|
import android.media.AudioManager;
|
|
|
|
import android.net.Uri;
|
2011-05-03 21:42:26 +00:00
|
|
|
import android.os.Handler;
|
2011-05-10 18:36:57 +00:00
|
|
|
import android.os.Looper;
|
|
|
|
import android.os.Process;
|
2010-12-03 22:44:47 +00:00
|
|
|
import android.text.SpannableString;
|
2010-09-26 23:16:21 +00:00
|
|
|
import android.text.TextUtils;
|
2011-05-10 18:36:57 +00:00
|
|
|
import android.util.Log;
|
2010-09-26 23:16:21 +00:00
|
|
|
|
Improve EmailContent caching...
* Guarantee that up to 16 Account (with HostAuths), and Policy rows
are always cached. Also, 6 commonly used Mailboxes per Account
(Inbox, Outbox, Drafts, Sent, Trash, and Search)
* Precache these rows when EmailProvider starts up
* Ensure that newly added, precachable rows are cached when created
* Clean up some inefficient/wrong caching code
* Fix a commonly called method in NotificationManager in which we
load a single Mailbox row using selection vs withAppendedId
* Confirm that we don't read from the database in typical use and
heavy message loading
* Add a special URI for finding mailbox by type (using the cache)
* Add special-case code for EmailContent.count(Account.CONTENT_URI)
which is used in a number of places (including on the UI thread)
and whose value is easily determined
* Add a special URI to get the default account id
* Confirm that all unit tests work
The goal here is to be able to load all Account, HostAuth, Policy,
and Mailbox objects (by id) without worrying about disk access.
There will still be a single disk read for uncommon Mailbox reads,
but this should be considered acceptable.
Change-Id: Ibc9aa7acc73185e360b0b6f3053b90a985e97210
TODO: Unit tests
2011-06-19 01:03:11 +00:00
|
|
|
import com.android.email.activity.ContactStatusLoader;
|
|
|
|
import com.android.email.activity.Welcome;
|
|
|
|
import com.android.email.activity.setup.AccountSecurity;
|
|
|
|
import com.android.email.activity.setup.AccountSettings;
|
|
|
|
import com.android.emailcommon.Logging;
|
|
|
|
import com.android.emailcommon.mail.Address;
|
|
|
|
import com.android.emailcommon.provider.Account;
|
|
|
|
import com.android.emailcommon.provider.EmailContent;
|
2011-07-20 01:19:59 +00:00
|
|
|
import com.android.emailcommon.provider.EmailContent.AccountColumns;
|
Improve EmailContent caching...
* Guarantee that up to 16 Account (with HostAuths), and Policy rows
are always cached. Also, 6 commonly used Mailboxes per Account
(Inbox, Outbox, Drafts, Sent, Trash, and Search)
* Precache these rows when EmailProvider starts up
* Ensure that newly added, precachable rows are cached when created
* Clean up some inefficient/wrong caching code
* Fix a commonly called method in NotificationManager in which we
load a single Mailbox row using selection vs withAppendedId
* Confirm that we don't read from the database in typical use and
heavy message loading
* Add a special URI for finding mailbox by type (using the cache)
* Add special-case code for EmailContent.count(Account.CONTENT_URI)
which is used in a number of places (including on the UI thread)
and whose value is easily determined
* Add a special URI to get the default account id
* Confirm that all unit tests work
The goal here is to be able to load all Account, HostAuth, Policy,
and Mailbox objects (by id) without worrying about disk access.
There will still be a single disk read for uncommon Mailbox reads,
but this should be considered acceptable.
Change-Id: Ibc9aa7acc73185e360b0b6f3053b90a985e97210
TODO: Unit tests
2011-06-19 01:03:11 +00:00
|
|
|
import com.android.emailcommon.provider.EmailContent.Attachment;
|
|
|
|
import com.android.emailcommon.provider.EmailContent.MailboxColumns;
|
|
|
|
import com.android.emailcommon.provider.EmailContent.Message;
|
|
|
|
import com.android.emailcommon.provider.EmailContent.MessageColumns;
|
|
|
|
import com.android.emailcommon.provider.Mailbox;
|
|
|
|
import com.android.emailcommon.utility.Utility;
|
|
|
|
import com.google.common.annotations.VisibleForTesting;
|
|
|
|
|
2011-05-03 21:42:26 +00:00
|
|
|
import java.util.HashMap;
|
2011-05-11 22:29:24 +00:00
|
|
|
import java.util.HashSet;
|
2011-05-03 21:42:26 +00:00
|
|
|
|
2010-09-26 23:16:21 +00:00
|
|
|
/**
|
|
|
|
* Class that manages notifications.
|
|
|
|
*/
|
|
|
|
public class NotificationController {
|
2011-03-22 00:08:16 +00:00
|
|
|
private static final int NOTIFICATION_ID_SECURITY_NEEDED = 1;
|
2011-04-28 18:09:41 +00:00
|
|
|
/** Reserved for {@link com.android.exchange.CalendarSyncEnabler} */
|
|
|
|
@SuppressWarnings("unused")
|
|
|
|
private static final int NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED = 2;
|
2011-03-22 00:08:16 +00:00
|
|
|
private static final int NOTIFICATION_ID_ATTACHMENT_WARNING = 3;
|
|
|
|
private static final int NOTIFICATION_ID_PASSWORD_EXPIRING = 4;
|
|
|
|
private static final int NOTIFICATION_ID_PASSWORD_EXPIRED = 5;
|
2010-10-18 20:14:20 +00:00
|
|
|
|
|
|
|
private static final int NOTIFICATION_ID_BASE_NEW_MESSAGES = 0x10000000;
|
|
|
|
private static final int NOTIFICATION_ID_BASE_LOGIN_WARNING = 0x20000000;
|
2010-09-26 23:16:21 +00:00
|
|
|
|
2011-05-10 18:36:57 +00:00
|
|
|
/** Selection to retrieve accounts that should we notify user for changes */
|
|
|
|
private final static String NOTIFIED_ACCOUNT_SELECTION =
|
|
|
|
Account.FLAGS + "&" + Account.FLAGS_NOTIFY_NEW_MAIL + " != 0";
|
|
|
|
|
2011-05-11 22:29:24 +00:00
|
|
|
private static NotificationThread sNotificationThread;
|
|
|
|
private static Handler sNotificationHandler;
|
2010-09-26 23:16:21 +00:00
|
|
|
private static NotificationController sInstance;
|
|
|
|
private final Context mContext;
|
|
|
|
private final NotificationManager mNotificationManager;
|
|
|
|
private final AudioManager mAudioManager;
|
2010-12-15 23:26:30 +00:00
|
|
|
private final Bitmap mGenericSenderIcon;
|
2010-12-03 22:44:47 +00:00
|
|
|
private final Clock mClock;
|
2011-05-10 18:36:57 +00:00
|
|
|
// TODO We're maintaining all of our structures based upon the account ID. This is fine
|
|
|
|
// for now since the assumption is that we only ever look for changes in an account's
|
|
|
|
// INBOX. We should adjust our logic to use the mailbox ID instead.
|
2011-05-03 21:42:26 +00:00
|
|
|
/** Maps account id to the message data */
|
2011-07-20 01:19:59 +00:00
|
|
|
private final HashMap<Long, ContentObserver> mNotificationMap;
|
2011-05-11 22:29:24 +00:00
|
|
|
private ContentObserver mAccountObserver;
|
2011-05-12 17:13:45 +00:00
|
|
|
/**
|
2011-05-17 17:50:30 +00:00
|
|
|
* Suspend notifications for this account. If {@link Account#NO_ACCOUNT}, no
|
2011-05-12 17:13:45 +00:00
|
|
|
* account notifications are suspended. If {@link Account#ACCOUNT_ID_COMBINED_VIEW},
|
|
|
|
* notifications for all accounts are suspended.
|
|
|
|
*/
|
2011-05-17 17:50:30 +00:00
|
|
|
private long mSuspendAccountId = Account.NO_ACCOUNT;
|
2010-09-26 23:16:21 +00:00
|
|
|
|
|
|
|
/** Constructor */
|
2011-04-28 18:09:41 +00:00
|
|
|
@VisibleForTesting
|
|
|
|
NotificationController(Context context, Clock clock) {
|
2010-09-26 23:16:21 +00:00
|
|
|
mContext = context.getApplicationContext();
|
|
|
|
mNotificationManager = (NotificationManager) context.getSystemService(
|
|
|
|
Context.NOTIFICATION_SERVICE);
|
|
|
|
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
2010-12-15 23:26:30 +00:00
|
|
|
mGenericSenderIcon = BitmapFactory.decodeResource(mContext.getResources(),
|
|
|
|
R.drawable.ic_contact_picture);
|
2010-12-03 22:44:47 +00:00
|
|
|
mClock = clock;
|
2011-07-20 01:19:59 +00:00
|
|
|
mNotificationMap = new HashMap<Long, ContentObserver>();
|
2010-09-26 23:16:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/** Singleton access */
|
|
|
|
public static synchronized NotificationController getInstance(Context context) {
|
|
|
|
if (sInstance == null) {
|
2010-12-03 22:44:47 +00:00
|
|
|
sInstance = new NotificationController(context, Clock.INSTANCE);
|
2010-09-26 23:16:21 +00:00
|
|
|
}
|
|
|
|
return sInstance;
|
|
|
|
}
|
|
|
|
|
2010-12-01 20:58:36 +00:00
|
|
|
/**
|
2011-04-28 18:09:41 +00:00
|
|
|
* Returns a {@link Notification} for an event with the given account. The account contains
|
|
|
|
* specific rules on ring tone usage and these will be used to modify the notification
|
|
|
|
* behaviour.
|
2010-12-01 20:58:36 +00:00
|
|
|
*
|
2011-04-28 18:09:41 +00:00
|
|
|
* @param account The account this notification is being built for.
|
|
|
|
* @param ticker Text displayed when the notification is first shown. May be {@code null}.
|
|
|
|
* @param title The first line of text. May NOT be {@code null}.
|
|
|
|
* @param contentText The second line of text. May NOT be {@code null}.
|
|
|
|
* @param intent The intent to start if the user clicks on the notification.
|
|
|
|
* @param largeIcon A large icon. May be {@code null}
|
2011-05-11 20:40:52 +00:00
|
|
|
* @param number A number to display using {@link Builder#setNumber(int)}. May
|
2011-04-28 18:09:41 +00:00
|
|
|
* be {@code null}.
|
2011-05-03 21:42:26 +00:00
|
|
|
* @param enableAudio If {@code false}, do not play any sound. Otherwise, play sound according
|
|
|
|
* to the settings for the given account.
|
2011-04-28 18:09:41 +00:00
|
|
|
* @return A {@link Notification} that can be sent to the notification service.
|
2010-12-01 20:58:36 +00:00
|
|
|
*/
|
2011-04-28 18:09:41 +00:00
|
|
|
private Notification createAccountNotification(Account account, String ticker,
|
|
|
|
CharSequence title, String contentText, Intent intent, Bitmap largeIcon,
|
2011-05-03 21:42:26 +00:00
|
|
|
Integer number, boolean enableAudio) {
|
2010-12-01 20:58:36 +00:00
|
|
|
// Pending Intent
|
2011-02-17 00:38:18 +00:00
|
|
|
PendingIntent pending = null;
|
|
|
|
if (intent != null) {
|
2011-04-28 18:09:41 +00:00
|
|
|
pending = PendingIntent.getActivity(
|
|
|
|
mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
2011-02-17 00:38:18 +00:00
|
|
|
}
|
2010-12-01 20:58:36 +00:00
|
|
|
|
2011-04-28 18:09:41 +00:00
|
|
|
// NOTE: the ticker is not shown for notifications in the Holo UX
|
|
|
|
Notification.Builder builder = new Notification.Builder(mContext)
|
|
|
|
.setContentTitle(title)
|
|
|
|
.setContentText(contentText)
|
|
|
|
.setContentIntent(pending)
|
|
|
|
.setLargeIcon(largeIcon)
|
|
|
|
.setNumber(number == null ? 0 : number)
|
|
|
|
.setSmallIcon(R.drawable.stat_notify_email_generic)
|
|
|
|
.setWhen(mClock.getTime())
|
|
|
|
.setTicker(ticker);
|
2011-05-03 21:42:26 +00:00
|
|
|
|
|
|
|
if (enableAudio) {
|
|
|
|
setupSoundAndVibration(builder, account);
|
|
|
|
}
|
2010-12-01 20:58:36 +00:00
|
|
|
|
2011-04-28 18:09:41 +00:00
|
|
|
Notification notification = builder.getNotification();
|
|
|
|
return notification;
|
|
|
|
}
|
2010-12-01 20:58:36 +00:00
|
|
|
|
2011-04-28 18:09:41 +00:00
|
|
|
/**
|
|
|
|
* Generic notifier for any account. Uses notification rules from account.
|
|
|
|
*
|
|
|
|
* @param account The account this notification is being built for.
|
|
|
|
* @param ticker Text displayed when the notification is first shown. May be {@code null}.
|
|
|
|
* @param title The first line of text. May NOT be {@code null}.
|
|
|
|
* @param contentText The second line of text. May NOT be {@code null}.
|
|
|
|
* @param intent The intent to start if the user clicks on the notification.
|
|
|
|
* @param notificationId The ID of the notification to register with the service.
|
|
|
|
*/
|
|
|
|
private void showAccountNotification(Account account, String ticker, String title,
|
|
|
|
String contentText, Intent intent, int notificationId) {
|
2011-05-03 21:42:26 +00:00
|
|
|
Notification notification = createAccountNotification(account, ticker, title, contentText,
|
|
|
|
intent, null, null, true);
|
2010-12-01 20:58:36 +00:00
|
|
|
mNotificationManager.notify(notificationId, notification);
|
|
|
|
}
|
|
|
|
|
2010-09-26 23:16:21 +00:00
|
|
|
/**
|
2011-04-28 18:09:41 +00:00
|
|
|
* Returns a notification ID for new message notifications for the given account.
|
2010-09-26 23:16:21 +00:00
|
|
|
*/
|
|
|
|
private int getNewMessageNotificationId(long accountId) {
|
2011-04-28 18:09:41 +00:00
|
|
|
// We assume accountId will always be less than 0x0FFFFFFF; is there a better way?
|
2010-10-18 20:14:20 +00:00
|
|
|
return (int) (NOTIFICATION_ID_BASE_NEW_MESSAGES + accountId);
|
2010-09-26 23:16:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2011-05-10 18:36:57 +00:00
|
|
|
* Tells the notification controller if it should be watching for changes to the message table.
|
|
|
|
* This is the main life cycle method for message notifications. When we stop observing
|
|
|
|
* database changes, we save the state [e.g. message ID and count] of the most recent
|
|
|
|
* notification shown to the user. And, when we start observing database changes, we restore
|
|
|
|
* the saved state.
|
|
|
|
* @param watch If {@code true}, we register observers for all accounts whose settings have
|
2011-05-12 17:13:45 +00:00
|
|
|
* notifications enabled. Otherwise, all observers are unregistered.
|
2010-09-26 23:16:21 +00:00
|
|
|
*/
|
2011-05-10 18:36:57 +00:00
|
|
|
public void watchForMessages(final boolean watch) {
|
|
|
|
// Don't create the thread if we're only going to stop watching
|
2011-05-12 17:13:45 +00:00
|
|
|
if (!watch && sNotificationThread == null) return;
|
2011-05-10 18:36:57 +00:00
|
|
|
|
2011-05-12 17:13:45 +00:00
|
|
|
ensureHandlerExists();
|
2011-05-10 18:36:57 +00:00
|
|
|
// Run this on the message notification handler
|
2011-05-11 22:29:24 +00:00
|
|
|
sNotificationHandler.post(new Runnable() {
|
2011-05-10 18:36:57 +00:00
|
|
|
@Override
|
|
|
|
public void run() {
|
|
|
|
ContentResolver resolver = mContext.getContentResolver();
|
|
|
|
if (!watch) {
|
2011-05-12 17:13:45 +00:00
|
|
|
unregisterMessageNotification(Account.ACCOUNT_ID_COMBINED_VIEW);
|
2011-05-11 22:29:24 +00:00
|
|
|
if (mAccountObserver != null) {
|
|
|
|
resolver.unregisterContentObserver(mAccountObserver);
|
|
|
|
mAccountObserver = null;
|
|
|
|
}
|
2011-05-10 18:36:57 +00:00
|
|
|
|
|
|
|
// tear down the event loop
|
2011-05-11 22:29:24 +00:00
|
|
|
sNotificationThread.quit();
|
|
|
|
sNotificationThread = null;
|
2011-05-10 18:36:57 +00:00
|
|
|
return;
|
2011-05-03 21:42:26 +00:00
|
|
|
}
|
2011-05-10 18:36:57 +00:00
|
|
|
|
|
|
|
// otherwise, start new observers for all notified accounts
|
2011-05-12 17:13:45 +00:00
|
|
|
registerMessageNotification(Account.ACCOUNT_ID_COMBINED_VIEW);
|
2011-05-11 22:29:24 +00:00
|
|
|
// If we're already observing account changes, don't do anything else
|
|
|
|
if (mAccountObserver == null) {
|
|
|
|
mAccountObserver = new AccountContentObserver(sNotificationHandler, mContext);
|
|
|
|
resolver.registerContentObserver(Account.NOTIFIER_URI, true, mAccountObserver);
|
2011-05-10 18:36:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2010-09-26 23:16:21 +00:00
|
|
|
}
|
|
|
|
|
2011-05-12 17:13:45 +00:00
|
|
|
/**
|
|
|
|
* Temporarily suspend a single account from receiving notifications. NOTE: only a single
|
|
|
|
* account may ever be suspended at a time. So, if this method is invoked a second time,
|
|
|
|
* notifications for the previously suspended account will automatically be re-activated.
|
|
|
|
* @param suspend If {@code true}, suspend notifications for the given account. Otherwise,
|
|
|
|
* re-activate notifications for the previously suspended account.
|
|
|
|
* @param accountId The ID of the account. If this is the special account ID
|
|
|
|
* {@link Account#ACCOUNT_ID_COMBINED_VIEW}, notifications for all accounts are
|
|
|
|
* suspended. If {@code suspend} is {@code false}, the account ID is ignored.
|
|
|
|
*/
|
|
|
|
public void suspendMessageNotification(boolean suspend, long accountId) {
|
2011-05-17 17:50:30 +00:00
|
|
|
if (mSuspendAccountId != Account.NO_ACCOUNT) {
|
2011-05-12 17:13:45 +00:00
|
|
|
// we're already suspending an account; un-suspend it
|
2011-05-17 17:50:30 +00:00
|
|
|
mSuspendAccountId = Account.NO_ACCOUNT;
|
2011-05-12 17:13:45 +00:00
|
|
|
}
|
2011-05-17 17:50:30 +00:00
|
|
|
if (suspend && accountId != Account.NO_ACCOUNT && accountId > 0L) {
|
2011-05-12 17:13:45 +00:00
|
|
|
mSuspendAccountId = accountId;
|
|
|
|
if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
|
|
|
|
// Only go onto the notification handler if we really, absolutely need to
|
|
|
|
ensureHandlerExists();
|
|
|
|
sNotificationHandler.post(new Runnable() {
|
|
|
|
@Override
|
|
|
|
public void run() {
|
|
|
|
for (long accountId : mNotificationMap.keySet()) {
|
|
|
|
mNotificationManager.cancel(getNewMessageNotificationId(accountId));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
mNotificationManager.cancel(getNewMessageNotificationId(accountId));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Ensures the notification handler exists and is ready to handle requests.
|
|
|
|
*/
|
|
|
|
private static synchronized void ensureHandlerExists() {
|
|
|
|
if (sNotificationThread == null) {
|
|
|
|
sNotificationThread = new NotificationThread();
|
|
|
|
sNotificationHandler = new Handler(sNotificationThread.getLooper());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-09-26 23:16:21 +00:00
|
|
|
/**
|
2011-05-10 18:36:57 +00:00
|
|
|
* Registers an observer for changes to the INBOX for the given account. Since accounts
|
|
|
|
* may only have a single INBOX, we will never have more than one observer for an account.
|
|
|
|
* NOTE: This must be called on the notification handler thread.
|
|
|
|
* @param accountId The ID of the account to register the observer for. May be
|
2011-05-12 17:13:45 +00:00
|
|
|
* {@link Account#ACCOUNT_ID_COMBINED_VIEW} to register observers for all
|
|
|
|
* accounts that allow for user notification.
|
2010-09-26 23:16:21 +00:00
|
|
|
*/
|
2011-05-10 18:36:57 +00:00
|
|
|
private void registerMessageNotification(long accountId) {
|
|
|
|
ContentResolver resolver = mContext.getContentResolver();
|
2011-05-12 17:13:45 +00:00
|
|
|
if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
|
2011-05-10 18:36:57 +00:00
|
|
|
Cursor c = resolver.query(
|
|
|
|
Account.CONTENT_URI, EmailContent.ID_PROJECTION,
|
|
|
|
NOTIFIED_ACCOUNT_SELECTION, null, null);
|
|
|
|
try {
|
|
|
|
while (c.moveToNext()) {
|
|
|
|
long id = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
|
|
|
|
registerMessageNotification(id);
|
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
c.close();
|
|
|
|
}
|
|
|
|
} else {
|
2011-07-20 01:19:59 +00:00
|
|
|
ContentObserver obs = mNotificationMap.get(accountId);
|
|
|
|
if (obs != null) return; // we're already observing; nothing to do
|
2011-05-10 18:36:57 +00:00
|
|
|
|
|
|
|
Mailbox mailbox = Mailbox.restoreMailboxOfType(mContext, accountId, Mailbox.TYPE_INBOX);
|
2011-05-11 20:40:52 +00:00
|
|
|
if (mailbox == null) {
|
|
|
|
Log.w(Logging.LOG_TAG, "Could not load INBOX for account id: " + accountId);
|
|
|
|
return;
|
|
|
|
}
|
2011-05-10 18:36:57 +00:00
|
|
|
ContentObserver observer = new MessageContentObserver(
|
2011-05-11 22:29:24 +00:00
|
|
|
sNotificationHandler, mContext, mailbox.mId, accountId);
|
2011-05-10 18:36:57 +00:00
|
|
|
resolver.registerContentObserver(Message.NOTIFIER_URI, true, observer);
|
2011-07-20 01:19:59 +00:00
|
|
|
mNotificationMap.put(accountId, observer);
|
2011-05-11 22:29:24 +00:00
|
|
|
// Now, ping the observer for any initial notifications
|
2011-07-20 01:19:59 +00:00
|
|
|
observer.onChange(true);
|
2011-05-03 21:42:26 +00:00
|
|
|
}
|
2011-05-10 18:36:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Unregisters the observer for the given account. If the specified account does not have
|
2011-05-11 22:29:24 +00:00
|
|
|
* a registered observer, no action is performed. This will not clear any existing notification
|
|
|
|
* for the specified account. Use {@link NotificationManager#cancel(int)}.
|
2011-05-10 18:36:57 +00:00
|
|
|
* NOTE: This must be called on the notification handler thread.
|
2011-05-12 17:13:45 +00:00
|
|
|
* @param accountId The ID of the account to unregister from. To unregister all accounts that
|
|
|
|
* have observers, specify an ID of {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
|
2011-05-10 18:36:57 +00:00
|
|
|
*/
|
|
|
|
private void unregisterMessageNotification(long accountId) {
|
|
|
|
ContentResolver resolver = mContext.getContentResolver();
|
2011-05-12 17:13:45 +00:00
|
|
|
if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
|
2011-05-10 18:36:57 +00:00
|
|
|
// cancel all existing message observers
|
2011-07-20 01:19:59 +00:00
|
|
|
for (ContentObserver observer : mNotificationMap.values()) {
|
2011-05-10 18:36:57 +00:00
|
|
|
resolver.unregisterContentObserver(observer);
|
|
|
|
}
|
|
|
|
mNotificationMap.clear();
|
2011-05-03 21:42:26 +00:00
|
|
|
} else {
|
2011-07-20 01:19:59 +00:00
|
|
|
ContentObserver observer = mNotificationMap.remove(accountId);
|
|
|
|
if (observer != null) {
|
2011-05-10 18:36:57 +00:00
|
|
|
resolver.unregisterContentObserver(observer);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-09-26 23:16:21 +00:00
|
|
|
/**
|
2011-04-28 18:09:41 +00:00
|
|
|
* Returns a picture of the sender of the given message. If no picture is available, returns
|
|
|
|
* {@code null}.
|
2010-09-26 23:16:21 +00:00
|
|
|
*
|
2011-04-28 18:09:41 +00:00
|
|
|
* NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
|
2010-09-26 23:16:21 +00:00
|
|
|
*/
|
|
|
|
private Bitmap getSenderPhoto(Message message) {
|
|
|
|
Address sender = Address.unpackFirst(message.mFrom);
|
|
|
|
if (sender == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
String email = sender.getAddress();
|
|
|
|
if (TextUtils.isEmpty(email)) {
|
|
|
|
return null;
|
|
|
|
}
|
2011-04-28 18:09:41 +00:00
|
|
|
return ContactStatusLoader.getContactInfo(mContext, email).mPhoto;
|
2010-09-26 23:16:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2011-04-28 18:09:41 +00:00
|
|
|
* Returns a "new message" notification for the given account.
|
2010-09-26 23:16:21 +00:00
|
|
|
*
|
2011-04-28 18:09:41 +00:00
|
|
|
* NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
|
2010-09-26 23:16:21 +00:00
|
|
|
*/
|
2011-04-28 18:09:41 +00:00
|
|
|
@VisibleForTesting
|
2011-05-13 15:41:48 +00:00
|
|
|
Notification createNewMessageNotification(long accountId, long mailboxId, long messageId,
|
2011-06-24 00:48:25 +00:00
|
|
|
int unseenMessageCount, int unreadCount, boolean enableAudio) {
|
2010-09-26 23:16:21 +00:00
|
|
|
final Account account = Account.restoreAccountWithId(mContext, accountId);
|
|
|
|
if (account == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
// Get the latest message
|
2011-05-10 18:36:57 +00:00
|
|
|
final Message message = Message.restoreMessageWithId(mContext, messageId);
|
2010-09-26 23:16:21 +00:00
|
|
|
if (message == null) {
|
|
|
|
return null; // no message found???
|
|
|
|
}
|
|
|
|
|
2011-03-08 18:34:18 +00:00
|
|
|
String senderName = Address.toFriendly(Address.unpack(message.mFrom));
|
|
|
|
if (senderName == null) {
|
|
|
|
senderName = ""; // Happens when a message has no from.
|
|
|
|
}
|
2011-06-24 00:48:25 +00:00
|
|
|
final boolean multipleUnseen = unseenMessageCount > 1;
|
|
|
|
final Bitmap senderPhoto = multipleUnseen ? null : getSenderPhoto(message);
|
|
|
|
final SpannableString title = getNewMessageTitle(senderName, unseenMessageCount);
|
|
|
|
// TODO: add in display name on the second line for the text, once framework supports
|
|
|
|
// multiline texts.
|
|
|
|
final String text = multipleUnseen
|
|
|
|
? account.mDisplayName
|
|
|
|
: message.mSubject;
|
2011-04-28 18:09:41 +00:00
|
|
|
final Bitmap largeIcon = senderPhoto != null ? senderPhoto : mGenericSenderIcon;
|
2011-06-24 00:48:25 +00:00
|
|
|
final Integer number = unreadCount > 1 ? unreadCount : null;
|
2011-05-13 15:41:48 +00:00
|
|
|
final Intent intent;
|
2011-07-10 21:02:50 +00:00
|
|
|
if (unseenMessageCount > 1) {
|
2011-05-13 15:41:48 +00:00
|
|
|
intent = Welcome.createOpenAccountInboxIntent(mContext, accountId);
|
2011-07-10 21:02:50 +00:00
|
|
|
} else {
|
|
|
|
intent = Welcome.createOpenMessageIntent(mContext, accountId, mailboxId, messageId);
|
2011-05-13 15:41:48 +00:00
|
|
|
}
|
2010-09-26 23:16:21 +00:00
|
|
|
|
2011-06-24 00:48:25 +00:00
|
|
|
Notification notification = createAccountNotification(account, null, title, text,
|
2011-05-03 21:42:26 +00:00
|
|
|
intent, largeIcon, number, enableAudio);
|
2010-09-26 23:16:21 +00:00
|
|
|
return notification;
|
|
|
|
}
|
|
|
|
|
2010-12-03 22:44:47 +00:00
|
|
|
/**
|
2011-06-24 00:48:25 +00:00
|
|
|
* Creates a notification title for a new message. If there is only a single message,
|
|
|
|
* show the sender name. Otherwise, show "X new messages".
|
2010-12-03 22:44:47 +00:00
|
|
|
*/
|
2011-04-28 18:09:41 +00:00
|
|
|
@VisibleForTesting
|
2011-06-24 00:48:25 +00:00
|
|
|
SpannableString getNewMessageTitle(String sender, int unseenCount) {
|
|
|
|
String title;
|
|
|
|
if (unseenCount > 1) {
|
|
|
|
title = String.format(
|
|
|
|
mContext.getString(R.string.notification_multiple_new_messages_fmt),
|
|
|
|
unseenCount);
|
2010-12-03 22:44:47 +00:00
|
|
|
} else {
|
2011-06-24 00:48:25 +00:00
|
|
|
title = sender;
|
2010-12-03 22:44:47 +00:00
|
|
|
}
|
2011-06-24 00:48:25 +00:00
|
|
|
return new SpannableString(title);
|
2010-12-03 22:44:47 +00:00
|
|
|
}
|
|
|
|
|
2011-04-28 18:09:41 +00:00
|
|
|
/** Returns the system's current ringer mode */
|
|
|
|
@VisibleForTesting
|
|
|
|
int getRingerMode() {
|
2010-12-03 22:44:47 +00:00
|
|
|
return mAudioManager.getRingerMode();
|
|
|
|
}
|
|
|
|
|
2011-04-28 18:09:41 +00:00
|
|
|
/** Sets up the notification's sound and vibration based upon account details. */
|
|
|
|
@VisibleForTesting
|
|
|
|
void setupSoundAndVibration(Notification.Builder builder, Account account) {
|
2010-09-26 23:16:21 +00:00
|
|
|
final int flags = account.mFlags;
|
|
|
|
final String ringtoneUri = account.mRingtoneUri;
|
|
|
|
final boolean vibrate = (flags & Account.FLAGS_VIBRATE_ALWAYS) != 0;
|
|
|
|
final boolean vibrateWhenSilent = (flags & Account.FLAGS_VIBRATE_WHEN_SILENT) != 0;
|
2011-04-28 18:09:41 +00:00
|
|
|
final boolean isRingerSilent = getRingerMode() != AudioManager.RINGER_MODE_NORMAL;
|
2010-09-26 23:16:21 +00:00
|
|
|
|
2011-04-28 18:09:41 +00:00
|
|
|
int defaults = Notification.DEFAULT_LIGHTS;
|
|
|
|
if (vibrate || (vibrateWhenSilent && isRingerSilent)) {
|
|
|
|
defaults |= Notification.DEFAULT_VIBRATE;
|
2010-09-26 23:16:21 +00:00
|
|
|
}
|
|
|
|
|
2011-04-28 18:09:41 +00:00
|
|
|
builder.setSound((ringtoneUri == null) ? null : Uri.parse(ringtoneUri))
|
|
|
|
.setDefaults(defaults);
|
2010-09-26 23:16:21 +00:00
|
|
|
}
|
2010-10-18 20:14:20 +00:00
|
|
|
|
|
|
|
/**
|
2011-04-28 18:09:41 +00:00
|
|
|
* Show (or update) a notification that the given attachment could not be forwarded. This
|
|
|
|
* is a very unusual case, and perhaps we shouldn't even send a notification. For now,
|
|
|
|
* it's helpful for debugging.
|
|
|
|
*
|
2011-02-17 00:38:18 +00:00
|
|
|
* NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
|
2010-10-18 20:14:20 +00:00
|
|
|
*/
|
2011-02-17 00:38:18 +00:00
|
|
|
public void showDownloadForwardFailedNotification(Attachment attachment) {
|
|
|
|
final Account account = Account.restoreAccountWithId(mContext, attachment.mAccountKey);
|
|
|
|
if (account == null) return;
|
2011-03-22 00:08:16 +00:00
|
|
|
showAccountNotification(account,
|
2010-10-18 20:14:20 +00:00
|
|
|
mContext.getString(R.string.forward_download_failed_ticker),
|
2011-02-17 00:38:18 +00:00
|
|
|
mContext.getString(R.string.forward_download_failed_title),
|
|
|
|
attachment.mFileName,
|
|
|
|
null,
|
|
|
|
NOTIFICATION_ID_ATTACHMENT_WARNING);
|
2010-10-18 20:14:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2011-04-28 18:09:41 +00:00
|
|
|
* Returns a notification ID for login failed notifications for the given account account.
|
2010-10-18 20:14:20 +00:00
|
|
|
*/
|
|
|
|
private int getLoginFailedNotificationId(long accountId) {
|
|
|
|
return NOTIFICATION_ID_BASE_LOGIN_WARNING + (int)accountId;
|
|
|
|
}
|
|
|
|
|
2011-02-17 00:38:18 +00:00
|
|
|
/**
|
2011-04-28 18:09:41 +00:00
|
|
|
* Show (or update) a notification that there was a login failure for the given account.
|
|
|
|
*
|
2011-02-17 00:38:18 +00:00
|
|
|
* NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
|
|
|
|
*/
|
2010-10-18 20:14:20 +00:00
|
|
|
public void showLoginFailedNotification(long accountId) {
|
|
|
|
final Account account = Account.restoreAccountWithId(mContext, accountId);
|
|
|
|
if (account == null) return;
|
2011-03-22 00:08:16 +00:00
|
|
|
showAccountNotification(account,
|
2010-10-18 20:14:20 +00:00
|
|
|
mContext.getString(R.string.login_failed_ticker, account.mDisplayName),
|
2011-02-17 00:38:18 +00:00
|
|
|
mContext.getString(R.string.login_failed_title),
|
|
|
|
account.getDisplayName(),
|
2011-05-13 00:27:56 +00:00
|
|
|
AccountSettings.createAccountSettingsIntent(mContext, accountId,
|
2011-02-19 02:23:18 +00:00
|
|
|
account.mDisplayName),
|
2011-02-17 00:38:18 +00:00
|
|
|
getLoginFailedNotificationId(accountId));
|
2010-10-18 20:14:20 +00:00
|
|
|
}
|
|
|
|
|
2011-04-28 18:09:41 +00:00
|
|
|
/**
|
|
|
|
* Cancels the login failed notification for the given account.
|
|
|
|
*/
|
2010-10-18 20:14:20 +00:00
|
|
|
public void cancelLoginFailedNotification(long accountId) {
|
|
|
|
mNotificationManager.cancel(getLoginFailedNotificationId(accountId));
|
|
|
|
}
|
2011-03-22 00:08:16 +00:00
|
|
|
|
|
|
|
/**
|
2011-04-28 18:09:41 +00:00
|
|
|
* Show (or update) a notification that the user's password is expiring. The given account
|
|
|
|
* is used to update the display text, but, all accounts share the same notification ID.
|
2011-03-22 00:08:16 +00:00
|
|
|
*
|
2011-04-28 18:09:41 +00:00
|
|
|
* NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
|
2011-03-22 00:08:16 +00:00
|
|
|
*/
|
|
|
|
public void showPasswordExpiringNotification(long accountId) {
|
|
|
|
Account account = Account.restoreAccountWithId(mContext, accountId);
|
|
|
|
if (account == null) return;
|
2011-04-28 18:09:41 +00:00
|
|
|
|
2011-03-22 00:08:16 +00:00
|
|
|
Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext,
|
|
|
|
accountId, false);
|
2011-04-28 18:09:41 +00:00
|
|
|
String accountName = account.getDisplayName();
|
|
|
|
String ticker =
|
|
|
|
mContext.getString(R.string.password_expire_warning_ticker_fmt, accountName);
|
|
|
|
String title = mContext.getString(R.string.password_expire_warning_content_title);
|
|
|
|
showAccountNotification(account, ticker, title, accountName, intent,
|
2011-03-22 00:08:16 +00:00
|
|
|
NOTIFICATION_ID_PASSWORD_EXPIRING);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2011-04-28 18:09:41 +00:00
|
|
|
* Show (or update) a notification that the user's password has expired. The given account
|
|
|
|
* is used to update the display text, but, all accounts share the same notification ID.
|
2011-03-22 00:08:16 +00:00
|
|
|
*
|
2011-04-28 18:09:41 +00:00
|
|
|
* NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
|
2011-03-22 00:08:16 +00:00
|
|
|
*/
|
|
|
|
public void showPasswordExpiredNotification(long accountId) {
|
|
|
|
Account account = Account.restoreAccountWithId(mContext, accountId);
|
|
|
|
if (account == null) return;
|
2011-04-28 18:09:41 +00:00
|
|
|
|
2011-03-22 00:08:16 +00:00
|
|
|
Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext,
|
|
|
|
accountId, true);
|
2011-04-28 18:09:41 +00:00
|
|
|
String accountName = account.getDisplayName();
|
2011-03-22 00:08:16 +00:00
|
|
|
String ticker = mContext.getString(R.string.password_expired_ticker);
|
2011-04-28 18:09:41 +00:00
|
|
|
String title = mContext.getString(R.string.password_expired_content_title);
|
|
|
|
showAccountNotification(account, ticker, title, accountName, intent,
|
|
|
|
NOTIFICATION_ID_PASSWORD_EXPIRED);
|
2011-03-22 00:08:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2011-04-28 18:09:41 +00:00
|
|
|
* Cancels any password expire notifications [both expired & expiring].
|
2011-03-22 00:08:16 +00:00
|
|
|
*/
|
|
|
|
public void cancelPasswordExpirationNotifications() {
|
2011-05-10 18:36:57 +00:00
|
|
|
mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRING);
|
|
|
|
mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRED);
|
2011-03-22 00:08:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2011-04-28 18:09:41 +00:00
|
|
|
* Show (or update) a security needed notification. The given account is used to update
|
|
|
|
* the display text, but, all accounts share the same notification ID.
|
2011-03-22 00:08:16 +00:00
|
|
|
*/
|
|
|
|
public void showSecurityNeededNotification(Account account) {
|
|
|
|
Intent intent = AccountSecurity.actionUpdateSecurityIntent(mContext, account.mId, true);
|
2011-04-28 18:09:41 +00:00
|
|
|
String accountName = account.getDisplayName();
|
|
|
|
String ticker =
|
|
|
|
mContext.getString(R.string.security_notification_ticker_fmt, accountName);
|
|
|
|
String title = mContext.getString(R.string.security_notification_content_title);
|
|
|
|
showAccountNotification(account, ticker, title, accountName, intent,
|
2011-03-22 00:08:16 +00:00
|
|
|
NOTIFICATION_ID_SECURITY_NEEDED);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2011-04-28 18:09:41 +00:00
|
|
|
* Cancels the security needed notification.
|
2011-03-22 00:08:16 +00:00
|
|
|
*/
|
|
|
|
public void cancelSecurityNeededNotification() {
|
2011-05-10 18:36:57 +00:00
|
|
|
mNotificationManager.cancel(NOTIFICATION_ID_SECURITY_NEEDED);
|
2011-03-22 00:08:16 +00:00
|
|
|
}
|
2011-05-03 21:42:26 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Observer invoked whenever a message we're notifying the user about changes.
|
|
|
|
*/
|
|
|
|
private static class MessageContentObserver extends ContentObserver {
|
2011-05-10 18:36:57 +00:00
|
|
|
/** A selection to get messages the user hasn't seen before */
|
|
|
|
private final static String MESSAGE_SELECTION =
|
|
|
|
MessageColumns.MAILBOX_KEY + "=? AND " + MessageColumns.ID + ">? AND "
|
|
|
|
+ MessageColumns.FLAG_READ + "=0";
|
2011-05-03 21:42:26 +00:00
|
|
|
private final Context mContext;
|
2011-05-10 18:36:57 +00:00
|
|
|
private final long mMailboxId;
|
|
|
|
private final long mAccountId;
|
2011-05-03 21:42:26 +00:00
|
|
|
|
2011-05-10 18:36:57 +00:00
|
|
|
public MessageContentObserver(
|
|
|
|
Handler handler, Context context, long mailboxId, long accountId) {
|
|
|
|
super(handler);
|
2011-05-03 21:42:26 +00:00
|
|
|
mContext = context;
|
2011-05-10 18:36:57 +00:00
|
|
|
mMailboxId = mailboxId;
|
2011-05-03 21:42:26 +00:00
|
|
|
mAccountId = accountId;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onChange(boolean selfChange) {
|
2011-05-12 17:13:45 +00:00
|
|
|
if (mAccountId == sInstance.mSuspendAccountId
|
|
|
|
|| sInstance.mSuspendAccountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
|
|
|
|
return;
|
|
|
|
}
|
2011-05-03 21:42:26 +00:00
|
|
|
|
2011-07-20 01:19:59 +00:00
|
|
|
ContentObserver observer = sInstance.mNotificationMap.get(mAccountId);
|
|
|
|
if (observer == null) {
|
2011-05-10 18:36:57 +00:00
|
|
|
// notification for a mailbox that we aren't observing; this should not happen
|
|
|
|
Log.e(Logging.LOG_TAG, "Received notifiaction when observer data was null");
|
2011-05-03 21:42:26 +00:00
|
|
|
return;
|
|
|
|
}
|
2011-07-20 01:19:59 +00:00
|
|
|
Account account = Account.restoreAccountWithId(mContext, mAccountId);
|
|
|
|
long oldMessageId = account.mNotifiedMessageId;
|
|
|
|
int oldMessageCount = account.mNotifiedMessageCount;
|
2011-05-03 21:42:26 +00:00
|
|
|
|
|
|
|
ContentResolver resolver = mContext.getContentResolver();
|
2011-05-10 18:36:57 +00:00
|
|
|
long lastSeenMessageId = Utility.getFirstRowLong(
|
Improve EmailContent caching...
* Guarantee that up to 16 Account (with HostAuths), and Policy rows
are always cached. Also, 6 commonly used Mailboxes per Account
(Inbox, Outbox, Drafts, Sent, Trash, and Search)
* Precache these rows when EmailProvider starts up
* Ensure that newly added, precachable rows are cached when created
* Clean up some inefficient/wrong caching code
* Fix a commonly called method in NotificationManager in which we
load a single Mailbox row using selection vs withAppendedId
* Confirm that we don't read from the database in typical use and
heavy message loading
* Add a special URI for finding mailbox by type (using the cache)
* Add special-case code for EmailContent.count(Account.CONTENT_URI)
which is used in a number of places (including on the UI thread)
and whose value is easily determined
* Add a special URI to get the default account id
* Confirm that all unit tests work
The goal here is to be able to load all Account, HostAuth, Policy,
and Mailbox objects (by id) without worrying about disk access.
There will still be a single disk read for uncommon Mailbox reads,
but this should be considered acceptable.
Change-Id: Ibc9aa7acc73185e360b0b6f3053b90a985e97210
TODO: Unit tests
2011-06-19 01:03:11 +00:00
|
|
|
mContext, ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailboxId),
|
2011-05-10 18:36:57 +00:00
|
|
|
new String[] { MailboxColumns.LAST_SEEN_MESSAGE_KEY },
|
Improve EmailContent caching...
* Guarantee that up to 16 Account (with HostAuths), and Policy rows
are always cached. Also, 6 commonly used Mailboxes per Account
(Inbox, Outbox, Drafts, Sent, Trash, and Search)
* Precache these rows when EmailProvider starts up
* Ensure that newly added, precachable rows are cached when created
* Clean up some inefficient/wrong caching code
* Fix a commonly called method in NotificationManager in which we
load a single Mailbox row using selection vs withAppendedId
* Confirm that we don't read from the database in typical use and
heavy message loading
* Add a special URI for finding mailbox by type (using the cache)
* Add special-case code for EmailContent.count(Account.CONTENT_URI)
which is used in a number of places (including on the UI thread)
and whose value is easily determined
* Add a special URI to get the default account id
* Confirm that all unit tests work
The goal here is to be able to load all Account, HostAuth, Policy,
and Mailbox objects (by id) without worrying about disk access.
There will still be a single disk read for uncommon Mailbox reads,
but this should be considered acceptable.
Change-Id: Ibc9aa7acc73185e360b0b6f3053b90a985e97210
TODO: Unit tests
2011-06-19 01:03:11 +00:00
|
|
|
null, null, null, 0, 0L);
|
2011-05-10 18:36:57 +00:00
|
|
|
Cursor c = resolver.query(
|
|
|
|
Message.CONTENT_URI, EmailContent.ID_PROJECTION,
|
|
|
|
MESSAGE_SELECTION,
|
|
|
|
new String[] { Long.toString(mMailboxId), Long.toString(lastSeenMessageId) },
|
|
|
|
MessageColumns.ID + " DESC");
|
2011-05-11 22:29:24 +00:00
|
|
|
if (c == null) {
|
|
|
|
// Suspender time ... theoretically, this will never happen
|
|
|
|
Log.wtf(Logging.LOG_TAG, "#onChange(); NULL response for message id query");
|
|
|
|
return;
|
|
|
|
}
|
2011-05-03 21:42:26 +00:00
|
|
|
try {
|
2011-05-10 18:36:57 +00:00
|
|
|
int newMessageCount = c.getCount();
|
|
|
|
long newMessageId = 0L;
|
|
|
|
if (c.moveToNext()) {
|
|
|
|
newMessageId = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
|
2011-05-03 21:42:26 +00:00
|
|
|
}
|
2011-05-10 18:36:57 +00:00
|
|
|
|
|
|
|
if (newMessageCount == 0) {
|
|
|
|
// No messages to notify for; clear the notification
|
2011-05-11 22:29:24 +00:00
|
|
|
int notificationId = sInstance.getNewMessageNotificationId(mAccountId);
|
|
|
|
sInstance.mNotificationManager.cancel(notificationId);
|
2011-05-10 18:36:57 +00:00
|
|
|
} else if (newMessageCount != oldMessageCount
|
|
|
|
|| (newMessageId != 0 && newMessageId != oldMessageId)) {
|
|
|
|
// Either the count or last message has changed; update the notification
|
|
|
|
boolean playAudio = (oldMessageCount == 0); // play audio on first notification
|
2011-06-24 00:48:25 +00:00
|
|
|
|
|
|
|
int unreadCount = Utility.getFirstRowInt(
|
|
|
|
mContext, ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailboxId),
|
|
|
|
new String[] { MailboxColumns.UNREAD_COUNT },
|
|
|
|
null, null, null, 0, 0);
|
|
|
|
|
2011-05-10 18:36:57 +00:00
|
|
|
Notification n = sInstance.createNewMessageNotification(
|
2011-06-24 00:48:25 +00:00
|
|
|
mAccountId, mMailboxId, newMessageId,
|
|
|
|
newMessageCount, unreadCount, playAudio);
|
2011-05-10 18:36:57 +00:00
|
|
|
if (n != null) {
|
|
|
|
// Make the notification visible
|
|
|
|
sInstance.mNotificationManager.notify(
|
|
|
|
sInstance.getNewMessageNotificationId(mAccountId), n);
|
2011-05-03 21:42:26 +00:00
|
|
|
}
|
2011-05-10 18:36:57 +00:00
|
|
|
}
|
2011-07-20 01:19:59 +00:00
|
|
|
// Save away the new values
|
|
|
|
ContentValues cv = new ContentValues();
|
|
|
|
cv.put(AccountColumns.NOTIFIED_MESSAGE_ID, newMessageId);
|
|
|
|
cv.put(AccountColumns.NOTIFIED_MESSAGE_COUNT, newMessageCount);
|
|
|
|
resolver.update(ContentUris.withAppendedId(Account.CONTENT_URI, mAccountId), cv,
|
|
|
|
null, null);
|
2011-05-10 18:36:57 +00:00
|
|
|
} finally {
|
|
|
|
c.close();
|
2011-05-03 21:42:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-05-11 22:29:24 +00:00
|
|
|
/**
|
|
|
|
* Observer invoked whenever an account is modified. This could mean the user changed the
|
|
|
|
* notification settings.
|
|
|
|
*/
|
|
|
|
private static class AccountContentObserver extends ContentObserver {
|
|
|
|
private final Context mContext;
|
|
|
|
public AccountContentObserver(Handler handler, Context context) {
|
|
|
|
super(handler);
|
|
|
|
mContext = context;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void onChange(boolean selfChange) {
|
|
|
|
final ContentResolver resolver = mContext.getContentResolver();
|
|
|
|
final Cursor c = resolver.query(
|
|
|
|
Account.CONTENT_URI, EmailContent.ID_PROJECTION,
|
|
|
|
NOTIFIED_ACCOUNT_SELECTION, null, null);
|
|
|
|
final HashSet<Long> newAccountList = new HashSet<Long>();
|
|
|
|
final HashSet<Long> removedAccountList = new HashSet<Long>();
|
|
|
|
if (c == null) {
|
|
|
|
// Suspender time ... theoretically, this will never happen
|
|
|
|
Log.wtf(Logging.LOG_TAG, "#onChange(); NULL response for account id query");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
while (c.moveToNext()) {
|
|
|
|
long accountId = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
|
|
|
|
newAccountList.add(accountId);
|
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
if (c != null) {
|
|
|
|
c.close();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// NOTE: Looping over three lists is not necessarily the most efficient. However, the
|
|
|
|
// account lists are going to be very small, so, this will not be necessarily bad.
|
|
|
|
// Cycle through existing notification list and adjust as necessary
|
|
|
|
for (long accountId : sInstance.mNotificationMap.keySet()) {
|
|
|
|
if (!newAccountList.remove(accountId)) {
|
|
|
|
// account id not in the current set of notifiable accounts
|
|
|
|
removedAccountList.add(accountId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// A new account was added to the notification list
|
|
|
|
for (long accountId : newAccountList) {
|
|
|
|
sInstance.registerMessageNotification(accountId);
|
|
|
|
}
|
|
|
|
// An account was removed from the notification list
|
|
|
|
for (long accountId : removedAccountList) {
|
|
|
|
sInstance.unregisterMessageNotification(accountId);
|
|
|
|
int notificationId = sInstance.getNewMessageNotificationId(accountId);
|
|
|
|
sInstance.mNotificationManager.cancel(notificationId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-05-10 18:36:57 +00:00
|
|
|
/**
|
|
|
|
* Thread to handle all notification actions through its own {@link Looper}.
|
|
|
|
*/
|
|
|
|
private static class NotificationThread implements Runnable {
|
|
|
|
/** Lock to ensure proper initialization */
|
|
|
|
private final Object mLock = new Object();
|
|
|
|
/** The {@link Looper} that handles messages for this thread */
|
|
|
|
private Looper mLooper;
|
|
|
|
|
|
|
|
NotificationThread() {
|
|
|
|
new Thread(null, this, "EmailNotification").start();
|
|
|
|
synchronized (mLock) {
|
|
|
|
while (mLooper == null) {
|
|
|
|
try {
|
|
|
|
mLock.wait();
|
|
|
|
} catch (InterruptedException ex) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void run() {
|
|
|
|
synchronized (mLock) {
|
|
|
|
Looper.prepare();
|
|
|
|
mLooper = Looper.myLooper();
|
|
|
|
mLock.notifyAll();
|
|
|
|
}
|
|
|
|
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
|
|
|
|
Looper.loop();
|
|
|
|
}
|
|
|
|
void quit() {
|
|
|
|
mLooper.quit();
|
|
|
|
}
|
|
|
|
Looper getLooper() {
|
|
|
|
return mLooper;
|
|
|
|
}
|
2011-05-03 21:42:26 +00:00
|
|
|
}
|
2010-09-26 23:16:21 +00:00
|
|
|
}
|