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