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