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