NotificationController.java revision 8d8537cd2e39268e0fdcd019bc8b6c4572b7c520
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 17package com.android.email; 18 19import com.android.email.activity.ContactStatusLoader; 20import com.android.email.activity.Welcome; 21import com.android.email.activity.setup.AccountSettingsXL; 22import com.android.email.mail.Address; 23import com.android.email.provider.EmailContent; 24import com.android.email.provider.EmailContent.Account; 25import com.android.email.provider.EmailContent.Attachment; 26import com.android.email.provider.EmailContent.Message; 27 28import android.app.Notification; 29import android.app.Notification.Builder; 30import android.app.NotificationManager; 31import android.app.PendingIntent; 32import android.content.Context; 33import android.content.Intent; 34import android.graphics.Bitmap; 35import android.graphics.BitmapFactory; 36import android.media.AudioManager; 37import android.net.Uri; 38import android.text.SpannableString; 39import android.text.TextUtils; 40import android.text.style.TextAppearanceSpan; 41 42/** 43 * Class that manages notifications. 44 * 45 * TODO Gather all notification related code here 46 */ 47public class NotificationController { 48 public static final int NOTIFICATION_ID_SECURITY_NEEDED = 1; 49 public static final int NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED = 2; 50 public static final int NOTIFICATION_ID_ATTACHMENT_WARNING = 3; 51 public static final int NOTIFICATION_ID_PASSWORD_EXPIRING = 4; 52 public static final int NOTIFICATION_ID_PASSWORD_EXPIRED = 5; 53 54 private static final int NOTIFICATION_ID_BASE_NEW_MESSAGES = 0x10000000; 55 private static final int NOTIFICATION_ID_BASE_LOGIN_WARNING = 0x20000000; 56 57 private static NotificationController sInstance; 58 private final Context mContext; 59 private final NotificationManager mNotificationManager; 60 private final AudioManager mAudioManager; 61 private final Bitmap mGenericSenderIcon; 62 private final Clock mClock; 63 64 /** Constructor */ 65 /* package */ NotificationController(Context context, Clock clock) { 66 mContext = context.getApplicationContext(); 67 mNotificationManager = (NotificationManager) context.getSystemService( 68 Context.NOTIFICATION_SERVICE); 69 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 70 mGenericSenderIcon = BitmapFactory.decodeResource(mContext.getResources(), 71 R.drawable.ic_contact_picture); 72 mClock = clock; 73 } 74 75 /** Singleton access */ 76 public static synchronized NotificationController getInstance(Context context) { 77 if (sInstance == null) { 78 sInstance = new NotificationController(context, Clock.INSTANCE); 79 } 80 return sInstance; 81 } 82 83 /** 84 * Generic notifier for any account. Uses notification rules from account. 85 * 86 * @param account The account for which the notification is posted 87 * @param ticker String for ticker 88 * @param contentTitle String for notification content title 89 * @param contentText String for notification content text 90 * @param intent The intent to launch from the notification 91 * @param notificationId The notification id 92 */ 93 public void postAccountNotification(Account account, String ticker, String contentTitle, 94 String contentText, Intent intent, int notificationId) { 95 96 // Pending Intent 97 PendingIntent pending = 98 PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); 99 100 // Ringtone & Vibration 101 String ringtoneString = account.getRingtone(); 102 Uri ringTone = (ringtoneString == null) ? null : Uri.parse(ringtoneString); 103 boolean vibrate = 0 != (account.mFlags & Account.FLAGS_VIBRATE_ALWAYS); 104 boolean vibrateWhenSilent = 0 != (account.mFlags & Account.FLAGS_VIBRATE_WHEN_SILENT); 105 106 // Use the account's notification rules for sound & vibrate (but always notify) 107 boolean nowSilent = 108 mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE; 109 110 int defaults = Notification.DEFAULT_LIGHTS; 111 if (vibrate || (vibrateWhenSilent && nowSilent)) { 112 defaults |= Notification.DEFAULT_VIBRATE; 113 } 114 115 // Notification 116 Notification.Builder nb = new Notification.Builder(mContext); 117 nb.setSmallIcon(R.drawable.stat_notify_email_generic); 118 nb.setTicker(ticker); 119 nb.setContentTitle(contentTitle); 120 nb.setContentText(contentText); 121 nb.setContentIntent(pending); 122 nb.setSound(ringTone); 123 nb.setDefaults(defaults); 124 Notification notification = nb.getNotification(); 125 126 mNotificationManager.notify(notificationId, notification); 127 } 128 129 /** 130 * Generic notification canceler. 131 * @param notificationId The notification id 132 */ 133 public void cancelNotification(int notificationId) { 134 mNotificationManager.cancel(notificationId); 135 } 136 137 /** 138 * @return the "new message" notification ID for an account. It just assumes 139 * accountID won't be too huge. Any other smarter/cleaner way? 140 */ 141 private int getNewMessageNotificationId(long accountId) { 142 return (int) (NOTIFICATION_ID_BASE_NEW_MESSAGES + accountId); 143 } 144 145 /** 146 * Dismiss new message notification 147 * 148 * @param accountId ID of the target account, or -1 for all accounts. 149 */ 150 public void cancelNewMessageNotification(long accountId) { 151 if (accountId == -1) { 152 new Utility.ForEachAccount(mContext) { 153 @Override 154 protected void performAction(long accountId) { 155 cancelNewMessageNotification(accountId); 156 } 157 }.execute(); 158 } else { 159 mNotificationManager.cancel(getNewMessageNotificationId(accountId)); 160 } 161 } 162 163 /** 164 * Show (or update) the "new message" notification. 165 */ 166 public void showNewMessageNotification(final long accountId, final int unseenMessageCount, 167 final int justFetchedCount) { 168 Utility.runAsync(new Runnable() { 169 @Override 170 public void run() { 171 Notification n = createNewMessageNotification(accountId, unseenMessageCount); 172 if (n == null) { 173 return; 174 } 175 mNotificationManager.notify(getNewMessageNotificationId(accountId), n); 176 } 177 }); 178 } 179 180 /** 181 * @return The sender's photo, if available, or null. 182 * 183 * Don't call it on the UI thread. 184 */ 185 private Bitmap getSenderPhoto(Message message) { 186 Address sender = Address.unpackFirst(message.mFrom); 187 if (sender == null) { 188 return null; 189 } 190 String email = sender.getAddress(); 191 if (TextUtils.isEmpty(email)) { 192 return null; 193 } 194 return ContactStatusLoader.load(mContext, email).mPhoto; 195 } 196 197 /** 198 * Create a notification 199 * 200 * Don't call it on the UI thread. 201 */ 202 /* package */ Notification createNewMessageNotification(long accountId, 203 int unseenMessageCount) { 204 final Account account = Account.restoreAccountWithId(mContext, accountId); 205 if (account == null) { 206 return null; 207 } 208 // Get the latest message 209 final Message message = Message.getLatestIncomingMessage(mContext, accountId); 210 if (message == null) { 211 return null; // no message found??? 212 } 213 214 final String senderName = Address.toFriendly(Address.unpack(message.mFrom)); 215 final String subject = message.mSubject; 216 final Bitmap senderPhoto = getSenderPhoto(message); 217 218 // Intent to open inbox 219 PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0, 220 Welcome.createOpenAccountInboxIntent(mContext, accountId), 221 PendingIntent.FLAG_UPDATE_CURRENT); 222 223 Notification.Builder builder = new Notification.Builder(mContext) 224 .setSmallIcon(R.drawable.stat_notify_email_generic) 225 .setWhen(mClock.getTime()) 226 .setLargeIcon(senderPhoto != null ? senderPhoto : mGenericSenderIcon) 227 .setContentTitle(getNotificationTitle(senderName, account.mDisplayName)) 228 .setContentText(subject) 229 .setContentIntent(contentIntent); 230 if (unseenMessageCount > 1) { 231 builder.setNumber(unseenMessageCount); 232 } 233 234 Notification notification = builder.getNotification(); 235 236 setupNotificationSoundAndVibrationFromAccount(notification, account); 237 return notification; 238 } 239 240 /** 241 * Creates the notification title. 242 * 243 * If only 1 account, just show the sender name. 244 * If 2+ accounts, make it "SENDER_NAME to RECEIVER_NAME", and gray out the "to RECEIVER_NAME" 245 * part. 246 */ 247 /* package */ SpannableString getNotificationTitle(String sender, String receiverDisplayName) { 248 final int numAccounts = EmailContent.count(mContext, Account.CONTENT_URI); 249 if (numAccounts == 1) { 250 return new SpannableString(sender); 251 } else { 252 // "to [account name]" 253 String toAcccount = mContext.getResources().getString(R.string.notification_to_account, 254 receiverDisplayName); 255 // "[Sender] to [account name]" 256 SpannableString senderToAccount = new SpannableString(sender + " " + toAcccount); 257 258 // "[Sender] to [account name]" 259 // ^^^^^^^^^^^^^^^^^ <- Make this part gray 260 TextAppearanceSpan secondarySpan = new TextAppearanceSpan( 261 mContext, R.style.notification_secondary_text); 262 senderToAccount.setSpan(secondarySpan, sender.length() + 1, senderToAccount.length(), 263 0); 264 return senderToAccount; 265 } 266 } 267 268 // Overridden for testing (AudioManager can't be mocked out.) 269 /* package */ int getRingerMode() { 270 return mAudioManager.getRingerMode(); 271 } 272 273 /* package */ boolean isRingerModeSilent() { 274 return getRingerMode() != AudioManager.RINGER_MODE_NORMAL; 275 } 276 277 /* package */ void setupNotificationSoundAndVibrationFromAccount(Notification notification, 278 Account account) { 279 final int flags = account.mFlags; 280 final String ringtoneUri = account.mRingtoneUri; 281 final boolean vibrate = (flags & Account.FLAGS_VIBRATE_ALWAYS) != 0; 282 final boolean vibrateWhenSilent = (flags & Account.FLAGS_VIBRATE_WHEN_SILENT) != 0; 283 284 notification.sound = (ringtoneUri == null) ? null : Uri.parse(ringtoneUri); 285 286 if (vibrate || (vibrateWhenSilent && isRingerModeSilent())) { 287 notification.defaults |= Notification.DEFAULT_VIBRATE; 288 } 289 290 // This code is identical to that used by Gmail and GTalk for notifications 291 notification.flags |= Notification.FLAG_SHOW_LIGHTS; 292 notification.defaults |= Notification.DEFAULT_LIGHTS; 293 } 294 295 /** 296 * Generic warning notification 297 */ 298 public void showWarningNotification(int id, String tickerText, String notificationText, 299 Intent intent) { 300 PendingIntent pendingIntent = null; 301 if (intent != null) { 302 pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 303 PendingIntent.FLAG_UPDATE_CURRENT); 304 } 305 Builder b = new Builder(mContext); 306 b.setSmallIcon(android.R.drawable.stat_notify_error) 307 .setTicker(tickerText) 308 .setWhen(mClock.getTime()) 309 .setContentTitle(tickerText) 310 .setContentText(notificationText) 311 .setContentIntent(pendingIntent) 312 .setAutoCancel(true); 313 Notification n = b.getNotification(); 314 mNotificationManager.notify(id, n); 315 } 316 317 /** 318 * Alert the user that an attachment couldn't be forwarded. This is a very unusual case, and 319 * perhaps we shouldn't even send a notification. For now, it's helpful for debugging. 320 */ 321 public void showDownloadForwardFailedNotification(Attachment att) { 322 showWarningNotification(NOTIFICATION_ID_ATTACHMENT_WARNING, 323 mContext.getString(R.string.forward_download_failed_ticker), 324 mContext.getString(R.string.forward_download_failed_notification, 325 att.mFileName), null); 326 } 327 328 /** 329 * Alert the user that login failed for the specified account 330 */ 331 private int getLoginFailedNotificationId(long accountId) { 332 return NOTIFICATION_ID_BASE_LOGIN_WARNING + (int)accountId; 333 } 334 335 // NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS) 336 public void showLoginFailedNotification(long accountId) { 337 final Account account = Account.restoreAccountWithId(mContext, accountId); 338 if (account == null) return; 339 showWarningNotification(getLoginFailedNotificationId(accountId), 340 mContext.getString(R.string.login_failed_ticker, account.mDisplayName), 341 mContext.getString(R.string.login_failed_notification), 342 AccountSettingsXL.createAccountSettingsIntent(mContext, accountId)); 343 } 344 345 public void cancelLoginFailedNotification(long accountId) { 346 mNotificationManager.cancel(getLoginFailedNotificationId(accountId)); 347 } 348} 349