NotificationMgr.java revision 528bd1e5b3b2a1ba37ff9815b79b7a59a77235ca
1/* 2 * Copyright (C) 2006 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.phone; 18 19import android.app.Notification; 20import android.app.NotificationManager; 21import android.app.PendingIntent; 22import android.app.StatusBarManager; 23import android.content.AsyncQueryHandler; 24import android.content.ComponentName; 25import android.content.ContentResolver; 26import android.content.ContentUris; 27import android.content.Context; 28import android.content.Intent; 29import android.content.SharedPreferences; 30import android.database.Cursor; 31import android.graphics.Bitmap; 32import android.graphics.drawable.BitmapDrawable; 33import android.graphics.drawable.Drawable; 34import android.media.AudioManager; 35import android.net.Uri; 36import android.os.PowerManager; 37import android.os.SystemProperties; 38import android.preference.PreferenceManager; 39import android.provider.CallLog.Calls; 40import android.provider.ContactsContract.Contacts; 41import android.provider.ContactsContract.PhoneLookup; 42import android.provider.Settings; 43import android.telephony.PhoneNumberUtils; 44import android.telephony.ServiceState; 45import android.text.BidiFormatter; 46import android.text.TextDirectionHeuristics; 47import android.text.TextUtils; 48import android.util.Log; 49import android.widget.Toast; 50 51import com.android.internal.telephony.Call; 52import com.android.internal.telephony.CallManager; 53import com.android.internal.telephony.CallerInfo; 54import com.android.internal.telephony.CallerInfoAsyncQuery; 55import com.android.internal.telephony.Connection; 56import com.android.internal.telephony.Phone; 57import com.android.internal.telephony.PhoneBase; 58import com.android.internal.telephony.PhoneConstants; 59import com.android.internal.telephony.TelephonyCapabilities; 60 61/** 62 * NotificationManager-related utility code for the Phone app. 63 * 64 * This is a singleton object which acts as the interface to the 65 * framework's NotificationManager, and is used to display status bar 66 * icons and control other status bar-related behavior. 67 * 68 * @see PhoneGlobals.notificationMgr 69 */ 70public class NotificationMgr implements CallerInfoAsyncQuery.OnQueryCompleteListener{ 71 private static final String LOG_TAG = "NotificationMgr"; 72 private static final boolean DBG = 73 (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1); 74 // Do not check in with VDBG = true, since that may write PII to the system log. 75 private static final boolean VDBG = false; 76 77 private static final String[] CALL_LOG_PROJECTION = new String[] { 78 Calls._ID, 79 Calls.NUMBER, 80 Calls.NUMBER_PRESENTATION, 81 Calls.DATE, 82 Calls.DURATION, 83 Calls.TYPE, 84 }; 85 86 // notification types 87 static final int MISSED_CALL_NOTIFICATION = 1; 88 static final int IN_CALL_NOTIFICATION = 2; 89 static final int MMI_NOTIFICATION = 3; 90 static final int NETWORK_SELECTION_NOTIFICATION = 4; 91 static final int VOICEMAIL_NOTIFICATION = 5; 92 static final int CALL_FORWARD_NOTIFICATION = 6; 93 static final int DATA_DISCONNECTED_ROAMING_NOTIFICATION = 7; 94 static final int SELECTED_OPERATOR_FAIL_NOTIFICATION = 8; 95 96 /** The singleton NotificationMgr instance. */ 97 private static NotificationMgr sInstance; 98 99 private PhoneGlobals mApp; 100 private Phone mPhone; 101 private CallManager mCM; 102 103 private Context mContext; 104 private NotificationManager mNotificationManager; 105 private StatusBarManager mStatusBarManager; 106 private PowerManager mPowerManager; 107 private Toast mToast; 108 private boolean mShowingSpeakerphoneIcon; 109 private boolean mShowingMuteIcon; 110 111 public StatusBarHelper statusBarHelper; 112 113 // used to track the missed call counter, default to 0. 114 private int mNumberMissedCalls = 0; 115 116 // Currently-displayed resource IDs for some status bar icons (or zero 117 // if no notification is active): 118 private int mInCallResId; 119 120 // used to track the notification of selected network unavailable 121 private boolean mSelectedUnavailableNotify = false; 122 123 // Retry params for the getVoiceMailNumber() call; see updateMwi(). 124 private static final int MAX_VM_NUMBER_RETRIES = 5; 125 private static final int VM_NUMBER_RETRY_DELAY_MILLIS = 10000; 126 private int mVmNumberRetriesRemaining = MAX_VM_NUMBER_RETRIES; 127 128 // Query used to look up caller-id info for the "call log" notification. 129 private QueryHandler mQueryHandler = null; 130 private static final int CALL_LOG_TOKEN = -1; 131 private static final int CONTACT_TOKEN = -2; 132 133 /** 134 * Private constructor (this is a singleton). 135 * @see init() 136 */ 137 private NotificationMgr(PhoneGlobals app) { 138 mApp = app; 139 mContext = app; 140 mNotificationManager = 141 (NotificationManager) app.getSystemService(Context.NOTIFICATION_SERVICE); 142 mStatusBarManager = 143 (StatusBarManager) app.getSystemService(Context.STATUS_BAR_SERVICE); 144 mPowerManager = 145 (PowerManager) app.getSystemService(Context.POWER_SERVICE); 146 mPhone = app.phone; // TODO: better style to use mCM.getDefaultPhone() everywhere instead 147 mCM = app.mCM; 148 statusBarHelper = new StatusBarHelper(); 149 } 150 151 /** 152 * Initialize the singleton NotificationMgr instance. 153 * 154 * This is only done once, at startup, from PhoneApp.onCreate(). 155 * From then on, the NotificationMgr instance is available via the 156 * PhoneApp's public "notificationMgr" field, which is why there's no 157 * getInstance() method here. 158 */ 159 /* package */ static NotificationMgr init(PhoneGlobals app) { 160 synchronized (NotificationMgr.class) { 161 if (sInstance == null) { 162 sInstance = new NotificationMgr(app); 163 // Update the notifications that need to be touched at startup. 164 sInstance.updateNotificationsAtStartup(); 165 } else { 166 Log.wtf(LOG_TAG, "init() called multiple times! sInstance = " + sInstance); 167 } 168 return sInstance; 169 } 170 } 171 172 /** 173 * Helper class that's a wrapper around the framework's 174 * StatusBarManager.disable() API. 175 * 176 * This class is used to control features like: 177 * 178 * - Disabling the status bar "notification windowshade" 179 * while the in-call UI is up 180 * 181 * - Disabling notification alerts (audible or vibrating) 182 * while a phone call is active 183 * 184 * - Disabling navigation via the system bar (the "soft buttons" at 185 * the bottom of the screen on devices with no hard buttons) 186 * 187 * We control these features through a single point of control to make 188 * sure that the various StatusBarManager.disable() calls don't 189 * interfere with each other. 190 */ 191 public class StatusBarHelper { 192 // Current desired state of status bar / system bar behavior 193 private boolean mIsNotificationEnabled = true; 194 private boolean mIsExpandedViewEnabled = true; 195 private boolean mIsSystemBarNavigationEnabled = true; 196 197 private StatusBarHelper () { 198 } 199 200 /** 201 * Enables or disables auditory / vibrational alerts. 202 * 203 * (We disable these any time a voice call is active, regardless 204 * of whether or not the in-call UI is visible.) 205 */ 206 public void enableNotificationAlerts(boolean enable) { 207 if (mIsNotificationEnabled != enable) { 208 mIsNotificationEnabled = enable; 209 updateStatusBar(); 210 } 211 } 212 213 /** 214 * Enables or disables the expanded view of the status bar 215 * (i.e. the ability to pull down the "notification windowshade"). 216 * 217 * (This feature is disabled by the InCallScreen while the in-call 218 * UI is active.) 219 */ 220 public void enableExpandedView(boolean enable) { 221 if (mIsExpandedViewEnabled != enable) { 222 mIsExpandedViewEnabled = enable; 223 updateStatusBar(); 224 } 225 } 226 227 /** 228 * Enables or disables the navigation via the system bar (the 229 * "soft buttons" at the bottom of the screen) 230 * 231 * (This feature is disabled while an incoming call is ringing, 232 * because it's easy to accidentally touch the system bar while 233 * pulling the phone out of your pocket.) 234 */ 235 public void enableSystemBarNavigation(boolean enable) { 236 if (mIsSystemBarNavigationEnabled != enable) { 237 mIsSystemBarNavigationEnabled = enable; 238 updateStatusBar(); 239 } 240 } 241 242 /** 243 * Updates the status bar to reflect the current desired state. 244 */ 245 private void updateStatusBar() { 246 int state = StatusBarManager.DISABLE_NONE; 247 248 if (!mIsExpandedViewEnabled) { 249 state |= StatusBarManager.DISABLE_EXPAND; 250 } 251 if (!mIsNotificationEnabled) { 252 state |= StatusBarManager.DISABLE_NOTIFICATION_ALERTS; 253 } 254 if (!mIsSystemBarNavigationEnabled) { 255 // Disable *all* possible navigation via the system bar. 256 state |= StatusBarManager.DISABLE_HOME; 257 state |= StatusBarManager.DISABLE_RECENT; 258 state |= StatusBarManager.DISABLE_BACK; 259 } 260 261 if (DBG) log("updateStatusBar: state = 0x" + Integer.toHexString(state)); 262 mStatusBarManager.disable(state); 263 } 264 } 265 266 /** 267 * Makes sure phone-related notifications are up to date on a 268 * freshly-booted device. 269 */ 270 private void updateNotificationsAtStartup() { 271 if (DBG) log("updateNotificationsAtStartup()..."); 272 273 // instantiate query handler 274 mQueryHandler = new QueryHandler(mContext.getContentResolver()); 275 276 // setup query spec, look for all Missed calls that are new. 277 StringBuilder where = new StringBuilder("type="); 278 where.append(Calls.MISSED_TYPE); 279 where.append(" AND new=1"); 280 281 // start the query 282 if (DBG) log("- start call log query..."); 283 mQueryHandler.startQuery(CALL_LOG_TOKEN, null, Calls.CONTENT_URI, CALL_LOG_PROJECTION, 284 where.toString(), null, Calls.DEFAULT_SORT_ORDER); 285 286 // Update (or cancel) the in-call notification 287 if (DBG) log("- updating in-call notification at startup..."); 288 updateInCallNotification(); 289 290 // Depend on android.app.StatusBarManager to be set to 291 // disable(DISABLE_NONE) upon startup. This will be the 292 // case even if the phone app crashes. 293 } 294 295 /** The projection to use when querying the phones table */ 296 static final String[] PHONES_PROJECTION = new String[] { 297 PhoneLookup.NUMBER, 298 PhoneLookup.DISPLAY_NAME, 299 PhoneLookup._ID 300 }; 301 302 /** 303 * Class used to run asynchronous queries to re-populate the notifications we care about. 304 * There are really 3 steps to this: 305 * 1. Find the list of missed calls 306 * 2. For each call, run a query to retrieve the caller's name. 307 * 3. For each caller, try obtaining photo. 308 */ 309 private class QueryHandler extends AsyncQueryHandler 310 implements ContactsAsyncHelper.OnImageLoadCompleteListener { 311 312 /** 313 * Used to store relevant fields for the Missed Call 314 * notifications. 315 */ 316 private class NotificationInfo { 317 public String name; 318 public String number; 319 public int presentation; 320 /** 321 * Type of the call. {@link android.provider.CallLog.Calls#INCOMING_TYPE} 322 * {@link android.provider.CallLog.Calls#OUTGOING_TYPE}, or 323 * {@link android.provider.CallLog.Calls#MISSED_TYPE}. 324 */ 325 public String type; 326 public long date; 327 } 328 329 public QueryHandler(ContentResolver cr) { 330 super(cr); 331 } 332 333 /** 334 * Handles the query results. 335 */ 336 @Override 337 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 338 // TODO: it would be faster to use a join here, but for the purposes 339 // of this small record set, it should be ok. 340 341 // Note that CursorJoiner is not useable here because the number 342 // comparisons are not strictly equals; the comparisons happen in 343 // the SQL function PHONE_NUMBERS_EQUAL, which is not available for 344 // the CursorJoiner. 345 346 // Executing our own query is also feasible (with a join), but that 347 // will require some work (possibly destabilizing) in Contacts 348 // Provider. 349 350 // At this point, we will execute subqueries on each row just as 351 // CallLogActivity.java does. 352 switch (token) { 353 case CALL_LOG_TOKEN: 354 if (DBG) log("call log query complete."); 355 356 // initial call to retrieve the call list. 357 if (cursor != null) { 358 while (cursor.moveToNext()) { 359 // for each call in the call log list, create 360 // the notification object and query contacts 361 NotificationInfo n = getNotificationInfo (cursor); 362 363 if (DBG) log("query contacts for number: " + n.number); 364 365 mQueryHandler.startQuery(CONTACT_TOKEN, n, 366 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, n.number), 367 PHONES_PROJECTION, null, null, PhoneLookup.NUMBER); 368 } 369 370 if (DBG) log("closing call log cursor."); 371 cursor.close(); 372 } 373 break; 374 case CONTACT_TOKEN: 375 if (DBG) log("contact query complete."); 376 377 // subqueries to get the caller name. 378 if ((cursor != null) && (cookie != null)){ 379 NotificationInfo n = (NotificationInfo) cookie; 380 381 Uri personUri = null; 382 if (cursor.moveToFirst()) { 383 n.name = cursor.getString( 384 cursor.getColumnIndexOrThrow(PhoneLookup.DISPLAY_NAME)); 385 long person_id = cursor.getLong( 386 cursor.getColumnIndexOrThrow(PhoneLookup._ID)); 387 if (DBG) { 388 log("contact :" + n.name + " found for phone: " + n.number 389 + ". id : " + person_id); 390 } 391 personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, person_id); 392 } 393 394 if (personUri != null) { 395 if (DBG) { 396 log("Start obtaining picture for the missed call. Uri: " 397 + personUri); 398 } 399 // Now try to obtain a photo for this person. 400 // ContactsAsyncHelper will do that and call onImageLoadComplete() 401 // after that. 402 ContactsAsyncHelper.startObtainPhotoAsync( 403 0, mContext, personUri, this, n); 404 } else { 405 if (DBG) { 406 log("Failed to find Uri for obtaining photo." 407 + " Just send notification without it."); 408 } 409 // We couldn't find person Uri, so we're sure we cannot obtain a photo. 410 // Call notifyMissedCall() right now. 411 notifyMissedCall(n.name, n.number, n.type, null, null, n.date); 412 } 413 414 if (DBG) log("closing contact cursor."); 415 cursor.close(); 416 } 417 break; 418 default: 419 } 420 } 421 422 @Override 423 public void onImageLoadComplete( 424 int token, Drawable photo, Bitmap photoIcon, Object cookie) { 425 if (DBG) log("Finished loading image: " + photo); 426 NotificationInfo n = (NotificationInfo) cookie; 427 notifyMissedCall(n.name, n.number, n.type, photo, photoIcon, n.date); 428 } 429 430 /** 431 * Factory method to generate a NotificationInfo object given a 432 * cursor from the call log table. 433 */ 434 private final NotificationInfo getNotificationInfo(Cursor cursor) { 435 NotificationInfo n = new NotificationInfo(); 436 n.name = null; 437 n.number = cursor.getString(cursor.getColumnIndexOrThrow(Calls.NUMBER)); 438 n.presentation = cursor.getInt(cursor.getColumnIndexOrThrow(Calls.NUMBER_PRESENTATION)); 439 n.type = cursor.getString(cursor.getColumnIndexOrThrow(Calls.TYPE)); 440 n.date = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DATE)); 441 442 // make sure we update the number depending upon saved values in 443 // CallLog.addCall(). If either special values for unknown or 444 // private number are detected, we need to hand off the message 445 // to the missed call notification. 446 if (n.presentation != Calls.PRESENTATION_ALLOWED) { 447 n.number = null; 448 } 449 450 if (DBG) log("NotificationInfo constructed for number: " + n.number); 451 452 return n; 453 } 454 } 455 456 /** 457 * Configures a Notification to emit the blinky green message-waiting/ 458 * missed-call signal. 459 */ 460 private static void configureLedNotification(Notification note) { 461 note.flags |= Notification.FLAG_SHOW_LIGHTS; 462 note.defaults |= Notification.DEFAULT_LIGHTS; 463 } 464 465 /** 466 * Displays a notification about a missed call. 467 * 468 * @param name the contact name. 469 * @param number the phone number. Note that this may be a non-callable String like "Unknown", 470 * or "Private Number", which possibly come from methods like 471 * {@link PhoneUtils#modifyForSpecialCnapCases(Context, CallerInfo, String, int)}. 472 * @param type the type of the call. {@link android.provider.CallLog.Calls#INCOMING_TYPE} 473 * {@link android.provider.CallLog.Calls#OUTGOING_TYPE}, or 474 * {@link android.provider.CallLog.Calls#MISSED_TYPE} 475 * @param photo picture which may be used for the notification (when photoIcon is null). 476 * This also can be null when the picture itself isn't available. If photoIcon is available 477 * it should be prioritized (because this may be too huge for notification). 478 * See also {@link ContactsAsyncHelper}. 479 * @param photoIcon picture which should be used for the notification. Can be null. This is 480 * the most suitable for {@link android.app.Notification.Builder#setLargeIcon(Bitmap)}, this 481 * should be used when non-null. 482 * @param date the time when the missed call happened 483 */ 484 /* package */ void notifyMissedCall( 485 String name, String number, String type, Drawable photo, Bitmap photoIcon, long date) { 486 487 // When the user clicks this notification, we go to the call log. 488 final PendingIntent pendingCallLogIntent = PhoneGlobals.createPendingCallLogIntent( 489 mContext); 490 491 // Never display the missed call notification on non-voice-capable 492 // devices, even if the device does somehow manage to get an 493 // incoming call. 494 if (!PhoneGlobals.sVoiceCapable) { 495 if (DBG) log("notifyMissedCall: non-voice-capable device, not posting notification"); 496 return; 497 } 498 499 if (VDBG) { 500 log("notifyMissedCall(). name: " + name + ", number: " + number 501 + ", label: " + type + ", photo: " + photo + ", photoIcon: " + photoIcon 502 + ", date: " + date); 503 } 504 505 // title resource id 506 int titleResId; 507 // the text in the notification's line 1 and 2. 508 String expandedText, callName; 509 510 // increment number of missed calls. 511 mNumberMissedCalls++; 512 513 // get the name for the ticker text 514 // i.e. "Missed call from <caller name or number>" 515 if (name != null && TextUtils.isGraphic(name)) { 516 callName = name; 517 } else if (!TextUtils.isEmpty(number)){ 518 final BidiFormatter bidiFormatter = BidiFormatter.getInstance(); 519 // A number should always be displayed LTR using {@link BidiFormatter} 520 // regardless of the content of the rest of the notification. 521 callName = bidiFormatter.unicodeWrap(number, TextDirectionHeuristics.LTR); 522 } else { 523 // use "unknown" if the caller is unidentifiable. 524 callName = mContext.getString(R.string.unknown); 525 } 526 527 // display the first line of the notification: 528 // 1 missed call: call name 529 // more than 1 missed call: <number of calls> + "missed calls" 530 if (mNumberMissedCalls == 1) { 531 titleResId = R.string.notification_missedCallTitle; 532 expandedText = callName; 533 } else { 534 titleResId = R.string.notification_missedCallsTitle; 535 expandedText = mContext.getString(R.string.notification_missedCallsMsg, 536 mNumberMissedCalls); 537 } 538 539 Notification.Builder builder = new Notification.Builder(mContext); 540 builder.setSmallIcon(android.R.drawable.stat_notify_missed_call) 541 .setTicker(mContext.getString(R.string.notification_missedCallTicker, callName)) 542 .setWhen(date) 543 .setContentTitle(mContext.getText(titleResId)) 544 .setContentText(expandedText) 545 .setContentIntent(pendingCallLogIntent) 546 .setAutoCancel(true) 547 .setDeleteIntent(createClearMissedCallsIntent()); 548 549 // Simple workaround for issue 6476275; refrain having actions when the given number seems 550 // not a real one but a non-number which was embedded by methods outside (like 551 // PhoneUtils#modifyForSpecialCnapCases()). 552 // TODO: consider removing equals() checks here, and modify callers of this method instead. 553 if (mNumberMissedCalls == 1 554 && !TextUtils.isEmpty(number) 555 && !TextUtils.equals(number, mContext.getString(R.string.private_num)) 556 && !TextUtils.equals(number, mContext.getString(R.string.unknown))){ 557 if (DBG) log("Add actions with the number " + number); 558 559 builder.addAction(R.drawable.stat_sys_phone_call, 560 mContext.getString(R.string.notification_missedCall_call_back), 561 PhoneGlobals.getCallBackPendingIntent(mContext, number)); 562 563 builder.addAction(R.drawable.ic_text_holo_dark, 564 mContext.getString(R.string.notification_missedCall_message), 565 PhoneGlobals.getSendSmsFromNotificationPendingIntent(mContext, number)); 566 567 if (photoIcon != null) { 568 builder.setLargeIcon(photoIcon); 569 } else if (photo instanceof BitmapDrawable) { 570 builder.setLargeIcon(((BitmapDrawable) photo).getBitmap()); 571 } 572 } else { 573 if (DBG) { 574 log("Suppress actions. number: " + number + ", missedCalls: " + mNumberMissedCalls); 575 } 576 } 577 578 Notification notification = builder.getNotification(); 579 configureLedNotification(notification); 580 mNotificationManager.notify(MISSED_CALL_NOTIFICATION, notification); 581 } 582 583 /** Returns an intent to be invoked when the missed call notification is cleared. */ 584 private PendingIntent createClearMissedCallsIntent() { 585 Intent intent = new Intent(mContext, ClearMissedCallsService.class); 586 intent.setAction(ClearMissedCallsService.ACTION_CLEAR_MISSED_CALLS); 587 return PendingIntent.getService(mContext, 0, intent, 0); 588 } 589 590 /** 591 * Cancels the "missed call" notification. 592 * 593 * @see ITelephony.cancelMissedCallsNotification() 594 */ 595 void cancelMissedCallNotification() { 596 // reset the number of missed calls to 0. 597 mNumberMissedCalls = 0; 598 mNotificationManager.cancel(MISSED_CALL_NOTIFICATION); 599 } 600 601 private void notifySpeakerphone() { 602 if (!mShowingSpeakerphoneIcon) { 603 mStatusBarManager.setIcon("speakerphone", android.R.drawable.stat_sys_speakerphone, 0, 604 mContext.getString(R.string.accessibility_speakerphone_enabled)); 605 mShowingSpeakerphoneIcon = true; 606 } 607 } 608 609 private void cancelSpeakerphone() { 610 if (mShowingSpeakerphoneIcon) { 611 mStatusBarManager.removeIcon("speakerphone"); 612 mShowingSpeakerphoneIcon = false; 613 } 614 } 615 616 /** 617 * Shows or hides the "speakerphone" notification in the status bar, 618 * based on the actual current state of the speaker. 619 * 620 * If you already know the current speaker state (e.g. if you just 621 * called AudioManager.setSpeakerphoneOn() yourself) then you should 622 * directly call {@link #updateSpeakerNotification(boolean)} instead. 623 * 624 * (But note that the status bar icon is *never* shown while the in-call UI 625 * is active; it only appears if you bail out to some other activity.) 626 */ 627 private void updateSpeakerNotification() { 628 AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); 629 boolean showNotification = 630 (mPhone.getState() == PhoneConstants.State.OFFHOOK) && audioManager.isSpeakerphoneOn(); 631 632 if (DBG) log(showNotification 633 ? "updateSpeakerNotification: speaker ON" 634 : "updateSpeakerNotification: speaker OFF (or not offhook)"); 635 636 updateSpeakerNotification(showNotification); 637 } 638 639 /** 640 * Shows or hides the "speakerphone" notification in the status bar. 641 * 642 * @param showNotification if true, call notifySpeakerphone(); 643 * if false, call cancelSpeakerphone(). 644 * 645 * Use {@link updateSpeakerNotification()} to update the status bar 646 * based on the actual current state of the speaker. 647 * 648 * (But note that the status bar icon is *never* shown while the in-call UI 649 * is active; it only appears if you bail out to some other activity.) 650 */ 651 public void updateSpeakerNotification(boolean showNotification) { 652 if (DBG) log("updateSpeakerNotification(" + showNotification + ")..."); 653 654 // Regardless of the value of the showNotification param, suppress 655 // the status bar icon if the the InCallScreen is the foreground 656 // activity, since the in-call UI already provides an onscreen 657 // indication of the speaker state. (This reduces clutter in the 658 // status bar.) 659 if (mApp.isShowingCallScreen()) { 660 cancelSpeakerphone(); 661 return; 662 } 663 664 if (showNotification) { 665 notifySpeakerphone(); 666 } else { 667 cancelSpeakerphone(); 668 } 669 } 670 671 private void notifyMute() { 672 if (!mShowingMuteIcon) { 673 mStatusBarManager.setIcon("mute", android.R.drawable.stat_notify_call_mute, 0, 674 mContext.getString(R.string.accessibility_call_muted)); 675 mShowingMuteIcon = true; 676 } 677 } 678 679 private void cancelMute() { 680 if (mShowingMuteIcon) { 681 mStatusBarManager.removeIcon("mute"); 682 mShowingMuteIcon = false; 683 } 684 } 685 686 /** 687 * Shows or hides the "mute" notification in the status bar, 688 * based on the current mute state of the Phone. 689 * 690 * (But note that the status bar icon is *never* shown while the in-call UI 691 * is active; it only appears if you bail out to some other activity.) 692 */ 693 void updateMuteNotification() { 694 // Suppress the status bar icon if the the InCallScreen is the 695 // foreground activity, since the in-call UI already provides an 696 // onscreen indication of the mute state. (This reduces clutter 697 // in the status bar.) 698 if (mApp.isShowingCallScreen()) { 699 cancelMute(); 700 return; 701 } 702 703 if ((mCM.getState() == PhoneConstants.State.OFFHOOK) && PhoneUtils.getMute()) { 704 if (DBG) log("updateMuteNotification: MUTED"); 705 notifyMute(); 706 } else { 707 if (DBG) log("updateMuteNotification: not muted (or not offhook)"); 708 cancelMute(); 709 } 710 } 711 712 /** 713 * Updates the phone app's status bar notification based on the 714 * current telephony state, or cancels the notification if the phone 715 * is totally idle. 716 * 717 * This method will never actually launch the incoming-call UI. 718 * (Use updateNotificationAndLaunchIncomingCallUi() for that.) 719 */ 720 public void updateInCallNotification() { 721 // allowFullScreenIntent=false means *don't* allow the incoming 722 // call UI to be launched. 723 updateInCallNotification(false); 724 } 725 726 /** 727 * Updates the phone app's status bar notification *and* launches the 728 * incoming call UI in response to a new incoming call. 729 * 730 * This is just like updateInCallNotification(), with one exception: 731 * If an incoming call is ringing (or call-waiting), the notification 732 * will also include a "fullScreenIntent" that will cause the 733 * InCallScreen to be launched immediately, unless the current 734 * foreground activity is marked as "immersive". 735 * 736 * (This is the mechanism that actually brings up the incoming call UI 737 * when we receive a "new ringing connection" event from the telephony 738 * layer.) 739 * 740 * Watch out: this method should ONLY be called directly from the code 741 * path in CallNotifier that handles the "new ringing connection" 742 * event from the telephony layer. All other places that update the 743 * in-call notification (like for phone state changes) should call 744 * updateInCallNotification() instead. (This ensures that we don't 745 * end up launching the InCallScreen multiple times for a single 746 * incoming call, which could cause slow responsiveness and/or visible 747 * glitches.) 748 * 749 * Also note that this method is safe to call even if the phone isn't 750 * actually ringing (or, more likely, if an incoming call *was* 751 * ringing briefly but then disconnected). In that case, we'll simply 752 * update or cancel the in-call notification based on the current 753 * phone state. 754 * 755 * @see #updateInCallNotification(boolean) 756 */ 757 public void updateNotificationAndLaunchIncomingCallUi() { 758 // Set allowFullScreenIntent=true to indicate that we *should* 759 // launch the incoming call UI if necessary. 760 updateInCallNotification(true); 761 } 762 763 /** 764 * Helper method for updateInCallNotification() and 765 * updateNotificationAndLaunchIncomingCallUi(): Update the phone app's 766 * status bar notification based on the current telephony state, or 767 * cancels the notification if the phone is totally idle. 768 * 769 * @param allowFullScreenIntent If true, *and* an incoming call is 770 * ringing, the notification will include a "fullScreenIntent" 771 * pointing at the InCallScreen (which will cause the InCallScreen 772 * to be launched.) 773 * Watch out: This should be set to true *only* when directly 774 * handling the "new ringing connection" event from the telephony 775 * layer (see updateNotificationAndLaunchIncomingCallUi().) 776 */ 777 private void updateInCallNotification(boolean allowFullScreenIntent) { 778 int resId; 779 if (DBG) log("updateInCallNotification(allowFullScreenIntent = " 780 + allowFullScreenIntent + ")..."); 781 782 // Never display the "ongoing call" notification on 783 // non-voice-capable devices, even if the phone is actually 784 // offhook (like during a non-interactive OTASP call.) 785 if (!PhoneGlobals.sVoiceCapable) { 786 if (DBG) log("- non-voice-capable device; suppressing notification."); 787 return; 788 } 789 790 // If the phone is idle, completely clean up all call-related 791 // notifications. 792 if (mCM.getState() == PhoneConstants.State.IDLE) { 793 cancelInCall(); 794 cancelMute(); 795 cancelSpeakerphone(); 796 return; 797 } 798 799 final boolean hasRingingCall = mCM.hasActiveRingingCall(); 800 final boolean hasActiveCall = mCM.hasActiveFgCall(); 801 final boolean hasHoldingCall = mCM.hasActiveBgCall(); 802 if (DBG) { 803 log(" - hasRingingCall = " + hasRingingCall); 804 log(" - hasActiveCall = " + hasActiveCall); 805 log(" - hasHoldingCall = " + hasHoldingCall); 806 } 807 808 // Suppress the in-call notification if the InCallScreen is the 809 // foreground activity, since it's already obvious that you're on a 810 // call. (The status bar icon is needed only if you navigate *away* 811 // from the in-call UI.) 812 boolean suppressNotification = mApp.isShowingCallScreen(); 813 // if (DBG) log("- suppressNotification: initial value: " + suppressNotification); 814 815 // ...except for a couple of cases where we *never* suppress the 816 // notification: 817 // 818 // - If there's an incoming ringing call: always show the 819 // notification, since the in-call notification is what actually 820 // launches the incoming call UI in the first place (see 821 // notification.fullScreenIntent below.) This makes sure that we'll 822 // correctly handle the case where a new incoming call comes in but 823 // the InCallScreen is already in the foreground. 824 if (hasRingingCall) suppressNotification = false; 825 826 // - If "voice privacy" mode is active: always show the notification, 827 // since that's the only "voice privacy" indication we have. 828 boolean enhancedVoicePrivacy = mApp.notifier.getVoicePrivacyState(); 829 // if (DBG) log("updateInCallNotification: enhancedVoicePrivacy = " + enhancedVoicePrivacy); 830 if (enhancedVoicePrivacy) suppressNotification = false; 831 832 if (suppressNotification) { 833 if (DBG) log("- suppressNotification = true; reducing clutter in status bar..."); 834 cancelInCall(); 835 // Suppress the mute and speaker status bar icons too 836 // (also to reduce clutter in the status bar.) 837 cancelSpeakerphone(); 838 cancelMute(); 839 return; 840 } 841 842 // Display the appropriate icon in the status bar, 843 // based on the current phone and/or bluetooth state. 844 845 if (hasRingingCall) { 846 // There's an incoming ringing call. 847 resId = R.drawable.stat_sys_phone_call; 848 } else if (!hasActiveCall && hasHoldingCall) { 849 // There's only one call, and it's on hold. 850 if (enhancedVoicePrivacy) { 851 resId = R.drawable.stat_sys_vp_phone_call_on_hold; 852 } else { 853 resId = R.drawable.stat_sys_phone_call_on_hold; 854 } 855 } else { 856 if (enhancedVoicePrivacy) { 857 resId = R.drawable.stat_sys_vp_phone_call; 858 } else { 859 resId = R.drawable.stat_sys_phone_call; 860 } 861 } 862 863 // Note we can't just bail out now if (resId == mInCallResId), 864 // since even if the status icon hasn't changed, some *other* 865 // notification-related info may be different from the last time 866 // we were here (like the caller-id info of the foreground call, 867 // if the user swapped calls...) 868 869 if (DBG) log("- Updating status bar icon: resId = " + resId); 870 mInCallResId = resId; 871 872 // Even if both lines are in use, we only show a single item in 873 // the expanded Notifications UI. It's labeled "Ongoing call" 874 // (or "On hold" if there's only one call, and it's on hold.) 875 // Also, we don't have room to display caller-id info from two 876 // different calls. So if both lines are in use, display info 877 // from the foreground call. And if there's a ringing call, 878 // display that regardless of the state of the other calls. 879 880 Call currentCall; 881 if (hasRingingCall) { 882 currentCall = mCM.getFirstActiveRingingCall(); 883 } else if (hasActiveCall) { 884 currentCall = mCM.getActiveFgCall(); 885 } else { 886 currentCall = mCM.getFirstActiveBgCall(); 887 } 888 Connection currentConn = currentCall.getEarliestConnection(); 889 890 final Notification.Builder builder = new Notification.Builder(mContext); 891 builder.setSmallIcon(mInCallResId).setOngoing(true); 892 893 // PendingIntent that can be used to launch the InCallScreen. The 894 // system fires off this intent if the user pulls down the windowshade 895 // and clicks the notification's expanded view. It's also used to 896 // launch the InCallScreen immediately when when there's an incoming 897 // call (see the "fullScreenIntent" field below). 898 PendingIntent inCallPendingIntent = 899 PendingIntent.getActivity(mContext, 0, 900 PhoneGlobals.createInCallIntent(), 0); 901 builder.setContentIntent(inCallPendingIntent); 902 903 // Update icon on the left of the notification. 904 // - If it is directly available from CallerInfo, we'll just use that. 905 // - If it is not, use the same icon as in the status bar. 906 CallerInfo callerInfo = null; 907 if (currentConn != null) { 908 Object o = currentConn.getUserData(); 909 if (o instanceof CallerInfo) { 910 callerInfo = (CallerInfo) o; 911 } else if (o instanceof PhoneUtils.CallerInfoToken) { 912 callerInfo = ((PhoneUtils.CallerInfoToken) o).currentInfo; 913 } else { 914 Log.w(LOG_TAG, "CallerInfo isn't available while Call object is available."); 915 } 916 } 917 boolean largeIconWasSet = false; 918 if (callerInfo != null) { 919 // In most cases, the user will see the notification after CallerInfo is already 920 // available, so photo will be available from this block. 921 if (callerInfo.isCachedPhotoCurrent) { 922 // .. and in that case CallerInfo's cachedPhotoIcon should also be available. 923 // If it happens not, then try using cachedPhoto, assuming Drawable coming from 924 // ContactProvider will be BitmapDrawable. 925 if (callerInfo.cachedPhotoIcon != null) { 926 builder.setLargeIcon(callerInfo.cachedPhotoIcon); 927 largeIconWasSet = true; 928 } else if (callerInfo.cachedPhoto instanceof BitmapDrawable) { 929 if (DBG) log("- BitmapDrawable found for large icon"); 930 Bitmap bitmap = ((BitmapDrawable) callerInfo.cachedPhoto).getBitmap(); 931 builder.setLargeIcon(bitmap); 932 largeIconWasSet = true; 933 } else { 934 if (DBG) { 935 log("- Failed to fetch icon from CallerInfo's cached photo." 936 + " (cachedPhotoIcon: " + callerInfo.cachedPhotoIcon 937 + ", cachedPhoto: " + callerInfo.cachedPhoto + ")." 938 + " Ignore it."); 939 } 940 } 941 } 942 943 if (!largeIconWasSet && callerInfo.photoResource > 0) { 944 if (DBG) { 945 log("- BitmapDrawable nor person Id not found for large icon." 946 + " Use photoResource: " + callerInfo.photoResource); 947 } 948 Drawable drawable = 949 mContext.getResources().getDrawable(callerInfo.photoResource); 950 if (drawable instanceof BitmapDrawable) { 951 Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap(); 952 builder.setLargeIcon(bitmap); 953 largeIconWasSet = true; 954 } else { 955 if (DBG) { 956 log("- PhotoResource was found but it didn't return BitmapDrawable." 957 + " Ignore it"); 958 } 959 } 960 } 961 } else { 962 if (DBG) log("- CallerInfo not found. Use the same icon as in the status bar."); 963 } 964 965 // Failed to fetch Bitmap. 966 if (!largeIconWasSet && DBG) { 967 log("- No useful Bitmap was found for the photo." 968 + " Use the same icon as in the status bar."); 969 } 970 971 // If the connection is valid, then build what we need for the 972 // content text of notification, and start the chronometer. 973 // Otherwise, don't bother and just stick with content title. 974 if (currentConn != null) { 975 if (DBG) log("- Updating context text and chronometer."); 976 if (hasRingingCall) { 977 // Incoming call is ringing. 978 builder.setContentText(mContext.getString(R.string.notification_incoming_call)); 979 builder.setUsesChronometer(false); 980 } else if (hasHoldingCall && !hasActiveCall) { 981 // Only one call, and it's on hold. 982 builder.setContentText(mContext.getString(R.string.notification_on_hold)); 983 builder.setUsesChronometer(false); 984 } else { 985 // We show the elapsed time of the current call using Chronometer. 986 builder.setUsesChronometer(true); 987 988 // Determine the "start time" of the current connection. 989 // We can't use currentConn.getConnectTime(), because (1) that's 990 // in the currentTimeMillis() time base, and (2) it's zero when 991 // the phone first goes off hook, since the getConnectTime counter 992 // doesn't start until the DIALING -> ACTIVE transition. 993 // Instead we start with the current connection's duration, 994 // and translate that into the elapsedRealtime() timebase. 995 long callDurationMsec = currentConn.getDurationMillis(); 996 builder.setWhen(System.currentTimeMillis() - callDurationMsec); 997 998 int contextTextId = R.string.notification_ongoing_call; 999 1000 Call call = mCM.getActiveFgCall(); 1001 if (TelephonyCapabilities.canDistinguishDialingAndConnected( 1002 call.getPhone().getPhoneType()) && call.isDialingOrAlerting()) { 1003 contextTextId = R.string.notification_dialing; 1004 } 1005 1006 builder.setContentText(mContext.getString(contextTextId)); 1007 } 1008 } else if (DBG) { 1009 Log.w(LOG_TAG, "updateInCallNotification: null connection, can't set exp view line 1."); 1010 } 1011 1012 // display conference call string if this call is a conference 1013 // call, otherwise display the connection information. 1014 1015 // Line 2 of the expanded view (smaller text). This is usually a 1016 // contact name or phone number. 1017 String expandedViewLine2 = ""; 1018 // TODO: it may not make sense for every point to make separate 1019 // checks for isConferenceCall, so we need to think about 1020 // possibly including this in startGetCallerInfo or some other 1021 // common point. 1022 if (PhoneUtils.isConferenceCall(currentCall)) { 1023 // if this is a conference call, just use that as the caller name. 1024 expandedViewLine2 = mContext.getString(R.string.card_title_conf_call); 1025 } else { 1026 // If necessary, start asynchronous query to do the caller-id lookup. 1027 PhoneUtils.CallerInfoToken cit = 1028 PhoneUtils.startGetCallerInfo(mContext, currentCall, this, this); 1029 expandedViewLine2 = PhoneUtils.getCompactNameFromCallerInfo(cit.currentInfo, mContext); 1030 // Note: For an incoming call, the very first time we get here we 1031 // won't have a contact name yet, since we only just started the 1032 // caller-id query. So expandedViewLine2 will start off as a raw 1033 // phone number, but we'll update it very quickly when the query 1034 // completes (see onQueryComplete() below.) 1035 } 1036 1037 if (DBG) log("- Updating expanded view: line 2 '" + /*expandedViewLine2*/ "xxxxxxx" + "'"); 1038 builder.setContentTitle(expandedViewLine2); 1039 1040 // TODO: We also need to *update* this notification in some cases, 1041 // like when a call ends on one line but the other is still in use 1042 // (ie. make sure the caller info here corresponds to the active 1043 // line), and maybe even when the user swaps calls (ie. if we only 1044 // show info here for the "current active call".) 1045 1046 // Activate a couple of special Notification features if an 1047 // incoming call is ringing: 1048 if (hasRingingCall) { 1049 if (DBG) log("- Using hi-pri notification for ringing call!"); 1050 1051 // This is a high-priority event that should be shown even if the 1052 // status bar is hidden or if an immersive activity is running. 1053 builder.setPriority(Notification.PRIORITY_HIGH); 1054 1055 // If an immersive activity is running, we have room for a single 1056 // line of text in the small notification popup window. 1057 // We use expandedViewLine2 for this (i.e. the name or number of 1058 // the incoming caller), since that's more relevant than 1059 // expandedViewLine1 (which is something generic like "Incoming 1060 // call".) 1061 builder.setTicker(expandedViewLine2); 1062 1063 if (allowFullScreenIntent) { 1064 // Ok, we actually want to launch the incoming call 1065 // UI at this point (in addition to simply posting a notification 1066 // to the status bar). Setting fullScreenIntent will cause 1067 // the InCallScreen to be launched immediately *unless* the 1068 // current foreground activity is marked as "immersive". 1069 if (DBG) log("- Setting fullScreenIntent: " + inCallPendingIntent); 1070 builder.setFullScreenIntent(inCallPendingIntent, true); 1071 1072 // Ugly hack alert: 1073 // 1074 // The NotificationManager has the (undocumented) behavior 1075 // that it will *ignore* the fullScreenIntent field if you 1076 // post a new Notification that matches the ID of one that's 1077 // already active. Unfortunately this is exactly what happens 1078 // when you get an incoming call-waiting call: the 1079 // "ongoing call" notification is already visible, so the 1080 // InCallScreen won't get launched in this case! 1081 // (The result: if you bail out of the in-call UI while on a 1082 // call and then get a call-waiting call, the incoming call UI 1083 // won't come up automatically.) 1084 // 1085 // The workaround is to just notice this exact case (this is a 1086 // call-waiting call *and* the InCallScreen is not in the 1087 // foreground) and manually cancel the in-call notification 1088 // before (re)posting it. 1089 // 1090 // TODO: there should be a cleaner way of avoiding this 1091 // problem (see discussion in bug 3184149.) 1092 Call ringingCall = mCM.getFirstActiveRingingCall(); 1093 if ((ringingCall.getState() == Call.State.WAITING) && !mApp.isShowingCallScreen()) { 1094 Log.i(LOG_TAG, "updateInCallNotification: call-waiting! force relaunch..."); 1095 // Cancel the IN_CALL_NOTIFICATION immediately before 1096 // (re)posting it; this seems to force the 1097 // NotificationManager to launch the fullScreenIntent. 1098 mNotificationManager.cancel(IN_CALL_NOTIFICATION); 1099 } 1100 } 1101 } else { // not ringing call 1102 // Make the notification prioritized over the other normal notifications. 1103 builder.setPriority(Notification.PRIORITY_HIGH); 1104 1105 // TODO: use "if (DBG)" for this comment. 1106 log("Will show \"hang-up\" action in the ongoing active call Notification"); 1107 // TODO: use better asset. 1108 builder.addAction(R.drawable.stat_sys_phone_call_end, 1109 mContext.getText(R.string.notification_action_end_call), 1110 PhoneGlobals.createHangUpOngoingCallPendingIntent(mContext)); 1111 } 1112 1113 Notification notification = builder.getNotification(); 1114 if (DBG) log("Notifying IN_CALL_NOTIFICATION: " + notification); 1115 // TODO(klp): not needed anymore. Possibly delete this and move notification to incallui. 1116 //mNotificationManager.notify(IN_CALL_NOTIFICATION, notification); 1117 1118 // Finally, refresh the mute and speakerphone notifications (since 1119 // some phone state changes can indirectly affect the mute and/or 1120 // speaker state). 1121 updateSpeakerNotification(); 1122 updateMuteNotification(); 1123 } 1124 1125 /** 1126 * Implemented for CallerInfoAsyncQuery.OnQueryCompleteListener interface. 1127 * refreshes the contentView when called. 1128 */ 1129 @Override 1130 public void onQueryComplete(int token, Object cookie, CallerInfo ci){ 1131 if (DBG) log("CallerInfo query complete (for NotificationMgr), " 1132 + "updating in-call notification.."); 1133 if (DBG) log("- cookie: " + cookie); 1134 if (DBG) log("- ci: " + ci); 1135 1136 if (cookie == this) { 1137 // Ok, this is the caller-id query we fired off in 1138 // updateInCallNotification(), presumably when an incoming call 1139 // first appeared. If the caller-id info matched any contacts, 1140 // compactName should now be a real person name rather than a raw 1141 // phone number: 1142 if (DBG) log("- compactName is now: " 1143 + PhoneUtils.getCompactNameFromCallerInfo(ci, mContext)); 1144 1145 // Now that our CallerInfo object has been fully filled-in, 1146 // refresh the in-call notification. 1147 if (DBG) log("- updating notification after query complete..."); 1148 updateInCallNotification(); 1149 } else { 1150 Log.w(LOG_TAG, "onQueryComplete: caller-id query from unknown source! " 1151 + "cookie = " + cookie); 1152 } 1153 } 1154 1155 /** 1156 * Take down the in-call notification. 1157 * @see updateInCallNotification() 1158 */ 1159 private void cancelInCall() { 1160 if (DBG) log("cancelInCall()..."); 1161 mNotificationManager.cancel(IN_CALL_NOTIFICATION); 1162 mInCallResId = 0; 1163 } 1164 1165 /** 1166 * Completely take down the in-call notification *and* the mute/speaker 1167 * notifications as well, to indicate that the phone is now idle. 1168 */ 1169 /* package */ void cancelCallInProgressNotifications() { 1170 if (DBG) log("cancelCallInProgressNotifications()..."); 1171 if (mInCallResId == 0) { 1172 return; 1173 } 1174 1175 if (DBG) log("cancelCallInProgressNotifications: " + mInCallResId); 1176 cancelInCall(); 1177 cancelMute(); 1178 cancelSpeakerphone(); 1179 } 1180 1181 /** 1182 * Updates the message waiting indicator (voicemail) notification. 1183 * 1184 * @param visible true if there are messages waiting 1185 */ 1186 /* package */ void updateMwi(boolean visible) { 1187 if (DBG) log("updateMwi(): " + visible); 1188 1189 if (visible) { 1190 int resId = android.R.drawable.stat_notify_voicemail; 1191 1192 // This Notification can get a lot fancier once we have more 1193 // information about the current voicemail messages. 1194 // (For example, the current voicemail system can't tell 1195 // us the caller-id or timestamp of a message, or tell us the 1196 // message count.) 1197 1198 // But for now, the UI is ultra-simple: if the MWI indication 1199 // is supposed to be visible, just show a single generic 1200 // notification. 1201 1202 String notificationTitle = mContext.getString(R.string.notification_voicemail_title); 1203 String vmNumber = mPhone.getVoiceMailNumber(); 1204 if (DBG) log("- got vm number: '" + vmNumber + "'"); 1205 1206 // Watch out: vmNumber may be null, for two possible reasons: 1207 // 1208 // (1) This phone really has no voicemail number 1209 // 1210 // (2) This phone *does* have a voicemail number, but 1211 // the SIM isn't ready yet. 1212 // 1213 // Case (2) *does* happen in practice if you have voicemail 1214 // messages when the device first boots: we get an MWI 1215 // notification as soon as we register on the network, but the 1216 // SIM hasn't finished loading yet. 1217 // 1218 // So handle case (2) by retrying the lookup after a short 1219 // delay. 1220 1221 if ((vmNumber == null) && !mPhone.getIccRecordsLoaded()) { 1222 if (DBG) log("- Null vm number: SIM records not loaded (yet)..."); 1223 1224 // TODO: rather than retrying after an arbitrary delay, it 1225 // would be cleaner to instead just wait for a 1226 // SIM_RECORDS_LOADED notification. 1227 // (Unfortunately right now there's no convenient way to 1228 // get that notification in phone app code. We'd first 1229 // want to add a call like registerForSimRecordsLoaded() 1230 // to Phone.java and GSMPhone.java, and *then* we could 1231 // listen for that in the CallNotifier class.) 1232 1233 // Limit the number of retries (in case the SIM is broken 1234 // or missing and can *never* load successfully.) 1235 if (mVmNumberRetriesRemaining-- > 0) { 1236 if (DBG) log(" - Retrying in " + VM_NUMBER_RETRY_DELAY_MILLIS + " msec..."); 1237 mApp.notifier.sendMwiChangedDelayed(VM_NUMBER_RETRY_DELAY_MILLIS); 1238 return; 1239 } else { 1240 Log.w(LOG_TAG, "NotificationMgr.updateMwi: getVoiceMailNumber() failed after " 1241 + MAX_VM_NUMBER_RETRIES + " retries; giving up."); 1242 // ...and continue with vmNumber==null, just as if the 1243 // phone had no VM number set up in the first place. 1244 } 1245 } 1246 1247 if (TelephonyCapabilities.supportsVoiceMessageCount(mPhone)) { 1248 int vmCount = mPhone.getVoiceMessageCount(); 1249 String titleFormat = mContext.getString(R.string.notification_voicemail_title_count); 1250 notificationTitle = String.format(titleFormat, vmCount); 1251 } 1252 1253 String notificationText; 1254 if (TextUtils.isEmpty(vmNumber)) { 1255 notificationText = mContext.getString( 1256 R.string.notification_voicemail_no_vm_number); 1257 } else { 1258 notificationText = String.format( 1259 mContext.getString(R.string.notification_voicemail_text_format), 1260 PhoneNumberUtils.formatNumber(vmNumber)); 1261 } 1262 1263 Intent intent = new Intent(Intent.ACTION_CALL, 1264 Uri.fromParts(Constants.SCHEME_VOICEMAIL, "", null)); 1265 PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0); 1266 1267 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); 1268 Uri ringtoneUri; 1269 String uriString = prefs.getString( 1270 CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_RINGTONE_KEY, null); 1271 if (!TextUtils.isEmpty(uriString)) { 1272 ringtoneUri = Uri.parse(uriString); 1273 } else { 1274 ringtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI; 1275 } 1276 1277 Notification.Builder builder = new Notification.Builder(mContext); 1278 builder.setSmallIcon(resId) 1279 .setWhen(System.currentTimeMillis()) 1280 .setContentTitle(notificationTitle) 1281 .setContentText(notificationText) 1282 .setContentIntent(pendingIntent) 1283 .setSound(ringtoneUri); 1284 Notification notification = builder.getNotification(); 1285 1286 CallFeaturesSetting.migrateVoicemailVibrationSettingsIfNeeded(prefs); 1287 final boolean vibrate = prefs.getBoolean( 1288 CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_VIBRATE_KEY, false); 1289 if (vibrate) { 1290 notification.defaults |= Notification.DEFAULT_VIBRATE; 1291 } 1292 notification.flags |= Notification.FLAG_NO_CLEAR; 1293 configureLedNotification(notification); 1294 mNotificationManager.notify(VOICEMAIL_NOTIFICATION, notification); 1295 } else { 1296 mNotificationManager.cancel(VOICEMAIL_NOTIFICATION); 1297 } 1298 } 1299 1300 /** 1301 * Updates the message call forwarding indicator notification. 1302 * 1303 * @param visible true if there are messages waiting 1304 */ 1305 /* package */ void updateCfi(boolean visible) { 1306 if (DBG) log("updateCfi(): " + visible); 1307 if (visible) { 1308 // If Unconditional Call Forwarding (forward all calls) for VOICE 1309 // is enabled, just show a notification. We'll default to expanded 1310 // view for now, so the there is less confusion about the icon. If 1311 // it is deemed too weird to have CF indications as expanded views, 1312 // then we'll flip the flag back. 1313 1314 // TODO: We may want to take a look to see if the notification can 1315 // display the target to forward calls to. This will require some 1316 // effort though, since there are multiple layers of messages that 1317 // will need to propagate that information. 1318 1319 Notification notification; 1320 final boolean showExpandedNotification = true; 1321 if (showExpandedNotification) { 1322 Intent intent = new Intent(Intent.ACTION_MAIN); 1323 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1324 intent.setClassName("com.android.phone", 1325 "com.android.phone.CallFeaturesSetting"); 1326 1327 notification = new Notification( 1328 R.drawable.stat_sys_phone_call_forward, // icon 1329 null, // tickerText 1330 0); // The "timestamp" of this notification is meaningless; 1331 // we only care about whether CFI is currently on or not. 1332 notification.setLatestEventInfo( 1333 mContext, // context 1334 mContext.getString(R.string.labelCF), // expandedTitle 1335 mContext.getString(R.string.sum_cfu_enabled_indicator), // expandedText 1336 PendingIntent.getActivity(mContext, 0, intent, 0)); // contentIntent 1337 } else { 1338 notification = new Notification( 1339 R.drawable.stat_sys_phone_call_forward, // icon 1340 null, // tickerText 1341 System.currentTimeMillis() // when 1342 ); 1343 } 1344 1345 notification.flags |= Notification.FLAG_ONGOING_EVENT; // also implies FLAG_NO_CLEAR 1346 1347 mNotificationManager.notify( 1348 CALL_FORWARD_NOTIFICATION, 1349 notification); 1350 } else { 1351 mNotificationManager.cancel(CALL_FORWARD_NOTIFICATION); 1352 } 1353 } 1354 1355 /** 1356 * Shows the "data disconnected due to roaming" notification, which 1357 * appears when you lose data connectivity because you're roaming and 1358 * you have the "data roaming" feature turned off. 1359 */ 1360 /* package */ void showDataDisconnectedRoaming() { 1361 if (DBG) log("showDataDisconnectedRoaming()..."); 1362 1363 // "Mobile network settings" screen / dialog 1364 Intent intent = new Intent(mContext, com.android.phone.MobileNetworkSettings.class); 1365 1366 final CharSequence contentText = mContext.getText(R.string.roaming_reenable_message); 1367 1368 final Notification.Builder builder = new Notification.Builder(mContext); 1369 builder.setSmallIcon(android.R.drawable.stat_sys_warning); 1370 builder.setContentTitle(mContext.getText(R.string.roaming)); 1371 builder.setContentText(contentText); 1372 builder.setContentIntent(PendingIntent.getActivity(mContext, 0, intent, 0)); 1373 1374 final Notification notif = new Notification.BigTextStyle(builder).bigText(contentText) 1375 .build(); 1376 1377 mNotificationManager.notify(DATA_DISCONNECTED_ROAMING_NOTIFICATION, notif); 1378 } 1379 1380 /** 1381 * Turns off the "data disconnected due to roaming" notification. 1382 */ 1383 /* package */ void hideDataDisconnectedRoaming() { 1384 if (DBG) log("hideDataDisconnectedRoaming()..."); 1385 mNotificationManager.cancel(DATA_DISCONNECTED_ROAMING_NOTIFICATION); 1386 } 1387 1388 /** 1389 * Display the network selection "no service" notification 1390 * @param operator is the numeric operator number 1391 */ 1392 private void showNetworkSelection(String operator) { 1393 if (DBG) log("showNetworkSelection(" + operator + ")..."); 1394 1395 String titleText = mContext.getString( 1396 R.string.notification_network_selection_title); 1397 String expandedText = mContext.getString( 1398 R.string.notification_network_selection_text, operator); 1399 1400 Notification notification = new Notification(); 1401 notification.icon = android.R.drawable.stat_sys_warning; 1402 notification.when = 0; 1403 notification.flags = Notification.FLAG_ONGOING_EVENT; 1404 notification.tickerText = null; 1405 1406 // create the target network operators settings intent 1407 Intent intent = new Intent(Intent.ACTION_MAIN); 1408 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | 1409 Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 1410 // Use NetworkSetting to handle the selection intent 1411 intent.setComponent(new ComponentName("com.android.phone", 1412 "com.android.phone.NetworkSetting")); 1413 PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0); 1414 1415 notification.setLatestEventInfo(mContext, titleText, expandedText, pi); 1416 1417 mNotificationManager.notify(SELECTED_OPERATOR_FAIL_NOTIFICATION, notification); 1418 } 1419 1420 /** 1421 * Turn off the network selection "no service" notification 1422 */ 1423 private void cancelNetworkSelection() { 1424 if (DBG) log("cancelNetworkSelection()..."); 1425 mNotificationManager.cancel(SELECTED_OPERATOR_FAIL_NOTIFICATION); 1426 } 1427 1428 /** 1429 * Update notification about no service of user selected operator 1430 * 1431 * @param serviceState Phone service state 1432 */ 1433 void updateNetworkSelection(int serviceState) { 1434 if (TelephonyCapabilities.supportsNetworkSelection(mPhone)) { 1435 // get the shared preference of network_selection. 1436 // empty is auto mode, otherwise it is the operator alpha name 1437 // in case there is no operator name, check the operator numeric 1438 SharedPreferences sp = 1439 PreferenceManager.getDefaultSharedPreferences(mContext); 1440 String networkSelection = 1441 sp.getString(PhoneBase.NETWORK_SELECTION_NAME_KEY, ""); 1442 if (TextUtils.isEmpty(networkSelection)) { 1443 networkSelection = 1444 sp.getString(PhoneBase.NETWORK_SELECTION_KEY, ""); 1445 } 1446 1447 if (DBG) log("updateNetworkSelection()..." + "state = " + 1448 serviceState + " new network " + networkSelection); 1449 1450 if (serviceState == ServiceState.STATE_OUT_OF_SERVICE 1451 && !TextUtils.isEmpty(networkSelection)) { 1452 if (!mSelectedUnavailableNotify) { 1453 showNetworkSelection(networkSelection); 1454 mSelectedUnavailableNotify = true; 1455 } 1456 } else { 1457 if (mSelectedUnavailableNotify) { 1458 cancelNetworkSelection(); 1459 mSelectedUnavailableNotify = false; 1460 } 1461 } 1462 } 1463 } 1464 1465 /* package */ void postTransientNotification(int notifyId, CharSequence msg) { 1466 if (mToast != null) { 1467 mToast.cancel(); 1468 } 1469 1470 mToast = Toast.makeText(mContext, msg, Toast.LENGTH_LONG); 1471 mToast.show(); 1472 } 1473 1474 private void log(String msg) { 1475 Log.d(LOG_TAG, msg); 1476 } 1477} 1478