Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 1 | /* |
| 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 | |
| 17 | package com.android.email; |
| 18 | |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 19 | import android.app.Notification; |
Todd Kennedy | dfdc8b6 | 2011-05-11 13:40:52 -0700 | [diff] [blame] | 20 | import android.app.Notification.Builder; |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 21 | import android.app.NotificationManager; |
| 22 | import android.app.PendingIntent; |
Todd Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 23 | import android.content.ContentResolver; |
Marc Blank | 6e418aa | 2011-06-18 18:03:11 -0700 | [diff] [blame] | 24 | import android.content.ContentUris; |
Marc Blank | aca9426 | 2011-07-19 18:19:59 -0700 | [diff] [blame] | 25 | import android.content.ContentValues; |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 26 | import android.content.Context; |
Marc Blank | d3e4f3c | 2010-10-18 13:14:20 -0700 | [diff] [blame] | 27 | import android.content.Intent; |
Todd Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 28 | import android.database.ContentObserver; |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 29 | import android.database.Cursor; |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 30 | import android.graphics.Bitmap; |
Makoto Onuki | 74e0948 | 2010-12-03 14:44:47 -0800 | [diff] [blame] | 31 | import android.graphics.BitmapFactory; |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 32 | import android.media.AudioManager; |
| 33 | import android.net.Uri; |
Todd Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 34 | import android.os.Handler; |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 35 | import android.os.Looper; |
| 36 | import android.os.Process; |
Makoto Onuki | 74e0948 | 2010-12-03 14:44:47 -0800 | [diff] [blame] | 37 | import android.text.SpannableString; |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 38 | import android.text.TextUtils; |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 39 | import android.util.Log; |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 40 | |
Marc Blank | 6e418aa | 2011-06-18 18:03:11 -0700 | [diff] [blame] | 41 | import com.android.email.activity.ContactStatusLoader; |
| 42 | import com.android.email.activity.Welcome; |
| 43 | import com.android.email.activity.setup.AccountSecurity; |
| 44 | import com.android.email.activity.setup.AccountSettings; |
| 45 | import com.android.emailcommon.Logging; |
| 46 | import com.android.emailcommon.mail.Address; |
| 47 | import com.android.emailcommon.provider.Account; |
| 48 | import com.android.emailcommon.provider.EmailContent; |
Marc Blank | aca9426 | 2011-07-19 18:19:59 -0700 | [diff] [blame] | 49 | import com.android.emailcommon.provider.EmailContent.AccountColumns; |
Marc Blank | 6e418aa | 2011-06-18 18:03:11 -0700 | [diff] [blame] | 50 | import com.android.emailcommon.provider.EmailContent.Attachment; |
| 51 | import com.android.emailcommon.provider.EmailContent.MailboxColumns; |
| 52 | import com.android.emailcommon.provider.EmailContent.Message; |
| 53 | import com.android.emailcommon.provider.EmailContent.MessageColumns; |
| 54 | import com.android.emailcommon.provider.Mailbox; |
| 55 | import com.android.emailcommon.utility.Utility; |
| 56 | import com.google.common.annotations.VisibleForTesting; |
| 57 | |
Todd Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 58 | import java.util.HashMap; |
Todd Kennedy | e7fb4ac | 2011-05-11 15:29:24 -0700 | [diff] [blame] | 59 | import java.util.HashSet; |
Todd Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 60 | |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 61 | /** |
| 62 | * Class that manages notifications. |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 63 | */ |
| 64 | public class NotificationController { |
Makoto Onuki | 308ce92 | 2011-03-21 17:08:16 -0700 | [diff] [blame] | 65 | private static final int NOTIFICATION_ID_SECURITY_NEEDED = 1; |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 66 | /** Reserved for {@link com.android.exchange.CalendarSyncEnabler} */ |
| 67 | @SuppressWarnings("unused") |
| 68 | private static final int NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED = 2; |
Makoto Onuki | 308ce92 | 2011-03-21 17:08:16 -0700 | [diff] [blame] | 69 | 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 Blank | d3e4f3c | 2010-10-18 13:14:20 -0700 | [diff] [blame] | 72 | |
| 73 | private static final int NOTIFICATION_ID_BASE_NEW_MESSAGES = 0x10000000; |
| 74 | private static final int NOTIFICATION_ID_BASE_LOGIN_WARNING = 0x20000000; |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 75 | |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 76 | /** 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 Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 79 | |
Todd Kennedy | e7fb4ac | 2011-05-11 15:29:24 -0700 | [diff] [blame] | 80 | private static NotificationThread sNotificationThread; |
| 81 | private static Handler sNotificationHandler; |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 82 | private static NotificationController sInstance; |
| 83 | private final Context mContext; |
| 84 | private final NotificationManager mNotificationManager; |
| 85 | private final AudioManager mAudioManager; |
Andy Stadler | c1c3b6f | 2010-12-15 15:26:30 -0800 | [diff] [blame] | 86 | private final Bitmap mGenericSenderIcon; |
Ben Komalo | f13fee5 | 2011-08-17 17:10:06 -0700 | [diff] [blame] | 87 | private final Bitmap mGenericMultipleSenderIcon; |
Makoto Onuki | 74e0948 | 2010-12-03 14:44:47 -0800 | [diff] [blame] | 88 | private final Clock mClock; |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 89 | // 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 Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 92 | /** Maps account id to the message data */ |
Marc Blank | aca9426 | 2011-07-19 18:19:59 -0700 | [diff] [blame] | 93 | private final HashMap<Long, ContentObserver> mNotificationMap; |
Todd Kennedy | e7fb4ac | 2011-05-11 15:29:24 -0700 | [diff] [blame] | 94 | private ContentObserver mAccountObserver; |
Todd Kennedy | 5701e0a | 2011-05-12 10:13:45 -0700 | [diff] [blame] | 95 | /** |
Makoto Onuki | b36ac01 | 2011-05-17 10:50:30 -0700 | [diff] [blame] | 96 | * Suspend notifications for this account. If {@link Account#NO_ACCOUNT}, no |
Todd Kennedy | 5701e0a | 2011-05-12 10:13:45 -0700 | [diff] [blame] | 97 | * account notifications are suspended. If {@link Account#ACCOUNT_ID_COMBINED_VIEW}, |
| 98 | * notifications for all accounts are suspended. |
| 99 | */ |
Makoto Onuki | b36ac01 | 2011-05-17 10:50:30 -0700 | [diff] [blame] | 100 | private long mSuspendAccountId = Account.NO_ACCOUNT; |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 101 | |
Ben Komalo | 23a4b15 | 2011-07-22 11:41:51 -0700 | [diff] [blame] | 102 | /** |
| 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 Komalo | 48a3a1c | 2011-07-22 13:29:48 -0700 | [diff] [blame] | 114 | private static final long MIN_SOUND_INTERVAL_MS = 15 * 1000; // 15 seconds |
Ben Komalo | 23a4b15 | 2011-07-22 11:41:51 -0700 | [diff] [blame] | 115 | |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 116 | /** Constructor */ |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 117 | @VisibleForTesting |
| 118 | NotificationController(Context context, Clock clock) { |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 119 | mContext = context.getApplicationContext(); |
| 120 | mNotificationManager = (NotificationManager) context.getSystemService( |
| 121 | Context.NOTIFICATION_SERVICE); |
| 122 | mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); |
Andy Stadler | c1c3b6f | 2010-12-15 15:26:30 -0800 | [diff] [blame] | 123 | mGenericSenderIcon = BitmapFactory.decodeResource(mContext.getResources(), |
| 124 | R.drawable.ic_contact_picture); |
Ben Komalo | f13fee5 | 2011-08-17 17:10:06 -0700 | [diff] [blame] | 125 | mGenericMultipleSenderIcon = BitmapFactory.decodeResource(mContext.getResources(), |
| 126 | R.drawable.ic_notification_multiple_mail_holo_dark); |
Makoto Onuki | 74e0948 | 2010-12-03 14:44:47 -0800 | [diff] [blame] | 127 | mClock = clock; |
Marc Blank | aca9426 | 2011-07-19 18:19:59 -0700 | [diff] [blame] | 128 | mNotificationMap = new HashMap<Long, ContentObserver>(); |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 129 | } |
| 130 | |
| 131 | /** Singleton access */ |
| 132 | public static synchronized NotificationController getInstance(Context context) { |
| 133 | if (sInstance == null) { |
Makoto Onuki | 74e0948 | 2010-12-03 14:44:47 -0800 | [diff] [blame] | 134 | sInstance = new NotificationController(context, Clock.INSTANCE); |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 135 | } |
| 136 | return sInstance; |
| 137 | } |
| 138 | |
| 139 | /** |
Marc Blank | d9b2a8f | 2011-07-28 13:55:10 -0700 | [diff] [blame] | 140 | * 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 Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 151 | * 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 Stadler | 1ca111c | 2010-12-01 12:58:36 -0800 | [diff] [blame] | 154 | * |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 155 | * @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 Kennedy | dfdc8b6 | 2011-05-11 13:40:52 -0700 | [diff] [blame] | 161 | * @param number A number to display using {@link Builder#setNumber(int)}. May |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 162 | * be {@code null}. |
Todd Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 163 | * @param enableAudio If {@code false}, do not play any sound. Otherwise, play sound according |
| 164 | * to the settings for the given account. |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 165 | * @return A {@link Notification} that can be sent to the notification service. |
Andy Stadler | 1ca111c | 2010-12-01 12:58:36 -0800 | [diff] [blame] | 166 | */ |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 167 | private Notification createAccountNotification(Account account, String ticker, |
| 168 | CharSequence title, String contentText, Intent intent, Bitmap largeIcon, |
Marc Blank | d9b2a8f | 2011-07-28 13:55:10 -0700 | [diff] [blame] | 169 | Integer number, boolean enableAudio, boolean ongoing) { |
Andy Stadler | 1ca111c | 2010-12-01 12:58:36 -0800 | [diff] [blame] | 170 | // Pending Intent |
Andy Stadler | c6d344a | 2011-02-16 16:38:18 -0800 | [diff] [blame] | 171 | PendingIntent pending = null; |
| 172 | if (intent != null) { |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 173 | pending = PendingIntent.getActivity( |
| 174 | mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); |
Andy Stadler | c6d344a | 2011-02-16 16:38:18 -0800 | [diff] [blame] | 175 | } |
Andy Stadler | 1ca111c | 2010-12-01 12:58:36 -0800 | [diff] [blame] | 176 | |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 177 | // 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 Blank | d9b2a8f | 2011-07-28 13:55:10 -0700 | [diff] [blame] | 186 | .setTicker(ticker) |
| 187 | .setOngoing(ongoing); |
Todd Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 188 | |
| 189 | if (enableAudio) { |
| 190 | setupSoundAndVibration(builder, account); |
| 191 | } |
Andy Stadler | 1ca111c | 2010-12-01 12:58:36 -0800 | [diff] [blame] | 192 | |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 193 | Notification notification = builder.getNotification(); |
| 194 | return notification; |
| 195 | } |
Andy Stadler | 1ca111c | 2010-12-01 12:58:36 -0800 | [diff] [blame] | 196 | |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 197 | /** |
| 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 Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 209 | Notification notification = createAccountNotification(account, ticker, title, contentText, |
Marc Blank | d9b2a8f | 2011-07-28 13:55:10 -0700 | [diff] [blame] | 210 | intent, null, null, true, needsOngoingNotification(notificationId)); |
Andy Stadler | 1ca111c | 2010-12-01 12:58:36 -0800 | [diff] [blame] | 211 | mNotificationManager.notify(notificationId, notification); |
| 212 | } |
| 213 | |
| 214 | /** |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 215 | * Returns a notification ID for new message notifications for the given account. |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 216 | */ |
| 217 | private int getNewMessageNotificationId(long accountId) { |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 218 | // We assume accountId will always be less than 0x0FFFFFFF; is there a better way? |
Marc Blank | d3e4f3c | 2010-10-18 13:14:20 -0700 | [diff] [blame] | 219 | return (int) (NOTIFICATION_ID_BASE_NEW_MESSAGES + accountId); |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 220 | } |
| 221 | |
| 222 | /** |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 223 | * 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 Kennedy | 5701e0a | 2011-05-12 10:13:45 -0700 | [diff] [blame] | 229 | * notifications enabled. Otherwise, all observers are unregistered. |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 230 | */ |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 231 | public void watchForMessages(final boolean watch) { |
Ben Komalo | eb9dcfa | 2011-07-21 11:02:51 -0700 | [diff] [blame] | 232 | if (Email.DEBUG) { |
| 233 | Log.i(Logging.LOG_TAG, "Notifications being toggled: " + watch); |
| 234 | } |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 235 | // Don't create the thread if we're only going to stop watching |
Todd Kennedy | 5701e0a | 2011-05-12 10:13:45 -0700 | [diff] [blame] | 236 | if (!watch && sNotificationThread == null) return; |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 237 | |
Todd Kennedy | 5701e0a | 2011-05-12 10:13:45 -0700 | [diff] [blame] | 238 | ensureHandlerExists(); |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 239 | // Run this on the message notification handler |
Todd Kennedy | e7fb4ac | 2011-05-11 15:29:24 -0700 | [diff] [blame] | 240 | sNotificationHandler.post(new Runnable() { |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 241 | @Override |
| 242 | public void run() { |
| 243 | ContentResolver resolver = mContext.getContentResolver(); |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 244 | if (!watch) { |
Todd Kennedy | 5701e0a | 2011-05-12 10:13:45 -0700 | [diff] [blame] | 245 | unregisterMessageNotification(Account.ACCOUNT_ID_COMBINED_VIEW); |
Todd Kennedy | e7fb4ac | 2011-05-11 15:29:24 -0700 | [diff] [blame] | 246 | if (mAccountObserver != null) { |
| 247 | resolver.unregisterContentObserver(mAccountObserver); |
| 248 | mAccountObserver = null; |
| 249 | } |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 250 | |
| 251 | // tear down the event loop |
Todd Kennedy | e7fb4ac | 2011-05-11 15:29:24 -0700 | [diff] [blame] | 252 | sNotificationThread.quit(); |
Todd Kennedy | e7fb4ac | 2011-05-11 15:29:24 -0700 | [diff] [blame] | 253 | sNotificationThread = null; |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 254 | return; |
| 255 | } |
| 256 | |
| 257 | // otherwise, start new observers for all notified accounts |
Todd Kennedy | 5701e0a | 2011-05-12 10:13:45 -0700 | [diff] [blame] | 258 | registerMessageNotification(Account.ACCOUNT_ID_COMBINED_VIEW); |
Todd Kennedy | e7fb4ac | 2011-05-11 15:29:24 -0700 | [diff] [blame] | 259 | // If we're already observing account changes, don't do anything else |
| 260 | if (mAccountObserver == null) { |
Ben Komalo | eb9dcfa | 2011-07-21 11:02:51 -0700 | [diff] [blame] | 261 | if (Email.DEBUG) { |
| 262 | Log.i(Logging.LOG_TAG, "Observing account changes for notifications"); |
| 263 | } |
Todd Kennedy | e7fb4ac | 2011-05-11 15:29:24 -0700 | [diff] [blame] | 264 | mAccountObserver = new AccountContentObserver(sNotificationHandler, mContext); |
| 265 | resolver.registerContentObserver(Account.NOTIFIER_URI, true, mAccountObserver); |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 266 | } |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 267 | } |
| 268 | }); |
| 269 | } |
| 270 | |
| 271 | /** |
Todd Kennedy | 5701e0a | 2011-05-12 10:13:45 -0700 | [diff] [blame] | 272 | * 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 Onuki | b36ac01 | 2011-05-17 10:50:30 -0700 | [diff] [blame] | 282 | if (mSuspendAccountId != Account.NO_ACCOUNT) { |
Todd Kennedy | 5701e0a | 2011-05-12 10:13:45 -0700 | [diff] [blame] | 283 | // we're already suspending an account; un-suspend it |
Makoto Onuki | b36ac01 | 2011-05-17 10:50:30 -0700 | [diff] [blame] | 284 | mSuspendAccountId = Account.NO_ACCOUNT; |
Todd Kennedy | 5701e0a | 2011-05-12 10:13:45 -0700 | [diff] [blame] | 285 | } |
Makoto Onuki | b36ac01 | 2011-05-17 10:50:30 -0700 | [diff] [blame] | 286 | if (suspend && accountId != Account.NO_ACCOUNT && accountId > 0L) { |
Todd Kennedy | 5701e0a | 2011-05-12 10:13:45 -0700 | [diff] [blame] | 287 | 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 Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 316 | * 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 Kennedy | 5701e0a | 2011-05-12 10:13:45 -0700 | [diff] [blame] | 320 | * {@link Account#ACCOUNT_ID_COMBINED_VIEW} to register observers for all |
| 321 | * accounts that allow for user notification. |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 322 | */ |
| 323 | private void registerMessageNotification(long accountId) { |
| 324 | ContentResolver resolver = mContext.getContentResolver(); |
Todd Kennedy | 5701e0a | 2011-05-12 10:13:45 -0700 | [diff] [blame] | 325 | if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) { |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 326 | 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 Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 336 | } |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 337 | } else { |
Marc Blank | aca9426 | 2011-07-19 18:19:59 -0700 | [diff] [blame] | 338 | ContentObserver obs = mNotificationMap.get(accountId); |
| 339 | if (obs != null) return; // we're already observing; nothing to do |
Todd Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 340 | |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 341 | Mailbox mailbox = Mailbox.restoreMailboxOfType(mContext, accountId, Mailbox.TYPE_INBOX); |
Todd Kennedy | dfdc8b6 | 2011-05-11 13:40:52 -0700 | [diff] [blame] | 342 | if (mailbox == null) { |
| 343 | Log.w(Logging.LOG_TAG, "Could not load INBOX for account id: " + accountId); |
| 344 | return; |
| 345 | } |
Ben Komalo | eb9dcfa | 2011-07-21 11:02:51 -0700 | [diff] [blame] | 346 | if (Email.DEBUG) { |
| 347 | Log.i(Logging.LOG_TAG, "Registering for notifications for account " + accountId); |
| 348 | } |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 349 | ContentObserver observer = new MessageContentObserver( |
Todd Kennedy | e7fb4ac | 2011-05-11 15:29:24 -0700 | [diff] [blame] | 350 | sNotificationHandler, mContext, mailbox.mId, accountId); |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 351 | resolver.registerContentObserver(Message.NOTIFIER_URI, true, observer); |
Marc Blank | aca9426 | 2011-07-19 18:19:59 -0700 | [diff] [blame] | 352 | mNotificationMap.put(accountId, observer); |
Todd Kennedy | e7fb4ac | 2011-05-11 15:29:24 -0700 | [diff] [blame] | 353 | // Now, ping the observer for any initial notifications |
Marc Blank | aca9426 | 2011-07-19 18:19:59 -0700 | [diff] [blame] | 354 | observer.onChange(true); |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 355 | } |
| 356 | } |
| 357 | |
| 358 | /** |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 359 | * Unregisters the observer for the given account. If the specified account does not have |
Todd Kennedy | e7fb4ac | 2011-05-11 15:29:24 -0700 | [diff] [blame] | 360 | * 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 Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 362 | * NOTE: This must be called on the notification handler thread. |
Todd Kennedy | 5701e0a | 2011-05-12 10:13:45 -0700 | [diff] [blame] | 363 | * @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 Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 365 | */ |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 366 | private void unregisterMessageNotification(long accountId) { |
| 367 | ContentResolver resolver = mContext.getContentResolver(); |
Todd Kennedy | 5701e0a | 2011-05-12 10:13:45 -0700 | [diff] [blame] | 368 | if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) { |
Ben Komalo | eb9dcfa | 2011-07-21 11:02:51 -0700 | [diff] [blame] | 369 | if (Email.DEBUG) { |
| 370 | Log.i(Logging.LOG_TAG, "Unregistering notifications for all accounts"); |
| 371 | } |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 372 | // cancel all existing message observers |
Marc Blank | aca9426 | 2011-07-19 18:19:59 -0700 | [diff] [blame] | 373 | for (ContentObserver observer : mNotificationMap.values()) { |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 374 | resolver.unregisterContentObserver(observer); |
| 375 | } |
| 376 | mNotificationMap.clear(); |
Todd Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 377 | } else { |
Ben Komalo | eb9dcfa | 2011-07-21 11:02:51 -0700 | [diff] [blame] | 378 | if (Email.DEBUG) { |
| 379 | Log.i(Logging.LOG_TAG, "Unregistering notifications for account " + accountId); |
| 380 | } |
Marc Blank | aca9426 | 2011-07-19 18:19:59 -0700 | [diff] [blame] | 381 | ContentObserver observer = mNotificationMap.remove(accountId); |
| 382 | if (observer != null) { |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 383 | resolver.unregisterContentObserver(observer); |
| 384 | } |
| 385 | } |
| 386 | } |
| 387 | |
| 388 | /** |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 389 | * Returns a picture of the sender of the given message. If no picture is available, returns |
| 390 | * {@code null}. |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 391 | * |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 392 | * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 393 | */ |
| 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 Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 403 | return ContactStatusLoader.getContactInfo(mContext, email).mPhoto; |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 404 | } |
| 405 | |
Makoto Onuki | dd6e6b8 | 2011-03-08 16:36:47 -0800 | [diff] [blame] | 406 | /** |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 407 | * Returns a "new message" notification for the given account. |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 408 | * |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 409 | * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 410 | */ |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 411 | @VisibleForTesting |
Todd Kennedy | f437892 | 2011-05-13 08:41:48 -0700 | [diff] [blame] | 412 | Notification createNewMessageNotification(long accountId, long mailboxId, long messageId, |
Ben Komalo | 23a4b15 | 2011-07-22 11:41:51 -0700 | [diff] [blame] | 413 | int unseenMessageCount, int unreadCount) { |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 414 | final Account account = Account.restoreAccountWithId(mContext, accountId); |
| 415 | if (account == null) { |
| 416 | return null; |
| 417 | } |
| 418 | // Get the latest message |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 419 | final Message message = Message.restoreMessageWithId(mContext, messageId); |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 420 | if (message == null) { |
| 421 | return null; // no message found??? |
| 422 | } |
| 423 | |
Makoto Onuki | 43c455e | 2011-03-08 10:34:18 -0800 | [diff] [blame] | 424 | String senderName = Address.toFriendly(Address.unpack(message.mFrom)); |
| 425 | if (senderName == null) { |
| 426 | senderName = ""; // Happens when a message has no from. |
| 427 | } |
Ben Komalo | 6f93d2e | 2011-06-23 17:48:25 -0700 | [diff] [blame] | 428 | final boolean multipleUnseen = unseenMessageCount > 1; |
Ben Komalo | f13fee5 | 2011-08-17 17:10:06 -0700 | [diff] [blame] | 429 | final Bitmap senderPhoto = multipleUnseen |
| 430 | ? mGenericMultipleSenderIcon |
| 431 | : getSenderPhoto(message); |
Ben Komalo | 6f93d2e | 2011-06-23 17:48:25 -0700 | [diff] [blame] | 432 | 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 Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 438 | final Bitmap largeIcon = senderPhoto != null ? senderPhoto : mGenericSenderIcon; |
Ben Komalo | 6f93d2e | 2011-06-23 17:48:25 -0700 | [diff] [blame] | 439 | final Integer number = unreadCount > 1 ? unreadCount : null; |
Todd Kennedy | f437892 | 2011-05-13 08:41:48 -0700 | [diff] [blame] | 440 | final Intent intent; |
Ben Komalo | e918830 | 2011-07-10 14:02:50 -0700 | [diff] [blame] | 441 | if (unseenMessageCount > 1) { |
Todd Kennedy | f437892 | 2011-05-13 08:41:48 -0700 | [diff] [blame] | 442 | intent = Welcome.createOpenAccountInboxIntent(mContext, accountId); |
Ben Komalo | e918830 | 2011-07-10 14:02:50 -0700 | [diff] [blame] | 443 | } else { |
| 444 | intent = Welcome.createOpenMessageIntent(mContext, accountId, mailboxId, messageId); |
Todd Kennedy | f437892 | 2011-05-13 08:41:48 -0700 | [diff] [blame] | 445 | } |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 446 | |
Ben Komalo | 23a4b15 | 2011-07-22 11:41:51 -0700 | [diff] [blame] | 447 | long now = mClock.getTime(); |
Ben Komalo | 48a3a1c | 2011-07-22 13:29:48 -0700 | [diff] [blame] | 448 | boolean enableAudio = (now - mLastMessageNotifyTime) > MIN_SOUND_INTERVAL_MS; |
Ben Komalo | 23a4b15 | 2011-07-22 11:41:51 -0700 | [diff] [blame] | 449 | Notification notification = createAccountNotification( |
| 450 | account, title.toString(), title, text, |
Marc Blank | d9b2a8f | 2011-07-28 13:55:10 -0700 | [diff] [blame] | 451 | intent, largeIcon, number, enableAudio, false); |
Ben Komalo | 23a4b15 | 2011-07-22 11:41:51 -0700 | [diff] [blame] | 452 | mLastMessageNotifyTime = now; |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 453 | return notification; |
| 454 | } |
| 455 | |
Makoto Onuki | 74e0948 | 2010-12-03 14:44:47 -0800 | [diff] [blame] | 456 | /** |
Ben Komalo | 6f93d2e | 2011-06-23 17:48:25 -0700 | [diff] [blame] | 457 | * 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 Onuki | 74e0948 | 2010-12-03 14:44:47 -0800 | [diff] [blame] | 459 | */ |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 460 | @VisibleForTesting |
Ben Komalo | 6f93d2e | 2011-06-23 17:48:25 -0700 | [diff] [blame] | 461 | 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 Onuki | 74e0948 | 2010-12-03 14:44:47 -0800 | [diff] [blame] | 467 | } else { |
Ben Komalo | 6f93d2e | 2011-06-23 17:48:25 -0700 | [diff] [blame] | 468 | title = sender; |
Makoto Onuki | 74e0948 | 2010-12-03 14:44:47 -0800 | [diff] [blame] | 469 | } |
Ben Komalo | 6f93d2e | 2011-06-23 17:48:25 -0700 | [diff] [blame] | 470 | return new SpannableString(title); |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 471 | } |
| 472 | |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 473 | /** Returns the system's current ringer mode */ |
| 474 | @VisibleForTesting |
| 475 | int getRingerMode() { |
Makoto Onuki | 74e0948 | 2010-12-03 14:44:47 -0800 | [diff] [blame] | 476 | return mAudioManager.getRingerMode(); |
| 477 | } |
| 478 | |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 479 | /** Sets up the notification's sound and vibration based upon account details. */ |
| 480 | @VisibleForTesting |
| 481 | void setupSoundAndVibration(Notification.Builder builder, Account account) { |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 482 | 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 Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 486 | final boolean isRingerSilent = getRingerMode() != AudioManager.RINGER_MODE_NORMAL; |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 487 | |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 488 | int defaults = Notification.DEFAULT_LIGHTS; |
| 489 | if (vibrate || (vibrateWhenSilent && isRingerSilent)) { |
| 490 | defaults |= Notification.DEFAULT_VIBRATE; |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 491 | } |
| 492 | |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 493 | builder.setSound((ringtoneUri == null) ? null : Uri.parse(ringtoneUri)) |
| 494 | .setDefaults(defaults); |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 495 | } |
Marc Blank | d3e4f3c | 2010-10-18 13:14:20 -0700 | [diff] [blame] | 496 | |
| 497 | /** |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 498 | * 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 Stadler | c6d344a | 2011-02-16 16:38:18 -0800 | [diff] [blame] | 502 | * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) |
Marc Blank | d3e4f3c | 2010-10-18 13:14:20 -0700 | [diff] [blame] | 503 | */ |
Andy Stadler | c6d344a | 2011-02-16 16:38:18 -0800 | [diff] [blame] | 504 | public void showDownloadForwardFailedNotification(Attachment attachment) { |
| 505 | final Account account = Account.restoreAccountWithId(mContext, attachment.mAccountKey); |
| 506 | if (account == null) return; |
Makoto Onuki | 308ce92 | 2011-03-21 17:08:16 -0700 | [diff] [blame] | 507 | showAccountNotification(account, |
Marc Blank | d3e4f3c | 2010-10-18 13:14:20 -0700 | [diff] [blame] | 508 | mContext.getString(R.string.forward_download_failed_ticker), |
Andy Stadler | c6d344a | 2011-02-16 16:38:18 -0800 | [diff] [blame] | 509 | mContext.getString(R.string.forward_download_failed_title), |
| 510 | attachment.mFileName, |
| 511 | null, |
| 512 | NOTIFICATION_ID_ATTACHMENT_WARNING); |
Marc Blank | d3e4f3c | 2010-10-18 13:14:20 -0700 | [diff] [blame] | 513 | } |
| 514 | |
| 515 | /** |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 516 | * Returns a notification ID for login failed notifications for the given account account. |
Marc Blank | d3e4f3c | 2010-10-18 13:14:20 -0700 | [diff] [blame] | 517 | */ |
| 518 | private int getLoginFailedNotificationId(long accountId) { |
| 519 | return NOTIFICATION_ID_BASE_LOGIN_WARNING + (int)accountId; |
| 520 | } |
| 521 | |
Andy Stadler | c6d344a | 2011-02-16 16:38:18 -0800 | [diff] [blame] | 522 | /** |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 523 | * Show (or update) a notification that there was a login failure for the given account. |
| 524 | * |
Andy Stadler | c6d344a | 2011-02-16 16:38:18 -0800 | [diff] [blame] | 525 | * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) |
| 526 | */ |
Marc Blank | d3e4f3c | 2010-10-18 13:14:20 -0700 | [diff] [blame] | 527 | public void showLoginFailedNotification(long accountId) { |
| 528 | final Account account = Account.restoreAccountWithId(mContext, accountId); |
| 529 | if (account == null) return; |
Makoto Onuki | 308ce92 | 2011-03-21 17:08:16 -0700 | [diff] [blame] | 530 | showAccountNotification(account, |
Marc Blank | d3e4f3c | 2010-10-18 13:14:20 -0700 | [diff] [blame] | 531 | mContext.getString(R.string.login_failed_ticker, account.mDisplayName), |
Andy Stadler | c6d344a | 2011-02-16 16:38:18 -0800 | [diff] [blame] | 532 | mContext.getString(R.string.login_failed_title), |
| 533 | account.getDisplayName(), |
Ben Komalo | 2866284 | 2011-05-12 17:27:56 -0700 | [diff] [blame] | 534 | AccountSettings.createAccountSettingsIntent(mContext, accountId, |
Andy Stadler | f489413 | 2011-02-18 18:23:18 -0800 | [diff] [blame] | 535 | account.mDisplayName), |
Andy Stadler | c6d344a | 2011-02-16 16:38:18 -0800 | [diff] [blame] | 536 | getLoginFailedNotificationId(accountId)); |
Marc Blank | d3e4f3c | 2010-10-18 13:14:20 -0700 | [diff] [blame] | 537 | } |
| 538 | |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 539 | /** |
| 540 | * Cancels the login failed notification for the given account. |
| 541 | */ |
Marc Blank | d3e4f3c | 2010-10-18 13:14:20 -0700 | [diff] [blame] | 542 | public void cancelLoginFailedNotification(long accountId) { |
| 543 | mNotificationManager.cancel(getLoginFailedNotificationId(accountId)); |
| 544 | } |
Makoto Onuki | 308ce92 | 2011-03-21 17:08:16 -0700 | [diff] [blame] | 545 | |
| 546 | /** |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 547 | * 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 Onuki | 308ce92 | 2011-03-21 17:08:16 -0700 | [diff] [blame] | 549 | * |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 550 | * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) |
Makoto Onuki | 308ce92 | 2011-03-21 17:08:16 -0700 | [diff] [blame] | 551 | */ |
| 552 | public void showPasswordExpiringNotification(long accountId) { |
| 553 | Account account = Account.restoreAccountWithId(mContext, accountId); |
| 554 | if (account == null) return; |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 555 | |
Makoto Onuki | 308ce92 | 2011-03-21 17:08:16 -0700 | [diff] [blame] | 556 | Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext, |
| 557 | accountId, false); |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 558 | 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 Onuki | 308ce92 | 2011-03-21 17:08:16 -0700 | [diff] [blame] | 563 | NOTIFICATION_ID_PASSWORD_EXPIRING); |
| 564 | } |
| 565 | |
| 566 | /** |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 567 | * 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 Onuki | 308ce92 | 2011-03-21 17:08:16 -0700 | [diff] [blame] | 569 | * |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 570 | * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) |
Makoto Onuki | 308ce92 | 2011-03-21 17:08:16 -0700 | [diff] [blame] | 571 | */ |
| 572 | public void showPasswordExpiredNotification(long accountId) { |
| 573 | Account account = Account.restoreAccountWithId(mContext, accountId); |
| 574 | if (account == null) return; |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 575 | |
Makoto Onuki | 308ce92 | 2011-03-21 17:08:16 -0700 | [diff] [blame] | 576 | Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext, |
| 577 | accountId, true); |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 578 | String accountName = account.getDisplayName(); |
Makoto Onuki | 308ce92 | 2011-03-21 17:08:16 -0700 | [diff] [blame] | 579 | String ticker = mContext.getString(R.string.password_expired_ticker); |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 580 | String title = mContext.getString(R.string.password_expired_content_title); |
| 581 | showAccountNotification(account, ticker, title, accountName, intent, |
| 582 | NOTIFICATION_ID_PASSWORD_EXPIRED); |
Makoto Onuki | 308ce92 | 2011-03-21 17:08:16 -0700 | [diff] [blame] | 583 | } |
| 584 | |
| 585 | /** |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 586 | * Cancels any password expire notifications [both expired & expiring]. |
Makoto Onuki | 308ce92 | 2011-03-21 17:08:16 -0700 | [diff] [blame] | 587 | */ |
| 588 | public void cancelPasswordExpirationNotifications() { |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 589 | mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRING); |
| 590 | mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRED); |
Makoto Onuki | 308ce92 | 2011-03-21 17:08:16 -0700 | [diff] [blame] | 591 | } |
| 592 | |
| 593 | /** |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 594 | * 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 Onuki | 308ce92 | 2011-03-21 17:08:16 -0700 | [diff] [blame] | 596 | */ |
| 597 | public void showSecurityNeededNotification(Account account) { |
Makoto Onuki | 308ce92 | 2011-03-21 17:08:16 -0700 | [diff] [blame] | 598 | Intent intent = AccountSecurity.actionUpdateSecurityIntent(mContext, account.mId, true); |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 599 | 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 Onuki | 308ce92 | 2011-03-21 17:08:16 -0700 | [diff] [blame] | 604 | NOTIFICATION_ID_SECURITY_NEEDED); |
| 605 | } |
| 606 | |
| 607 | /** |
Todd Kennedy | 958b15e | 2011-04-28 11:09:41 -0700 | [diff] [blame] | 608 | * Cancels the security needed notification. |
Makoto Onuki | 308ce92 | 2011-03-21 17:08:16 -0700 | [diff] [blame] | 609 | */ |
| 610 | public void cancelSecurityNeededNotification() { |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 611 | mNotificationManager.cancel(NOTIFICATION_ID_SECURITY_NEEDED); |
Makoto Onuki | 308ce92 | 2011-03-21 17:08:16 -0700 | [diff] [blame] | 612 | } |
Todd Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 613 | |
| 614 | /** |
| 615 | * Observer invoked whenever a message we're notifying the user about changes. |
| 616 | */ |
| 617 | private static class MessageContentObserver extends ContentObserver { |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 618 | /** A selection to get messages the user hasn't seen before */ |
| 619 | private final static String MESSAGE_SELECTION = |
Ben Komalo | 48a3a1c | 2011-07-22 13:29:48 -0700 | [diff] [blame] | 620 | MessageColumns.MAILBOX_KEY + "=? AND " |
| 621 | + MessageColumns.ID + ">? AND " |
| 622 | + MessageColumns.FLAG_READ + "=0 AND " |
| 623 | + Message.FLAG_LOADED_SELECTION; |
Todd Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 624 | private final Context mContext; |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 625 | private final long mMailboxId; |
| 626 | private final long mAccountId; |
Todd Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 627 | |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 628 | public MessageContentObserver( |
| 629 | Handler handler, Context context, long mailboxId, long accountId) { |
| 630 | super(handler); |
Todd Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 631 | mContext = context; |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 632 | mMailboxId = mailboxId; |
Todd Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 633 | mAccountId = accountId; |
Todd Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 634 | } |
| 635 | |
| 636 | @Override |
| 637 | public void onChange(boolean selfChange) { |
Todd Kennedy | 5701e0a | 2011-05-12 10:13:45 -0700 | [diff] [blame] | 638 | if (mAccountId == sInstance.mSuspendAccountId |
| 639 | || sInstance.mSuspendAccountId == Account.ACCOUNT_ID_COMBINED_VIEW) { |
| 640 | return; |
| 641 | } |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 642 | |
Marc Blank | aca9426 | 2011-07-19 18:19:59 -0700 | [diff] [blame] | 643 | ContentObserver observer = sInstance.mNotificationMap.get(mAccountId); |
| 644 | if (observer == null) { |
Ben Komalo | eb9dcfa | 2011-07-21 11:02:51 -0700 | [diff] [blame] | 645 | // 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 Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 648 | return; |
| 649 | } |
Marc Blank | aca9426 | 2011-07-19 18:19:59 -0700 | [diff] [blame] | 650 | Account account = Account.restoreAccountWithId(mContext, mAccountId); |
Ben Komalo | eb9dcfa | 2011-07-21 11:02:51 -0700 | [diff] [blame] | 651 | if (account == null) { |
| 652 | Log.w(Logging.LOG_TAG, "Couldn't find account for changed message notification"); |
| 653 | return; |
| 654 | } |
Marc Blank | aca9426 | 2011-07-19 18:19:59 -0700 | [diff] [blame] | 655 | long oldMessageId = account.mNotifiedMessageId; |
| 656 | int oldMessageCount = account.mNotifiedMessageCount; |
Todd Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 657 | |
Todd Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 658 | ContentResolver resolver = mContext.getContentResolver(); |
Ben Komalo | eb9dcfa | 2011-07-21 11:02:51 -0700 | [diff] [blame] | 659 | Long lastSeenMessageId = Utility.getFirstRowLong( |
Marc Blank | 6e418aa | 2011-06-18 18:03:11 -0700 | [diff] [blame] | 660 | mContext, ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailboxId), |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 661 | new String[] { MailboxColumns.LAST_SEEN_MESSAGE_KEY }, |
Ben Komalo | eb9dcfa | 2011-07-21 11:02:51 -0700 | [diff] [blame] | 662 | 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 Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 669 | 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 Kennedy | e7fb4ac | 2011-05-11 15:29:24 -0700 | [diff] [blame] | 674 | if (c == null) { |
Ben Komalo | eb9dcfa | 2011-07-21 11:02:51 -0700 | [diff] [blame] | 675 | // 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 Kennedy | e7fb4ac | 2011-05-11 15:29:24 -0700 | [diff] [blame] | 677 | return; |
| 678 | } |
Todd Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 679 | try { |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 680 | int newMessageCount = c.getCount(); |
| 681 | long newMessageId = 0L; |
| 682 | if (c.moveToNext()) { |
| 683 | newMessageId = c.getLong(EmailContent.ID_PROJECTION_COLUMN); |
Todd Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 684 | } |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 685 | |
| 686 | if (newMessageCount == 0) { |
| 687 | // No messages to notify for; clear the notification |
Todd Kennedy | e7fb4ac | 2011-05-11 15:29:24 -0700 | [diff] [blame] | 688 | int notificationId = sInstance.getNewMessageNotificationId(mAccountId); |
| 689 | sInstance.mNotificationManager.cancel(notificationId); |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 690 | } else if (newMessageCount != oldMessageCount |
| 691 | || (newMessageId != 0 && newMessageId != oldMessageId)) { |
| 692 | // Either the count or last message has changed; update the notification |
Ben Komalo | eb9dcfa | 2011-07-21 11:02:51 -0700 | [diff] [blame] | 693 | Integer unreadCount = Utility.getFirstRowInt( |
Ben Komalo | 6f93d2e | 2011-06-23 17:48:25 -0700 | [diff] [blame] | 694 | mContext, ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailboxId), |
| 695 | new String[] { MailboxColumns.UNREAD_COUNT }, |
Ben Komalo | eb9dcfa | 2011-07-21 11:02:51 -0700 | [diff] [blame] | 696 | 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 Komalo | 6f93d2e | 2011-06-23 17:48:25 -0700 | [diff] [blame] | 701 | |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 702 | Notification n = sInstance.createNewMessageNotification( |
Ben Komalo | 6f93d2e | 2011-06-23 17:48:25 -0700 | [diff] [blame] | 703 | mAccountId, mMailboxId, newMessageId, |
Ben Komalo | 23a4b15 | 2011-07-22 11:41:51 -0700 | [diff] [blame] | 704 | newMessageCount, unreadCount); |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 705 | if (n != null) { |
| 706 | // Make the notification visible |
| 707 | sInstance.mNotificationManager.notify( |
| 708 | sInstance.getNewMessageNotificationId(mAccountId), n); |
Todd Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 709 | } |
Todd Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 710 | } |
Marc Blank | aca9426 | 2011-07-19 18:19:59 -0700 | [diff] [blame] | 711 | // 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 Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 717 | } finally { |
| 718 | c.close(); |
Todd Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 719 | } |
| 720 | } |
| 721 | } |
| 722 | |
Todd Kennedy | e7fb4ac | 2011-05-11 15:29:24 -0700 | [diff] [blame] | 723 | /** |
| 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 Kennedy | 83693a6 | 2011-05-10 11:36:57 -0700 | [diff] [blame] | 779 | /** |
| 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 Kennedy | c4cdb11 | 2011-05-03 14:42:26 -0700 | [diff] [blame] | 816 | } |
Makoto Onuki | 899c5b8 | 2010-09-26 16:16:21 -0700 | [diff] [blame] | 817 | } |