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 boolean hasRingingCall = mCM.hasActiveRingingCall(); 507 final boolean hasActiveCall = mCM.hasActiveFgCall(); 508 final boolean hasHoldingCall = mCM.hasActiveBgCall(); 509 510 // Display the appropriate icon in the status bar, 511 // based on the current phone and/or bluetooth state. 512 513 boolean enhancedVoicePrivacy = PhoneApp.getInstance().notifier.getCdmaVoicePrivacyState(); 514 if (DBG) log("updateInCallNotification: enhancedVoicePrivacy = " + enhancedVoicePrivacy); 515 516 if (hasRingingCall) { 517 // There's an incoming ringing call. 518 resId = R.drawable.stat_sys_phone_call_ringing; 519 } else if (!hasActiveCall && hasHoldingCall) { 520 // There's only one call, and it's on hold. 521 if (enhancedVoicePrivacy) { 522 resId = R.drawable.stat_sys_vp_phone_call_on_hold; 523 } else { 524 resId = R.drawable.stat_sys_phone_call_on_hold; 525 } 526 } else if (PhoneApp.getInstance().showBluetoothIndication()) { 527 // Bluetooth is active. 528 if (enhancedVoicePrivacy) { 529 resId = R.drawable.stat_sys_vp_phone_call_bluetooth; 530 } else { 531 resId = R.drawable.stat_sys_phone_call_bluetooth; 532 } 533 } else { 534 if (enhancedVoicePrivacy) { 535 resId = R.drawable.stat_sys_vp_phone_call; 536 } else { 537 resId = R.drawable.stat_sys_phone_call; 538 } 539 } 540 541 // Note we can't just bail out now if (resId == mInCallResId), 542 // since even if the status icon hasn't changed, some *other* 543 // notification-related info may be different from the last time 544 // we were here (like the caller-id info of the foreground call, 545 // if the user swapped calls...) 546 547 if (DBG) log("- Updating status bar icon: resId = " + resId); 548 mInCallResId = resId; 549 550 // The icon in the expanded view is the same as in the status bar. 551 int expandedViewIcon = mInCallResId; 552 553 // Even if both lines are in use, we only show a single item in 554 // the expanded Notifications UI. It's labeled "Ongoing call" 555 // (or "On hold" if there's only one call, and it's on hold.) 556 // Also, we don't have room to display caller-id info from two 557 // different calls. So if both lines are in use, display info 558 // from the foreground call. And if there's a ringing call, 559 // display that regardless of the state of the other calls. 560 561 Call currentCall; 562 if (hasRingingCall) { 563 currentCall = mCM.getFirstActiveRingingCall(); 564 } else if (hasActiveCall) { 565 currentCall = mCM.getActiveFgCall(); 566 } else { 567 currentCall = mCM.getFirstActiveBgCall(); 568 } 569 Connection currentConn = currentCall.getEarliestConnection(); 570 571 Notification notification = new Notification(); 572 notification.icon = mInCallResId; 573 notification.flags |= Notification.FLAG_ONGOING_EVENT; 574 575 // PendingIntent that can be used to launch the InCallScreen. The 576 // system fires off this intent if the user pulls down the windowshade 577 // and clicks the notification's expanded view. It's also used to 578 // launch the InCallScreen immediately when when there's an incoming 579 // call (see the "fullScreenIntent" field below). 580 PendingIntent inCallPendingIntent = 581 PendingIntent.getActivity(mContext, 0, 582 PhoneApp.createInCallIntent(), 0); 583 notification.contentIntent = inCallPendingIntent; 584 585 // When expanded, the "Ongoing call" notification is (visually) 586 // different from most other Notifications, so we need to use a 587 // custom view hierarchy. 588 // Our custom view, which includes an icon (either "ongoing call" or 589 // "on hold") and 2 lines of text: (1) the label (either "ongoing 590 // call" with time counter, or "on hold), and (2) the compact name of 591 // the current Connection. 592 RemoteViews contentView = new RemoteViews(mContext.getPackageName(), 593 R.layout.ongoing_call_notification); 594 contentView.setImageViewResource(R.id.icon, expandedViewIcon); 595 596 // if the connection is valid, then build what we need for the 597 // first line of notification information, and start the chronometer. 598 // Otherwise, don't bother and just stick with line 2. 599 if (currentConn != null) { 600 // Determine the "start time" of the current connection, in terms 601 // of the SystemClock.elapsedRealtime() timebase (which is what 602 // the Chronometer widget needs.) 603 // We can't use currentConn.getConnectTime(), because (1) that's 604 // in the currentTimeMillis() time base, and (2) it's zero when 605 // the phone first goes off hook, since the getConnectTime counter 606 // doesn't start until the DIALING -> ACTIVE transition. 607 // Instead we start with the current connection's duration, 608 // and translate that into the elapsedRealtime() timebase. 609 long callDurationMsec = currentConn.getDurationMillis(); 610 long chronometerBaseTime = SystemClock.elapsedRealtime() - callDurationMsec; 611 612 // Line 1 of the expanded view (in bold text): 613 String expandedViewLine1; 614 if (hasRingingCall) { 615 // Incoming call is ringing. 616 // Note this isn't a format string! (We want "Incoming call" 617 // here, not "Incoming call (1:23)".) But that's OK; if you 618 // call String.format() with more arguments than format 619 // specifiers, the extra arguments are ignored. 620 expandedViewLine1 = mContext.getString(R.string.notification_incoming_call); 621 } else if (hasHoldingCall && !hasActiveCall) { 622 // Only one call, and it's on hold. 623 // Note this isn't a format string either (see comment above.) 624 expandedViewLine1 = mContext.getString(R.string.notification_on_hold); 625 } else { 626 // Normal ongoing call. 627 // Format string with a "%s" where the current call time should go. 628 expandedViewLine1 = mContext.getString(R.string.notification_ongoing_call_format); 629 } 630 631 if (DBG) log("- Updating expanded view: line 1 '" + /*expandedViewLine1*/ "xxxxxxx" + "'"); 632 633 // Text line #1 is actually a Chronometer, not a plain TextView. 634 // We format the elapsed time of the current call into a line like 635 // "Ongoing call (01:23)". 636 contentView.setChronometer(R.id.text1, 637 chronometerBaseTime, 638 expandedViewLine1, 639 true); 640 } else if (DBG) { 641 Log.w(LOG_TAG, "updateInCallNotification: null connection, can't set exp view line 1."); 642 } 643 644 // display conference call string if this call is a conference 645 // call, otherwise display the connection information. 646 647 // Line 2 of the expanded view (smaller text). This is usually a 648 // contact name or phone number. 649 String expandedViewLine2 = ""; 650 // TODO: it may not make sense for every point to make separate 651 // checks for isConferenceCall, so we need to think about 652 // possibly including this in startGetCallerInfo or some other 653 // common point. 654 if (PhoneUtils.isConferenceCall(currentCall)) { 655 // if this is a conference call, just use that as the caller name. 656 expandedViewLine2 = mContext.getString(R.string.card_title_conf_call); 657 } else { 658 // If necessary, start asynchronous query to do the caller-id lookup. 659 PhoneUtils.CallerInfoToken cit = 660 PhoneUtils.startGetCallerInfo(mContext, currentCall, this, this); 661 expandedViewLine2 = PhoneUtils.getCompactNameFromCallerInfo(cit.currentInfo, mContext); 662 // Note: For an incoming call, the very first time we get here we 663 // won't have a contact name yet, since we only just started the 664 // caller-id query. So expandedViewLine2 will start off as a raw 665 // phone number, but we'll update it very quickly when the query 666 // completes (see onQueryComplete() below.) 667 } 668 669 if (DBG) log("- Updating expanded view: line 2 '" + /*expandedViewLine2*/ "xxxxxxx" + "'"); 670 contentView.setTextViewText(R.id.text2, expandedViewLine2); 671 notification.contentView = contentView; 672 673 // TODO: We also need to *update* this notification in some cases, 674 // like when a call ends on one line but the other is still in use 675 // (ie. make sure the caller info here corresponds to the active 676 // line), and maybe even when the user swaps calls (ie. if we only 677 // show info here for the "current active call".) 678 679 // Activate a couple of special Notification features if an 680 // incoming call is ringing: 681 if (hasRingingCall) { 682 // We actually want to launch the incoming call UI at this point 683 // (rather than just posting a notification to the status bar). 684 // Setting fullScreenIntent will cause the InCallScreen to be 685 // launched immediately. 686 notification.fullScreenIntent = inCallPendingIntent; 687 } 688 689 if (DBG) log("Notifying IN_CALL_NOTIFICATION: " + notification); 690 mNotificationMgr.notify(IN_CALL_NOTIFICATION, 691 notification); 692 693 // Finally, refresh the mute and speakerphone notifications (since 694 // some phone state changes can indirectly affect the mute and/or 695 // speaker state). 696 updateSpeakerNotification(); 697 updateMuteNotification(); 698 } 699 700 /** 701 * Implemented for CallerInfoAsyncQuery.OnQueryCompleteListener interface. 702 * refreshes the contentView when called. 703 */ 704 public void onQueryComplete(int token, Object cookie, CallerInfo ci){ 705 if (DBG) log("CallerInfo query complete (for NotificationMgr), " 706 + "updating in-call notification.."); 707 if (DBG) log("- cookie: " + cookie); 708 if (DBG) log("- ci: " + ci); 709 710 if (cookie == this) { 711 // Ok, this is the caller-id query we fired off in 712 // updateInCallNotification(), presumably when an incoming call 713 // first appeared. If the caller-id info matched any contacts, 714 // compactName should now be a real person name rather than a raw 715 // phone number: 716 if (DBG) log("- compactName is now: " 717 + PhoneUtils.getCompactNameFromCallerInfo(ci, mContext)); 718 719 // Now that our CallerInfo object has been fully filled-in, 720 // refresh the in-call notification. 721 if (DBG) log("- updating notification after query complete..."); 722 updateInCallNotification(); 723 } else { 724 Log.w(LOG_TAG, "onQueryComplete: caller-id query from unknown source! " 725 + "cookie = " + cookie); 726 } 727 } 728 729 private void cancelInCall() { 730 if (DBG) log("cancelInCall()..."); 731 cancelMute(); 732 cancelSpeakerphone(); 733 mNotificationMgr.cancel(IN_CALL_NOTIFICATION); 734 mInCallResId = 0; 735 } 736 737 void cancelCallInProgressNotification() { 738 if (DBG) log("cancelCallInProgressNotification()..."); 739 if (mInCallResId == 0) { 740 return; 741 } 742 743 if (DBG) log("cancelCallInProgressNotification: " + mInCallResId); 744 cancelInCall(); 745 } 746 747 /** 748 * Updates the message waiting indicator (voicemail) notification. 749 * 750 * @param visible true if there are messages waiting 751 */ 752 /* package */ void updateMwi(boolean visible) { 753 if (DBG) log("updateMwi(): " + visible); 754 if (visible) { 755 int resId = android.R.drawable.stat_notify_voicemail; 756 757 // This Notification can get a lot fancier once we have more 758 // information about the current voicemail messages. 759 // (For example, the current voicemail system can't tell 760 // us the caller-id or timestamp of a message, or tell us the 761 // message count.) 762 763 // But for now, the UI is ultra-simple: if the MWI indication 764 // is supposed to be visible, just show a single generic 765 // notification. 766 767 String notificationTitle = mContext.getString(R.string.notification_voicemail_title); 768 String vmNumber = mPhone.getVoiceMailNumber(); 769 if (DBG) log("- got vm number: '" + vmNumber + "'"); 770 771 // Watch out: vmNumber may be null, for two possible reasons: 772 // 773 // (1) This phone really has no voicemail number 774 // 775 // (2) This phone *does* have a voicemail number, but 776 // the SIM isn't ready yet. 777 // 778 // Case (2) *does* happen in practice if you have voicemail 779 // messages when the device first boots: we get an MWI 780 // notification as soon as we register on the network, but the 781 // SIM hasn't finished loading yet. 782 // 783 // So handle case (2) by retrying the lookup after a short 784 // delay. 785 786 if ((vmNumber == null) && !mPhone.getIccRecordsLoaded()) { 787 if (DBG) log("- Null vm number: SIM records not loaded (yet)..."); 788 789 // TODO: rather than retrying after an arbitrary delay, it 790 // would be cleaner to instead just wait for a 791 // SIM_RECORDS_LOADED notification. 792 // (Unfortunately right now there's no convenient way to 793 // get that notification in phone app code. We'd first 794 // want to add a call like registerForSimRecordsLoaded() 795 // to Phone.java and GSMPhone.java, and *then* we could 796 // listen for that in the CallNotifier class.) 797 798 // Limit the number of retries (in case the SIM is broken 799 // or missing and can *never* load successfully.) 800 if (mVmNumberRetriesRemaining-- > 0) { 801 if (DBG) log(" - Retrying in " + VM_NUMBER_RETRY_DELAY_MILLIS + " msec..."); 802 PhoneApp.getInstance().notifier.sendMwiChangedDelayed( 803 VM_NUMBER_RETRY_DELAY_MILLIS); 804 return; 805 } else { 806 Log.w(LOG_TAG, "NotificationMgr.updateMwi: getVoiceMailNumber() failed after " 807 + MAX_VM_NUMBER_RETRIES + " retries; giving up."); 808 // ...and continue with vmNumber==null, just as if the 809 // phone had no VM number set up in the first place. 810 } 811 } 812 813 if (TelephonyCapabilities.supportsVoiceMessageCount(mPhone)) { 814 int vmCount = mPhone.getVoiceMessageCount(); 815 String titleFormat = mContext.getString(R.string.notification_voicemail_title_count); 816 notificationTitle = String.format(titleFormat, vmCount); 817 } 818 819 String notificationText; 820 if (TextUtils.isEmpty(vmNumber)) { 821 notificationText = mContext.getString( 822 R.string.notification_voicemail_no_vm_number); 823 } else { 824 notificationText = String.format( 825 mContext.getString(R.string.notification_voicemail_text_format), 826 PhoneNumberUtils.formatNumber(vmNumber)); 827 } 828 829 Intent intent = new Intent(Intent.ACTION_CALL, 830 Uri.fromParts("voicemail", "", null)); 831 PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0); 832 833 Notification notification = new Notification( 834 resId, // icon 835 null, // tickerText 836 System.currentTimeMillis() // Show the time the MWI notification came in, 837 // since we don't know the actual time of the 838 // most recent voicemail message 839 ); 840 notification.setLatestEventInfo( 841 mContext, // context 842 notificationTitle, // contentTitle 843 notificationText, // contentText 844 pendingIntent // contentIntent 845 ); 846 notification.defaults |= Notification.DEFAULT_SOUND; 847 notification.flags |= Notification.FLAG_NO_CLEAR; 848 configureLedNotification(notification); 849 mNotificationMgr.notify(VOICEMAIL_NOTIFICATION, notification); 850 } else { 851 mNotificationMgr.cancel(VOICEMAIL_NOTIFICATION); 852 } 853 } 854 855 /** 856 * Updates the message call forwarding indicator notification. 857 * 858 * @param visible true if there are messages waiting 859 */ 860 /* package */ void updateCfi(boolean visible) { 861 if (DBG) log("updateCfi(): " + visible); 862 if (visible) { 863 // If Unconditional Call Forwarding (forward all calls) for VOICE 864 // is enabled, just show a notification. We'll default to expanded 865 // view for now, so the there is less confusion about the icon. If 866 // it is deemed too weird to have CF indications as expanded views, 867 // then we'll flip the flag back. 868 869 // TODO: We may want to take a look to see if the notification can 870 // display the target to forward calls to. This will require some 871 // effort though, since there are multiple layers of messages that 872 // will need to propagate that information. 873 874 Notification notification; 875 final boolean showExpandedNotification = true; 876 if (showExpandedNotification) { 877 Intent intent = new Intent(Intent.ACTION_MAIN); 878 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 879 intent.setClassName("com.android.phone", 880 "com.android.phone.CallFeaturesSetting"); 881 882 notification = new Notification( 883 mContext, // context 884 R.drawable.stat_sys_phone_call_forward, // icon 885 null, // tickerText 886 0, // The "timestamp" of this notification is meaningless; 887 // we only care about whether CFI is currently on or not. 888 mContext.getString(R.string.labelCF), // expandedTitle 889 mContext.getString(R.string.sum_cfu_enabled_indicator), // expandedText 890 intent // contentIntent 891 ); 892 893 } else { 894 notification = new Notification( 895 R.drawable.stat_sys_phone_call_forward, // icon 896 null, // tickerText 897 System.currentTimeMillis() // when 898 ); 899 } 900 901 notification.flags |= Notification.FLAG_ONGOING_EVENT; // also implies FLAG_NO_CLEAR 902 903 mNotificationMgr.notify( 904 CALL_FORWARD_NOTIFICATION, 905 notification); 906 } else { 907 mNotificationMgr.cancel(CALL_FORWARD_NOTIFICATION); 908 } 909 } 910 911 /** 912 * Shows the "data disconnected due to roaming" notification, which 913 * appears when you lose data connectivity because you're roaming and 914 * you have the "data roaming" feature turned off. 915 */ 916 /* package */ void showDataDisconnectedRoaming() { 917 if (DBG) log("showDataDisconnectedRoaming()..."); 918 919 Intent intent = new Intent(mContext, 920 Settings.class); // "Mobile network settings" screen 921 922 Notification notification = new Notification( 923 mContext, // context 924 android.R.drawable.stat_sys_warning, // icon 925 null, // tickerText 926 System.currentTimeMillis(), 927 mContext.getString(R.string.roaming), // expandedTitle 928 mContext.getString(R.string.roaming_reenable_message), // expandedText 929 intent // contentIntent 930 ); 931 mNotificationMgr.notify( 932 DATA_DISCONNECTED_ROAMING_NOTIFICATION, 933 notification); 934 } 935 936 /** 937 * Turns off the "data disconnected due to roaming" notification. 938 */ 939 /* package */ void hideDataDisconnectedRoaming() { 940 if (DBG) log("hideDataDisconnectedRoaming()..."); 941 mNotificationMgr.cancel(DATA_DISCONNECTED_ROAMING_NOTIFICATION); 942 } 943 944 /** 945 * Display the network selection "no service" notification 946 * @param operator is the numeric operator number 947 */ 948 private void showNetworkSelection(String operator) { 949 if (DBG) log("showNetworkSelection(" + operator + ")..."); 950 951 String titleText = mContext.getString( 952 R.string.notification_network_selection_title); 953 String expandedText = mContext.getString( 954 R.string.notification_network_selection_text, operator); 955 956 Notification notification = new Notification(); 957 notification.icon = android.R.drawable.stat_sys_warning; 958 notification.when = 0; 959 notification.flags = Notification.FLAG_ONGOING_EVENT; 960 notification.tickerText = null; 961 962 // create the target network operators settings intent 963 Intent intent = new Intent(Intent.ACTION_MAIN); 964 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | 965 Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 966 // Use NetworkSetting to handle the selection intent 967 intent.setComponent(new ComponentName("com.android.phone", 968 "com.android.phone.NetworkSetting")); 969 PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0); 970 971 notification.setLatestEventInfo(mContext, titleText, expandedText, pi); 972 973 mNotificationMgr.notify(SELECTED_OPERATOR_FAIL_NOTIFICATION, notification); 974 } 975 976 /** 977 * Turn off the network selection "no service" notification 978 */ 979 private void cancelNetworkSelection() { 980 if (DBG) log("cancelNetworkSelection()..."); 981 mNotificationMgr.cancel(SELECTED_OPERATOR_FAIL_NOTIFICATION); 982 } 983 984 /** 985 * Update notification about no service of user selected operator 986 * 987 * @param serviceState Phone service state 988 */ 989 void updateNetworkSelection(int serviceState) { 990 if (TelephonyCapabilities.supportsNetworkSelection(mPhone)) { 991 // get the shared preference of network_selection. 992 // empty is auto mode, otherwise it is the operator alpha name 993 // in case there is no operator name, check the operator numeric 994 SharedPreferences sp = 995 PreferenceManager.getDefaultSharedPreferences(mContext); 996 String networkSelection = 997 sp.getString(PhoneBase.NETWORK_SELECTION_NAME_KEY, ""); 998 if (TextUtils.isEmpty(networkSelection)) { 999 networkSelection = 1000 sp.getString(PhoneBase.NETWORK_SELECTION_KEY, ""); 1001 } 1002 1003 if (DBG) log("updateNetworkSelection()..." + "state = " + 1004 serviceState + " new network " + networkSelection); 1005 1006 if (serviceState == ServiceState.STATE_OUT_OF_SERVICE 1007 && !TextUtils.isEmpty(networkSelection)) { 1008 if (!mSelectedUnavailableNotify) { 1009 showNetworkSelection(networkSelection); 1010 mSelectedUnavailableNotify = true; 1011 } 1012 } else { 1013 if (mSelectedUnavailableNotify) { 1014 cancelNetworkSelection(); 1015 mSelectedUnavailableNotify = false; 1016 } 1017 } 1018 } 1019 } 1020 1021 /* package */ void postTransientNotification(int notifyId, CharSequence msg) { 1022 if (mToast != null) { 1023 mToast.cancel(); 1024 } 1025 1026 mToast = Toast.makeText(mContext, msg, Toast.LENGTH_LONG); 1027 mToast.show(); 1028 } 1029 1030 private void log(String msg) { 1031 Log.d(LOG_TAG, msg); 1032 } 1033 } 1034