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.SystemClock; 33 import android.os.SystemProperties; 34 import android.preference.PreferenceManager; 35 import android.provider.CallLog.Calls; 36 import android.provider.ContactsContract.PhoneLookup; 37 import android.provider.Settings; 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 /** 55 * NotificationManager-related utility code for the Phone app. 56 */ 57 public class NotificationMgr implements CallerInfoAsyncQuery.OnQueryCompleteListener{ 58 private static final String LOG_TAG = "NotificationMgr"; 59 private static final boolean DBG = 60 (PhoneApp.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1); 61 62 private static final String[] CALL_LOG_PROJECTION = new String[] { 63 Calls._ID, 64 Calls.NUMBER, 65 Calls.DATE, 66 Calls.DURATION, 67 Calls.TYPE, 68 }; 69 70 // notification types 71 static final int MISSED_CALL_NOTIFICATION = 1; 72 static final int IN_CALL_NOTIFICATION = 2; 73 static final int MMI_NOTIFICATION = 3; 74 static final int NETWORK_SELECTION_NOTIFICATION = 4; 75 static final int VOICEMAIL_NOTIFICATION = 5; 76 static final int CALL_FORWARD_NOTIFICATION = 6; 77 static final int DATA_DISCONNECTED_ROAMING_NOTIFICATION = 7; 78 static final int SELECTED_OPERATOR_FAIL_NOTIFICATION = 8; 79 80 private static NotificationMgr sMe = null; 81 private Phone mPhone; 82 private CallManager mCM; 83 84 private Context mContext; 85 private NotificationManager mNotificationMgr; 86 private StatusBarManager mStatusBar; 87 private StatusBarMgr mStatusBarMgr; 88 private Toast mToast; 89 private boolean mShowingSpeakerphoneIcon; 90 private boolean mShowingMuteIcon; 91 92 // used to track the missed call counter, default to 0. 93 private int mNumberMissedCalls = 0; 94 95 // Currently-displayed resource IDs for some status bar icons (or zero 96 // if no notification is active): 97 private int mInCallResId; 98 99 // used to track the notification of selected network unavailable 100 private boolean mSelectedUnavailableNotify = false; 101 102 // Retry params for the getVoiceMailNumber() call; see updateMwi(). 103 private static final int MAX_VM_NUMBER_RETRIES = 5; 104 private static final int VM_NUMBER_RETRY_DELAY_MILLIS = 10000; 105 private int mVmNumberRetriesRemaining = MAX_VM_NUMBER_RETRIES; 106 107 // Query used to look up caller-id info for the "call log" notification. 108 private QueryHandler mQueryHandler = null; 109 private static final int CALL_LOG_TOKEN = -1; 110 private static final int CONTACT_TOKEN = -2; 111 112 NotificationMgr(Context context) { 113 mContext = context; 114 mNotificationMgr = (NotificationManager) 115 context.getSystemService(Context.NOTIFICATION_SERVICE); 116 117 mStatusBar = (StatusBarManager) context.getSystemService(Context.STATUS_BAR_SERVICE); 118 119 PhoneApp app = PhoneApp.getInstance(); 120 mPhone = app.phone; 121 mCM = app.mCM; 122 } 123 124 static void init(Context context) { 125 sMe = new NotificationMgr(context); 126 127 // update the notifications that need to be touched at startup. 128 sMe.updateNotificationsAtStartup(); 129 } 130 131 static NotificationMgr getDefault() { 132 return sMe; 133 } 134 135 /** 136 * Class that controls the status bar. This class maintains a set 137 * of state and acts as an interface between the Phone process and 138 * the Status bar. All interaction with the status bar should be 139 * though the methods contained herein. 140 */ 141 142 /** 143 * Factory method 144 */ 145 StatusBarMgr getStatusBarMgr() { 146 if (mStatusBarMgr == null) { 147 mStatusBarMgr = new StatusBarMgr(); 148 } 149 return mStatusBarMgr; 150 } 151 152 /** 153 * StatusBarMgr implementation 154 */ 155 class StatusBarMgr { 156 // current settings 157 private boolean mIsNotificationEnabled = true; 158 private boolean mIsExpandedViewEnabled = true; 159 160 private StatusBarMgr () { 161 } 162 163 /** 164 * Sets the notification state (enable / disable 165 * vibrating notifications) for the status bar, 166 * updates the status bar service if there is a change. 167 * Independent of the remaining Status Bar 168 * functionality, including icons and expanded view. 169 */ 170 void enableNotificationAlerts(boolean enable) { 171 if (mIsNotificationEnabled != enable) { 172 mIsNotificationEnabled = enable; 173 updateStatusBar(); 174 } 175 } 176 177 /** 178 * Sets the ability to expand the notifications for the 179 * status bar, updates the status bar service if there 180 * is a change. Independent of the remaining Status Bar 181 * functionality, including icons and notification 182 * alerts. 183 */ 184 void enableExpandedView(boolean enable) { 185 if (mIsExpandedViewEnabled != enable) { 186 mIsExpandedViewEnabled = enable; 187 updateStatusBar(); 188 } 189 } 190 191 /** 192 * Method to synchronize status bar state with our current 193 * state. 194 */ 195 void updateStatusBar() { 196 int state = StatusBarManager.DISABLE_NONE; 197 198 if (!mIsExpandedViewEnabled) { 199 state |= StatusBarManager.DISABLE_EXPAND; 200 } 201 202 if (!mIsNotificationEnabled) { 203 state |= StatusBarManager.DISABLE_NOTIFICATION_ALERTS; 204 } 205 206 // send the message to the status bar manager. 207 if (DBG) log("updating status bar state: " + state); 208 mStatusBar.disable(state); 209 } 210 } 211 212 /** 213 * Makes sure phone-related notifications are up to date on a 214 * freshly-booted device. 215 */ 216 private void updateNotificationsAtStartup() { 217 if (DBG) log("updateNotificationsAtStartup()..."); 218 219 // instantiate query handler 220 mQueryHandler = new QueryHandler(mContext.getContentResolver()); 221 222 // setup query spec, look for all Missed calls that are new. 223 StringBuilder where = new StringBuilder("type="); 224 where.append(Calls.MISSED_TYPE); 225 where.append(" AND new=1"); 226 227 // start the query 228 if (DBG) log("- start call log query..."); 229 mQueryHandler.startQuery(CALL_LOG_TOKEN, null, Calls.CONTENT_URI, CALL_LOG_PROJECTION, 230 where.toString(), null, Calls.DEFAULT_SORT_ORDER); 231 232 // Update (or cancel) the in-call notification 233 if (DBG) log("- updating in-call notification at startup..."); 234 updateInCallNotification(); 235 236 // Depend on android.app.StatusBarManager to be set to 237 // disable(DISABLE_NONE) upon startup. This will be the 238 // case even if the phone app crashes. 239 } 240 241 /** The projection to use when querying the phones table */ 242 static final String[] PHONES_PROJECTION = new String[] { 243 PhoneLookup.NUMBER, 244 PhoneLookup.DISPLAY_NAME 245 }; 246 247 /** 248 * Class used to run asynchronous queries to re-populate 249 * the notifications we care about. 250 */ 251 private class QueryHandler extends AsyncQueryHandler { 252 253 /** 254 * Used to store relevant fields for the Missed Call 255 * notifications. 256 */ 257 private class NotificationInfo { 258 public String name; 259 public String number; 260 public String label; 261 public long date; 262 } 263 264 public QueryHandler(ContentResolver cr) { 265 super(cr); 266 } 267 268 /** 269 * Handles the query results. There are really 2 steps to this, 270 * similar to what happens in RecentCallsListActivity. 271 * 1. Find the list of missed calls 272 * 2. For each call, run a query to retrieve the caller's name. 273 */ 274 @Override 275 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 276 // TODO: it would be faster to use a join here, but for the purposes 277 // of this small record set, it should be ok. 278 279 // Note that CursorJoiner is not useable here because the number 280 // comparisons are not strictly equals; the comparisons happen in 281 // the SQL function PHONE_NUMBERS_EQUAL, which is not available for 282 // the CursorJoiner. 283 284 // Executing our own query is also feasible (with a join), but that 285 // will require some work (possibly destabilizing) in Contacts 286 // Provider. 287 288 // At this point, we will execute subqueries on each row just as 289 // RecentCallsListActivity.java does. 290 switch (token) { 291 case CALL_LOG_TOKEN: 292 if (DBG) log("call log query complete."); 293 294 // initial call to retrieve the call list. 295 if (cursor != null) { 296 while (cursor.moveToNext()) { 297 // for each call in the call log list, create 298 // the notification object and query contacts 299 NotificationInfo n = getNotificationInfo (cursor); 300 301 if (DBG) log("query contacts for number: " + n.number); 302 303 mQueryHandler.startQuery(CONTACT_TOKEN, n, 304 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, n.number), 305 PHONES_PROJECTION, null, null, PhoneLookup.NUMBER); 306 } 307 308 if (DBG) log("closing call log cursor."); 309 cursor.close(); 310 } 311 break; 312 case CONTACT_TOKEN: 313 if (DBG) log("contact query complete."); 314 315 // subqueries to get the caller name. 316 if ((cursor != null) && (cookie != null)){ 317 NotificationInfo n = (NotificationInfo) cookie; 318 319 if (cursor.moveToFirst()) { 320 // we have contacts data, get the name. 321 if (DBG) log("contact :" + n.name + " found for phone: " + n.number); 322 n.name = cursor.getString( 323 cursor.getColumnIndexOrThrow(PhoneLookup.DISPLAY_NAME)); 324 } 325 326 // send the notification 327 if (DBG) log("sending notification."); 328 notifyMissedCall(n.name, n.number, n.label, n.date); 329 330 if (DBG) log("closing contact cursor."); 331 cursor.close(); 332 } 333 break; 334 default: 335 } 336 } 337 338 /** 339 * Factory method to generate a NotificationInfo object given a 340 * cursor from the call log table. 341 */ 342 private final NotificationInfo getNotificationInfo(Cursor cursor) { 343 NotificationInfo n = new NotificationInfo(); 344 n.name = null; 345 n.number = cursor.getString(cursor.getColumnIndexOrThrow(Calls.NUMBER)); 346 n.label = cursor.getString(cursor.getColumnIndexOrThrow(Calls.TYPE)); 347 n.date = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DATE)); 348 349 // make sure we update the number depending upon saved values in 350 // CallLog.addCall(). If either special values for unknown or 351 // private number are detected, we need to hand off the message 352 // to the missed call notification. 353 if ( (n.number.equals(CallerInfo.UNKNOWN_NUMBER)) || 354 (n.number.equals(CallerInfo.PRIVATE_NUMBER)) || 355 (n.number.equals(CallerInfo.PAYPHONE_NUMBER)) ) { 356 n.number = null; 357 } 358 359 if (DBG) log("NotificationInfo constructed for number: " + n.number); 360 361 return n; 362 } 363 } 364 365 /** 366 * Configures a Notification to emit the blinky green message-waiting/ 367 * missed-call signal. 368 */ 369 private static void configureLedNotification(Notification note) { 370 note.flags |= Notification.FLAG_SHOW_LIGHTS; 371 note.defaults |= Notification.DEFAULT_LIGHTS; 372 } 373 374 /** 375 * Displays a notification about a missed call. 376 * 377 * @param nameOrNumber either the contact name, or the phone number if no contact 378 * @param label the label of the number if nameOrNumber is a name, null if it is a number 379 */ 380 void notifyMissedCall(String name, String number, String label, long date) { 381 // title resource id 382 int titleResId; 383 // the text in the notification's line 1 and 2. 384 String expandedText, callName; 385 386 // increment number of missed calls. 387 mNumberMissedCalls++; 388 389 // get the name for the ticker text 390 // i.e. "Missed call from <caller name or number>" 391 if (name != null && TextUtils.isGraphic(name)) { 392 callName = name; 393 } else if (!TextUtils.isEmpty(number)){ 394 callName = number; 395 } else { 396 // use "unknown" if the caller is unidentifiable. 397 callName = mContext.getString(R.string.unknown); 398 } 399 400 // display the first line of the notification: 401 // 1 missed call: call name 402 // more than 1 missed call: <number of calls> + "missed calls" 403 if (mNumberMissedCalls == 1) { 404 titleResId = R.string.notification_missedCallTitle; 405 expandedText = callName; 406 } else { 407 titleResId = R.string.notification_missedCallsTitle; 408 expandedText = mContext.getString(R.string.notification_missedCallsMsg, 409 mNumberMissedCalls); 410 } 411 412 // create the target call log intent 413 final Intent intent = PhoneApp.createCallLogIntent(); 414 415 // make the notification 416 Notification note = new Notification(mContext, // context 417 android.R.drawable.stat_notify_missed_call, // icon 418 mContext.getString(R.string.notification_missedCallTicker, callName), // tickerText 419 date, // when 420 mContext.getText(titleResId), // expandedTitle 421 expandedText, // expandedText 422 intent // contentIntent 423 ); 424 configureLedNotification(note); 425 mNotificationMgr.notify(MISSED_CALL_NOTIFICATION, note); 426 } 427 428 void cancelMissedCallNotification() { 429 // reset the number of missed calls to 0. 430 mNumberMissedCalls = 0; 431 mNotificationMgr.cancel(MISSED_CALL_NOTIFICATION); 432 } 433 434 void notifySpeakerphone() { 435 if (!mShowingSpeakerphoneIcon) { 436 mStatusBar.setIcon("speakerphone", android.R.drawable.stat_sys_speakerphone, 0); 437 mShowingSpeakerphoneIcon = true; 438 } 439 } 440 441 void cancelSpeakerphone() { 442 if (mShowingSpeakerphoneIcon) { 443 mStatusBar.removeIcon("speakerphone"); 444 mShowingSpeakerphoneIcon = false; 445 } 446 } 447 448 /** 449 * Calls either notifySpeakerphone() or cancelSpeakerphone() based on 450 * the actual current state of the speaker. 451 */ 452 void updateSpeakerNotification() { 453 AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); 454 455 if ((mPhone.getState() == Phone.State.OFFHOOK) && audioManager.isSpeakerphoneOn()) { 456 if (DBG) log("updateSpeakerNotification: speaker ON"); 457 notifySpeakerphone(); 458 } else { 459 if (DBG) log("updateSpeakerNotification: speaker OFF (or not offhook)"); 460 cancelSpeakerphone(); 461 } 462 } 463 464 private void notifyMute() { 465 if (!mShowingMuteIcon) { 466 mStatusBar.setIcon("mute", android.R.drawable.stat_notify_call_mute, 0); 467 mShowingMuteIcon = true; 468 } 469 } 470 471 private void cancelMute() { 472 if (mShowingMuteIcon) { 473 mStatusBar.removeIcon("mute"); 474 mShowingMuteIcon = false; 475 } 476 } 477 478 /** 479 * Calls either notifyMute() or cancelMute() based on 480 * the actual current mute state of the Phone. 481 */ 482 void updateMuteNotification() { 483 if ((mCM.getState() == Phone.State.OFFHOOK) && PhoneUtils.getMute()) { 484 if (DBG) log("updateMuteNotification: MUTED"); 485 notifyMute(); 486 } else { 487 if (DBG) log("updateMuteNotification: not muted (or not offhook)"); 488 cancelMute(); 489 } 490 } 491 492 /** 493 * Updates the phone app's status bar notification based on the 494 * current telephony state, or cancels the notification if the phone 495 * is totally idle. 496 */ 497 void updateInCallNotification() { 498 int resId; 499 if (DBG) log("updateInCallNotification()..."); 500 501 if (mCM.getState() == Phone.State.IDLE) { 502 cancelInCall(); 503 return; 504 } 505 506 final PhoneApp app = PhoneApp.getInstance(); 507 final boolean hasRingingCall = mCM.hasActiveRingingCall(); 508 final boolean hasActiveCall = mCM.hasActiveFgCall(); 509 final boolean hasHoldingCall = mCM.hasActiveBgCall(); 510 if (DBG) { 511 log(" - hasRingingCall = " + hasRingingCall); 512 log(" - hasActiveCall = " + hasActiveCall); 513 log(" - hasHoldingCall = " + hasHoldingCall); 514 } 515 516 // Display the appropriate icon in the status bar, 517 // based on the current phone and/or bluetooth state. 518 519 boolean enhancedVoicePrivacy = app.notifier.getCdmaVoicePrivacyState(); 520 if (DBG) log("updateInCallNotification: enhancedVoicePrivacy = " + enhancedVoicePrivacy); 521 522 if (hasRingingCall) { 523 // There's an incoming ringing call. 524 resId = R.drawable.stat_sys_phone_call_ringing; 525 } else if (!hasActiveCall && hasHoldingCall) { 526 // There's only one call, and it's on hold. 527 if (enhancedVoicePrivacy) { 528 resId = R.drawable.stat_sys_vp_phone_call_on_hold; 529 } else { 530 resId = R.drawable.stat_sys_phone_call_on_hold; 531 } 532 } else if (app.showBluetoothIndication()) { 533 // Bluetooth is active. 534 if (enhancedVoicePrivacy) { 535 resId = R.drawable.stat_sys_vp_phone_call_bluetooth; 536 } else { 537 resId = R.drawable.stat_sys_phone_call_bluetooth; 538 } 539 } else { 540 if (enhancedVoicePrivacy) { 541 resId = R.drawable.stat_sys_vp_phone_call; 542 } else { 543 resId = R.drawable.stat_sys_phone_call; 544 } 545 } 546 547 // Note we can't just bail out now if (resId == mInCallResId), 548 // since even if the status icon hasn't changed, some *other* 549 // notification-related info may be different from the last time 550 // we were here (like the caller-id info of the foreground call, 551 // if the user swapped calls...) 552 553 if (DBG) log("- Updating status bar icon: resId = " + resId); 554 mInCallResId = resId; 555 556 // The icon in the expanded view is the same as in the status bar. 557 int expandedViewIcon = mInCallResId; 558 559 // Even if both lines are in use, we only show a single item in 560 // the expanded Notifications UI. It's labeled "Ongoing call" 561 // (or "On hold" if there's only one call, and it's on hold.) 562 // Also, we don't have room to display caller-id info from two 563 // different calls. So if both lines are in use, display info 564 // from the foreground call. And if there's a ringing call, 565 // display that regardless of the state of the other calls. 566 567 Call currentCall; 568 if (hasRingingCall) { 569 currentCall = mCM.getFirstActiveRingingCall(); 570 } else if (hasActiveCall) { 571 currentCall = mCM.getActiveFgCall(); 572 } else { 573 currentCall = mCM.getFirstActiveBgCall(); 574 } 575 Connection currentConn = currentCall.getEarliestConnection(); 576 577 Notification notification = new Notification(); 578 notification.icon = mInCallResId; 579 notification.flags |= Notification.FLAG_ONGOING_EVENT; 580 581 // PendingIntent that can be used to launch the InCallScreen. The 582 // system fires off this intent if the user pulls down the windowshade 583 // and clicks the notification's expanded view. It's also used to 584 // launch the InCallScreen immediately when when there's an incoming 585 // call (see the "fullScreenIntent" field below). 586 PendingIntent inCallPendingIntent = 587 PendingIntent.getActivity(mContext, 0, 588 PhoneApp.createInCallIntent(), 0); 589 notification.contentIntent = inCallPendingIntent; 590 591 // When expanded, the "Ongoing call" notification is (visually) 592 // different from most other Notifications, so we need to use a 593 // custom view hierarchy. 594 // Our custom view, which includes an icon (either "ongoing call" or 595 // "on hold") and 2 lines of text: (1) the label (either "ongoing 596 // call" with time counter, or "on hold), and (2) the compact name of 597 // the current Connection. 598 RemoteViews contentView = new RemoteViews(mContext.getPackageName(), 599 R.layout.ongoing_call_notification); 600 contentView.setImageViewResource(R.id.icon, expandedViewIcon); 601 602 // if the connection is valid, then build what we need for the 603 // first line of notification information, and start the chronometer. 604 // Otherwise, don't bother and just stick with line 2. 605 if (currentConn != null) { 606 // Determine the "start time" of the current connection, in terms 607 // of the SystemClock.elapsedRealtime() timebase (which is what 608 // the Chronometer widget needs.) 609 // We can't use currentConn.getConnectTime(), because (1) that's 610 // in the currentTimeMillis() time base, and (2) it's zero when 611 // the phone first goes off hook, since the getConnectTime counter 612 // doesn't start until the DIALING -> ACTIVE transition. 613 // Instead we start with the current connection's duration, 614 // and translate that into the elapsedRealtime() timebase. 615 long callDurationMsec = currentConn.getDurationMillis(); 616 long chronometerBaseTime = SystemClock.elapsedRealtime() - callDurationMsec; 617 618 // Line 1 of the expanded view (in bold text): 619 String expandedViewLine1; 620 if (hasRingingCall) { 621 // Incoming call is ringing. 622 // Note this isn't a format string! (We want "Incoming call" 623 // here, not "Incoming call (1:23)".) But that's OK; if you 624 // call String.format() with more arguments than format 625 // specifiers, the extra arguments are ignored. 626 expandedViewLine1 = mContext.getString(R.string.notification_incoming_call); 627 } else if (hasHoldingCall && !hasActiveCall) { 628 // Only one call, and it's on hold. 629 // Note this isn't a format string either (see comment above.) 630 expandedViewLine1 = mContext.getString(R.string.notification_on_hold); 631 } else { 632 // Normal ongoing call. 633 // Format string with a "%s" where the current call time should go. 634 expandedViewLine1 = mContext.getString(R.string.notification_ongoing_call_format); 635 } 636 637 if (DBG) log("- Updating expanded view: line 1 '" + /*expandedViewLine1*/ "xxxxxxx" + "'"); 638 639 // Text line #1 is actually a Chronometer, not a plain TextView. 640 // We format the elapsed time of the current call into a line like 641 // "Ongoing call (01:23)". 642 contentView.setChronometer(R.id.text1, 643 chronometerBaseTime, 644 expandedViewLine1, 645 true); 646 } else if (DBG) { 647 Log.w(LOG_TAG, "updateInCallNotification: null connection, can't set exp view line 1."); 648 } 649 650 // display conference call string if this call is a conference 651 // call, otherwise display the connection information. 652 653 // Line 2 of the expanded view (smaller text). This is usually a 654 // contact name or phone number. 655 String expandedViewLine2 = ""; 656 // TODO: it may not make sense for every point to make separate 657 // checks for isConferenceCall, so we need to think about 658 // possibly including this in startGetCallerInfo or some other 659 // common point. 660 if (PhoneUtils.isConferenceCall(currentCall)) { 661 // if this is a conference call, just use that as the caller name. 662 expandedViewLine2 = mContext.getString(R.string.card_title_conf_call); 663 } else { 664 // If necessary, start asynchronous query to do the caller-id lookup. 665 PhoneUtils.CallerInfoToken cit = 666 PhoneUtils.startGetCallerInfo(mContext, currentCall, this, this); 667 expandedViewLine2 = PhoneUtils.getCompactNameFromCallerInfo(cit.currentInfo, mContext); 668 // Note: For an incoming call, the very first time we get here we 669 // won't have a contact name yet, since we only just started the 670 // caller-id query. So expandedViewLine2 will start off as a raw 671 // phone number, but we'll update it very quickly when the query 672 // completes (see onQueryComplete() below.) 673 } 674 675 if (DBG) log("- Updating expanded view: line 2 '" + /*expandedViewLine2*/ "xxxxxxx" + "'"); 676 contentView.setTextViewText(R.id.text2, expandedViewLine2); 677 notification.contentView = contentView; 678 679 // TODO: We also need to *update* this notification in some cases, 680 // like when a call ends on one line but the other is still in use 681 // (ie. make sure the caller info here corresponds to the active 682 // line), and maybe even when the user swaps calls (ie. if we only 683 // show info here for the "current active call".) 684 685 // Activate a couple of special Notification features if an 686 // incoming call is ringing: 687 if (hasRingingCall) { 688 // We actually want to launch the incoming call UI at this point 689 // (rather than just posting a notification to the status bar). 690 // Setting fullScreenIntent will cause the InCallScreen to be 691 // launched immediately. 692 if (DBG) log("- Setting fullScreenIntent: " + inCallPendingIntent); 693 notification.fullScreenIntent = inCallPendingIntent; 694 695 // Ugly hack alert: 696 // 697 // The NotificationManager has the (undocumented) behavior 698 // that it will *ignore* the fullScreenIntent field if you 699 // post a new Notification that matches the ID of one that's 700 // already active. Unfortunately this is exactly what happens 701 // when you get an incoming call-waiting call: the 702 // "ongoing call" notification is already visible, so the 703 // InCallScreen won't get launched in this case! 704 // (The result: if you bail out of the in-call UI while on a 705 // call and then get a call-waiting call, the incoming call UI 706 // won't come up automatically.) 707 // 708 // The workaround is to just notice this exact case (this is a 709 // call-waiting call *and* the InCallScreen is not in the 710 // foreground) and manually cancel the in-call notification 711 // before (re)posting it. 712 // 713 // TODO: there should be a cleaner way of avoiding this 714 // problem (see discussion in bug 3184149.) 715 Call ringingCall = mCM.getFirstActiveRingingCall(); 716 if ((ringingCall.getState() == Call.State.WAITING) && !app.isShowingCallScreen()) { 717 Log.i(LOG_TAG, "updateInCallNotification: call-waiting! force relaunch..."); 718 // Cancel the IN_CALL_NOTIFICATION immediately before 719 // (re)posting it; this seems to force the 720 // NotificationManager to launch the fullScreenIntent. 721 mNotificationMgr.cancel(IN_CALL_NOTIFICATION); 722 } 723 } 724 725 if (DBG) log("Notifying IN_CALL_NOTIFICATION: " + notification); 726 mNotificationMgr.notify(IN_CALL_NOTIFICATION, 727 notification); 728 729 // Finally, refresh the mute and speakerphone notifications (since 730 // some phone state changes can indirectly affect the mute and/or 731 // speaker state). 732 updateSpeakerNotification(); 733 updateMuteNotification(); 734 } 735 736 /** 737 * Implemented for CallerInfoAsyncQuery.OnQueryCompleteListener interface. 738 * refreshes the contentView when called. 739 */ 740 public void onQueryComplete(int token, Object cookie, CallerInfo ci){ 741 if (DBG) log("CallerInfo query complete (for NotificationMgr), " 742 + "updating in-call notification.."); 743 if (DBG) log("- cookie: " + cookie); 744 if (DBG) log("- ci: " + ci); 745 746 if (cookie == this) { 747 // Ok, this is the caller-id query we fired off in 748 // updateInCallNotification(), presumably when an incoming call 749 // first appeared. If the caller-id info matched any contacts, 750 // compactName should now be a real person name rather than a raw 751 // phone number: 752 if (DBG) log("- compactName is now: " 753 + PhoneUtils.getCompactNameFromCallerInfo(ci, mContext)); 754 755 // Now that our CallerInfo object has been fully filled-in, 756 // refresh the in-call notification. 757 if (DBG) log("- updating notification after query complete..."); 758 updateInCallNotification(); 759 } else { 760 Log.w(LOG_TAG, "onQueryComplete: caller-id query from unknown source! " 761 + "cookie = " + cookie); 762 } 763 } 764 765 private void cancelInCall() { 766 if (DBG) log("cancelInCall()..."); 767 cancelMute(); 768 cancelSpeakerphone(); 769 mNotificationMgr.cancel(IN_CALL_NOTIFICATION); 770 mInCallResId = 0; 771 } 772 773 void cancelCallInProgressNotification() { 774 if (DBG) log("cancelCallInProgressNotification()..."); 775 if (mInCallResId == 0) { 776 return; 777 } 778 779 if (DBG) log("cancelCallInProgressNotification: " + mInCallResId); 780 cancelInCall(); 781 } 782 783 /** 784 * Updates the message waiting indicator (voicemail) notification. 785 * 786 * @param visible true if there are messages waiting 787 */ 788 /* package */ void updateMwi(boolean visible) { 789 if (DBG) log("updateMwi(): " + visible); 790 if (visible) { 791 int resId = android.R.drawable.stat_notify_voicemail; 792 793 // This Notification can get a lot fancier once we have more 794 // information about the current voicemail messages. 795 // (For example, the current voicemail system can't tell 796 // us the caller-id or timestamp of a message, or tell us the 797 // message count.) 798 799 // But for now, the UI is ultra-simple: if the MWI indication 800 // is supposed to be visible, just show a single generic 801 // notification. 802 803 String notificationTitle = mContext.getString(R.string.notification_voicemail_title); 804 String vmNumber = mPhone.getVoiceMailNumber(); 805 if (DBG) log("- got vm number: '" + vmNumber + "'"); 806 807 // Watch out: vmNumber may be null, for two possible reasons: 808 // 809 // (1) This phone really has no voicemail number 810 // 811 // (2) This phone *does* have a voicemail number, but 812 // the SIM isn't ready yet. 813 // 814 // Case (2) *does* happen in practice if you have voicemail 815 // messages when the device first boots: we get an MWI 816 // notification as soon as we register on the network, but the 817 // SIM hasn't finished loading yet. 818 // 819 // So handle case (2) by retrying the lookup after a short 820 // delay. 821 822 if ((vmNumber == null) && !mPhone.getIccRecordsLoaded()) { 823 if (DBG) log("- Null vm number: SIM records not loaded (yet)..."); 824 825 // TODO: rather than retrying after an arbitrary delay, it 826 // would be cleaner to instead just wait for a 827 // SIM_RECORDS_LOADED notification. 828 // (Unfortunately right now there's no convenient way to 829 // get that notification in phone app code. We'd first 830 // want to add a call like registerForSimRecordsLoaded() 831 // to Phone.java and GSMPhone.java, and *then* we could 832 // listen for that in the CallNotifier class.) 833 834 // Limit the number of retries (in case the SIM is broken 835 // or missing and can *never* load successfully.) 836 if (mVmNumberRetriesRemaining-- > 0) { 837 if (DBG) log(" - Retrying in " + VM_NUMBER_RETRY_DELAY_MILLIS + " msec..."); 838 PhoneApp.getInstance().notifier.sendMwiChangedDelayed( 839 VM_NUMBER_RETRY_DELAY_MILLIS); 840 return; 841 } else { 842 Log.w(LOG_TAG, "NotificationMgr.updateMwi: getVoiceMailNumber() failed after " 843 + MAX_VM_NUMBER_RETRIES + " retries; giving up."); 844 // ...and continue with vmNumber==null, just as if the 845 // phone had no VM number set up in the first place. 846 } 847 } 848 849 if (TelephonyCapabilities.supportsVoiceMessageCount(mPhone)) { 850 int vmCount = mPhone.getVoiceMessageCount(); 851 String titleFormat = mContext.getString(R.string.notification_voicemail_title_count); 852 notificationTitle = String.format(titleFormat, vmCount); 853 } 854 855 String notificationText; 856 if (TextUtils.isEmpty(vmNumber)) { 857 notificationText = mContext.getString( 858 R.string.notification_voicemail_no_vm_number); 859 } else { 860 notificationText = String.format( 861 mContext.getString(R.string.notification_voicemail_text_format), 862 PhoneNumberUtils.formatNumber(vmNumber)); 863 } 864 865 Intent intent = new Intent(Intent.ACTION_CALL, 866 Uri.fromParts("voicemail", "", null)); 867 PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0); 868 869 Notification notification = new Notification( 870 resId, // icon 871 null, // tickerText 872 System.currentTimeMillis() // Show the time the MWI notification came in, 873 // since we don't know the actual time of the 874 // most recent voicemail message 875 ); 876 notification.setLatestEventInfo( 877 mContext, // context 878 notificationTitle, // contentTitle 879 notificationText, // contentText 880 pendingIntent // contentIntent 881 ); 882 notification.defaults |= Notification.DEFAULT_SOUND; 883 notification.flags |= Notification.FLAG_NO_CLEAR; 884 configureLedNotification(notification); 885 mNotificationMgr.notify(VOICEMAIL_NOTIFICATION, notification); 886 } else { 887 mNotificationMgr.cancel(VOICEMAIL_NOTIFICATION); 888 } 889 } 890 891 /** 892 * Updates the message call forwarding indicator notification. 893 * 894 * @param visible true if there are messages waiting 895 */ 896 /* package */ void updateCfi(boolean visible) { 897 if (DBG) log("updateCfi(): " + visible); 898 if (visible) { 899 // If Unconditional Call Forwarding (forward all calls) for VOICE 900 // is enabled, just show a notification. We'll default to expanded 901 // view for now, so the there is less confusion about the icon. If 902 // it is deemed too weird to have CF indications as expanded views, 903 // then we'll flip the flag back. 904 905 // TODO: We may want to take a look to see if the notification can 906 // display the target to forward calls to. This will require some 907 // effort though, since there are multiple layers of messages that 908 // will need to propagate that information. 909 910 Notification notification; 911 final boolean showExpandedNotification = true; 912 if (showExpandedNotification) { 913 Intent intent = new Intent(Intent.ACTION_MAIN); 914 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 915 intent.setClassName("com.android.phone", 916 "com.android.phone.CallFeaturesSetting"); 917 918 notification = new Notification( 919 mContext, // context 920 R.drawable.stat_sys_phone_call_forward, // icon 921 null, // tickerText 922 0, // The "timestamp" of this notification is meaningless; 923 // we only care about whether CFI is currently on or not. 924 mContext.getString(R.string.labelCF), // expandedTitle 925 mContext.getString(R.string.sum_cfu_enabled_indicator), // expandedText 926 intent // contentIntent 927 ); 928 929 } else { 930 notification = new Notification( 931 R.drawable.stat_sys_phone_call_forward, // icon 932 null, // tickerText 933 System.currentTimeMillis() // when 934 ); 935 } 936 937 notification.flags |= Notification.FLAG_ONGOING_EVENT; // also implies FLAG_NO_CLEAR 938 939 mNotificationMgr.notify( 940 CALL_FORWARD_NOTIFICATION, 941 notification); 942 } else { 943 mNotificationMgr.cancel(CALL_FORWARD_NOTIFICATION); 944 } 945 } 946 947 /** 948 * Shows the "data disconnected due to roaming" notification, which 949 * appears when you lose data connectivity because you're roaming and 950 * you have the "data roaming" feature turned off. 951 */ 952 /* package */ void showDataDisconnectedRoaming() { 953 if (DBG) log("showDataDisconnectedRoaming()..."); 954 955 Intent intent = new Intent(mContext, 956 Settings.class); // "Mobile network settings" screen 957 958 Notification notification = new Notification( 959 mContext, // context 960 android.R.drawable.stat_sys_warning, // icon 961 null, // tickerText 962 System.currentTimeMillis(), 963 mContext.getString(R.string.roaming), // expandedTitle 964 mContext.getString(R.string.roaming_reenable_message), // expandedText 965 intent // contentIntent 966 ); 967 mNotificationMgr.notify( 968 DATA_DISCONNECTED_ROAMING_NOTIFICATION, 969 notification); 970 } 971 972 /** 973 * Turns off the "data disconnected due to roaming" notification. 974 */ 975 /* package */ void hideDataDisconnectedRoaming() { 976 if (DBG) log("hideDataDisconnectedRoaming()..."); 977 mNotificationMgr.cancel(DATA_DISCONNECTED_ROAMING_NOTIFICATION); 978 } 979 980 /** 981 * Display the network selection "no service" notification 982 * @param operator is the numeric operator number 983 */ 984 private void showNetworkSelection(String operator) { 985 if (DBG) log("showNetworkSelection(" + operator + ")..."); 986 987 String titleText = mContext.getString( 988 R.string.notification_network_selection_title); 989 String expandedText = mContext.getString( 990 R.string.notification_network_selection_text, operator); 991 992 Notification notification = new Notification(); 993 notification.icon = android.R.drawable.stat_sys_warning; 994 notification.when = 0; 995 notification.flags = Notification.FLAG_ONGOING_EVENT; 996 notification.tickerText = null; 997 998 // create the target network operators settings intent 999 Intent intent = new Intent(Intent.ACTION_MAIN); 1000 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | 1001 Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 1002 // Use NetworkSetting to handle the selection intent 1003 intent.setComponent(new ComponentName("com.android.phone", 1004 "com.android.phone.NetworkSetting")); 1005 PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0); 1006 1007 notification.setLatestEventInfo(mContext, titleText, expandedText, pi); 1008 1009 mNotificationMgr.notify(SELECTED_OPERATOR_FAIL_NOTIFICATION, notification); 1010 } 1011 1012 /** 1013 * Turn off the network selection "no service" notification 1014 */ 1015 private void cancelNetworkSelection() { 1016 if (DBG) log("cancelNetworkSelection()..."); 1017 mNotificationMgr.cancel(SELECTED_OPERATOR_FAIL_NOTIFICATION); 1018 } 1019 1020 /** 1021 * Update notification about no service of user selected operator 1022 * 1023 * @param serviceState Phone service state 1024 */ 1025 void updateNetworkSelection(int serviceState) { 1026 if (TelephonyCapabilities.supportsNetworkSelection(mPhone)) { 1027 // get the shared preference of network_selection. 1028 // empty is auto mode, otherwise it is the operator alpha name 1029 // in case there is no operator name, check the operator numeric 1030 SharedPreferences sp = 1031 PreferenceManager.getDefaultSharedPreferences(mContext); 1032 String networkSelection = 1033 sp.getString(PhoneBase.NETWORK_SELECTION_NAME_KEY, ""); 1034 if (TextUtils.isEmpty(networkSelection)) { 1035 networkSelection = 1036 sp.getString(PhoneBase.NETWORK_SELECTION_KEY, ""); 1037 } 1038 1039 if (DBG) log("updateNetworkSelection()..." + "state = " + 1040 serviceState + " new network " + networkSelection); 1041 1042 if (serviceState == ServiceState.STATE_OUT_OF_SERVICE 1043 && !TextUtils.isEmpty(networkSelection)) { 1044 if (!mSelectedUnavailableNotify) { 1045 showNetworkSelection(networkSelection); 1046 mSelectedUnavailableNotify = true; 1047 } 1048 } else { 1049 if (mSelectedUnavailableNotify) { 1050 cancelNetworkSelection(); 1051 mSelectedUnavailableNotify = false; 1052 } 1053 } 1054 } 1055 } 1056 1057 /* package */ void postTransientNotification(int notifyId, CharSequence msg) { 1058 if (mToast != null) { 1059 mToast.cancel(); 1060 } 1061 1062 mToast = Toast.makeText(mContext, msg, Toast.LENGTH_LONG); 1063 mToast.show(); 1064 } 1065 1066 private void log(String msg) { 1067 Log.d(LOG_TAG, msg); 1068 } 1069 } 1070