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