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