NotificationMgr.java revision 6bfbc4db43492e59d9b2050b93024245483e2c63
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.updateNotifications(); 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 notifications are up to date. 211 */ 212 void updateNotifications() { 213 if (DBG) log("begin querying call log"); 214 215 // instantiate query handler 216 mQueryHandler = new QueryHandler(mContext.getContentResolver()); 217 218 // setup query spec, look for all Missed calls that are new. 219 StringBuilder where = new StringBuilder("type="); 220 where.append(Calls.MISSED_TYPE); 221 where.append(" AND new=1"); 222 223 // start the query 224 mQueryHandler.startQuery(CALL_LOG_TOKEN, null, Calls.CONTENT_URI, CALL_LOG_PROJECTION, 225 where.toString(), null, Calls.DEFAULT_SORT_ORDER); 226 227 // synchronize the in call notification 228 if (mPhone.getState() != Phone.State.OFFHOOK) { 229 if (DBG) log("Phone is idle, canceling notification."); 230 cancelInCall(); 231 } else { 232 if (DBG) log("Phone is offhook, updating notification."); 233 updateInCallNotification(); 234 } 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.ledARGB = 0xff00ff00; 372 note.ledOnMS = 500; 373 note.ledOffMS = 2000; 374 } 375 376 /** 377 * Displays a notification about a missed call. 378 * 379 * @param nameOrNumber either the contact name, or the phone number if no contact 380 * @param label the label of the number if nameOrNumber is a name, null if it is a number 381 */ 382 void notifyMissedCall(String name, String number, String label, long date) { 383 // title resource id 384 int titleResId; 385 // the text in the notification's line 1 and 2. 386 String expandedText, callName; 387 388 // increment number of missed calls. 389 mNumberMissedCalls++; 390 391 // get the name for the ticker text 392 // i.e. "Missed call from <caller name or number>" 393 if (name != null && TextUtils.isGraphic(name)) { 394 callName = name; 395 } else if (!TextUtils.isEmpty(number)){ 396 callName = number; 397 } else { 398 // use "unknown" if the caller is unidentifiable. 399 callName = mContext.getString(R.string.unknown); 400 } 401 402 // display the first line of the notification: 403 // 1 missed call: call name 404 // more than 1 missed call: <number of calls> + "missed calls" 405 if (mNumberMissedCalls == 1) { 406 titleResId = R.string.notification_missedCallTitle; 407 expandedText = callName; 408 } else { 409 titleResId = R.string.notification_missedCallsTitle; 410 expandedText = mContext.getString(R.string.notification_missedCallsMsg, 411 mNumberMissedCalls); 412 } 413 414 // create the target call log intent 415 final Intent intent = PhoneApp.createCallLogIntent(); 416 417 // make the notification 418 Notification note = new Notification(mContext, // context 419 android.R.drawable.stat_notify_missed_call, // icon 420 mContext.getString(R.string.notification_missedCallTicker, callName), // tickerText 421 date, // when 422 mContext.getText(titleResId), // expandedTitle 423 expandedText, // expandedText 424 intent // contentIntent 425 ); 426 configureLedNotification(note); 427 mNotificationMgr.notify(MISSED_CALL_NOTIFICATION, note); 428 } 429 430 void cancelMissedCallNotification() { 431 // reset the number of missed calls to 0. 432 mNumberMissedCalls = 0; 433 mNotificationMgr.cancel(MISSED_CALL_NOTIFICATION); 434 } 435 436 void notifySpeakerphone() { 437 if (mSpeakerphoneIcon == null) { 438 mSpeakerphoneIcon = mStatusBar.addIcon("speakerphone", 439 android.R.drawable.stat_sys_speakerphone, 0); 440 } 441 } 442 443 void cancelSpeakerphone() { 444 if (mSpeakerphoneIcon != null) { 445 mStatusBar.removeIcon(mSpeakerphoneIcon); 446 mSpeakerphoneIcon = null; 447 } 448 } 449 450 /** 451 * Calls either notifySpeakerphone() or cancelSpeakerphone() based on 452 * the actual current state of the speaker. 453 */ 454 void updateSpeakerNotification() { 455 AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); 456 457 if ((mPhone.getState() == Phone.State.OFFHOOK) && audioManager.isSpeakerphoneOn()) { 458 if (DBG) log("updateSpeakerNotification: speaker ON"); 459 notifySpeakerphone(); 460 } else { 461 if (DBG) log("updateSpeakerNotification: speaker OFF (or not offhook)"); 462 cancelSpeakerphone(); 463 } 464 } 465 466 void notifyMute() { 467 if (mMuteIcon == null) { 468 mMuteIcon = mStatusBar.addIcon("mute", android.R.drawable.stat_notify_call_mute, 0); 469 } 470 } 471 472 void cancelMute() { 473 if (mMuteIcon != null) { 474 mStatusBar.removeIcon(mMuteIcon); 475 mMuteIcon = null; 476 } 477 } 478 479 /** 480 * Calls either notifyMute() or cancelMute() based on 481 * the actual current mute state of the Phone. 482 */ 483 void updateMuteNotification() { 484 if ((mPhone.getState() == Phone.State.OFFHOOK) && mPhone.getMute()) { 485 if (DBG) log("updateMuteNotification: MUTED"); 486 notifyMute(); 487 } else { 488 if (DBG) log("updateMuteNotification: not muted (or not offhook)"); 489 cancelMute(); 490 } 491 } 492 493 void updateInCallNotification() { 494 int resId; 495 if (DBG) log("updateInCallNotification()..."); 496 497 if (mPhone.getState() != Phone.State.OFFHOOK) { 498 return; 499 } 500 501 final boolean hasActiveCall = !mPhone.getForegroundCall().isIdle(); 502 final boolean hasHoldingCall = !mPhone.getBackgroundCall().isIdle(); 503 504 // Display the appropriate "in-call" icon in the status bar, 505 // which depends on the current phone and/or bluetooth state. 506 507 508 boolean enhancedVoicePrivacy = PhoneApp.getInstance().notifier.getCdmaVoicePrivacyState(); 509 if (DBG) log("updateInCallNotification: enhancedVoicePrivacy = " + enhancedVoicePrivacy); 510 511 if (!hasActiveCall && hasHoldingCall) { 512 // There's only one call, and it's on hold. 513 if (enhancedVoicePrivacy) { 514 resId = android.R.drawable.stat_sys_vp_phone_call_on_hold; 515 } else { 516 resId = android.R.drawable.stat_sys_phone_call_on_hold; 517 } 518 } else if (PhoneApp.getInstance().showBluetoothIndication()) { 519 // Bluetooth is active. 520 if (enhancedVoicePrivacy) { 521 resId = com.android.internal.R.drawable.stat_sys_vp_phone_call_bluetooth; 522 } else { 523 resId = com.android.internal.R.drawable.stat_sys_phone_call_bluetooth; 524 } 525 } else { 526 if (enhancedVoicePrivacy) { 527 resId = android.R.drawable.stat_sys_vp_phone_call; 528 } else { 529 resId = android.R.drawable.stat_sys_phone_call; 530 } 531 } 532 533 // Note we can't just bail out now if (resId == mInCallResId), 534 // since even if the status icon hasn't changed, some *other* 535 // notification-related info may be different from the last time 536 // we were here (like the caller-id info of the foreground call, 537 // if the user swapped calls...) 538 539 if (DBG) log("- Updating status bar icon: " + resId); 540 mInCallResId = resId; 541 542 // Even if both lines are in use, we only show a single item in 543 // the expanded Notifications UI. It's labeled "Ongoing call" 544 // (or "On hold" if there's only one call, and it's on hold.) 545 546 // The icon in the expanded view is the same as in the status bar. 547 int expandedViewIcon = mInCallResId; 548 549 // Also, we don't have room to display caller-id info from two 550 // different calls. So if there's only one call, use that, but if 551 // both lines are in use we display the caller-id info from the 552 // foreground call and totally ignore the background call. 553 Call currentCall = hasActiveCall ? mPhone.getForegroundCall() 554 : mPhone.getBackgroundCall(); 555 Connection currentConn = currentCall.getEarliestConnection(); 556 557 // When expanded, the "Ongoing call" notification is (visually) 558 // different from most other Notifications, so we need to use a 559 // custom view hierarchy. 560 561 Notification notification = new Notification(); 562 notification.icon = mInCallResId; 563 notification.contentIntent = PendingIntent.getActivity(mContext, 0, 564 PhoneApp.createInCallIntent(), 0); 565 notification.flags |= Notification.FLAG_ONGOING_EVENT; 566 567 // Our custom view, which includes an icon (either "ongoing call" or 568 // "on hold") and 2 lines of text: (1) the label (either "ongoing 569 // call" with time counter, or "on hold), and (2) the compact name of 570 // the current Connection. 571 RemoteViews contentView = new RemoteViews(mContext.getPackageName(), 572 R.layout.ongoing_call_notification); 573 contentView.setImageViewResource(R.id.icon, expandedViewIcon); 574 575 // if the connection is valid, then build what we need for the 576 // first line of notification information, and start the chronometer. 577 // Otherwise, don't bother and just stick with line 2. 578 if (currentConn != null) { 579 // Determine the "start time" of the current connection, in terms 580 // of the SystemClock.elapsedRealtime() timebase (which is what 581 // the Chronometer widget needs.) 582 // We can't use currentConn.getConnectTime(), because (1) that's 583 // in the currentTimeMillis() time base, and (2) it's zero when 584 // the phone first goes off hook, since the getConnectTime counter 585 // doesn't start until the DIALING -> ACTIVE transition. 586 // Instead we start with the current connection's duration, 587 // and translate that into the elapsedRealtime() timebase. 588 long callDurationMsec = currentConn.getDurationMillis(); 589 long chronometerBaseTime = SystemClock.elapsedRealtime() - callDurationMsec; 590 591 // Line 1 of the expanded view (in bold text): 592 String expandedViewLine1; 593 if (hasHoldingCall && !hasActiveCall) { 594 // Only one call, and it's on hold! 595 // Note this isn't a format string! (We want "On hold" here, 596 // not "On hold (1:23)".) That's OK; if you call 597 // String.format() with more arguments than format specifiers, 598 // the extra arguments are ignored. 599 expandedViewLine1 = mContext.getString(R.string.notification_on_hold); 600 } else { 601 // Format string with a "%s" where the current call time should go. 602 expandedViewLine1 = mContext.getString(R.string.notification_ongoing_call_format); 603 } 604 605 if (DBG) log("- Updating expanded view: line 1 '" + expandedViewLine1 + "'"); 606 607 // Text line #1 is actually a Chronometer, not a plain TextView. 608 // We format the elapsed time of the current call into a line like 609 // "Ongoing call (01:23)". 610 contentView.setChronometer(R.id.text1, 611 chronometerBaseTime, 612 expandedViewLine1, 613 true); 614 } else if (DBG) { 615 log("updateInCallNotification: connection is null, call status not updated."); 616 } 617 618 // display conference call string if this call is a conference 619 // call, otherwise display the connection information. 620 621 // TODO: it may not make sense for every point to make separate 622 // checks for isConferenceCall, so we need to think about 623 // possibly including this in startGetCallerInfo or some other 624 // common point. 625 String expandedViewLine2 = ""; 626 if (PhoneUtils.isConferenceCall(currentCall)) { 627 // if this is a conference call, just use that as the caller name. 628 expandedViewLine2 = mContext.getString(R.string.card_title_conf_call); 629 } else { 630 // Start asynchronous call to get the compact name. 631 PhoneUtils.CallerInfoToken cit = 632 PhoneUtils.startGetCallerInfo (mContext, currentCall, this, contentView); 633 // Line 2 of the expanded view (smaller text): 634 expandedViewLine2 = PhoneUtils.getCompactNameFromCallerInfo(cit.currentInfo, mContext); 635 } 636 637 if (DBG) log("- Updating expanded view: line 2 '" + expandedViewLine2 + "'"); 638 contentView.setTextViewText(R.id.text2, expandedViewLine2); 639 notification.contentView = contentView; 640 641 // TODO: We also need to *update* this notification in some cases, 642 // like when a call ends on one line but the other is still in use 643 // (ie. make sure the caller info here corresponds to the active 644 // line), and maybe even when the user swaps calls (ie. if we only 645 // show info here for the "current active call".) 646 647 if (DBG) log("Notifying IN_CALL_NOTIFICATION: " + notification); 648 mNotificationMgr.notify(IN_CALL_NOTIFICATION, 649 notification); 650 651 // Finally, refresh the mute and speakerphone notifications (since 652 // some phone state changes can indirectly affect the mute and/or 653 // speaker state). 654 updateSpeakerNotification(); 655 updateMuteNotification(); 656 } 657 658 /** 659 * Implemented for CallerInfoAsyncQuery.OnQueryCompleteListener interface. 660 * refreshes the contentView when called. 661 */ 662 public void onQueryComplete(int token, Object cookie, CallerInfo ci){ 663 if (DBG) log("callerinfo query complete, updating ui."); 664 665 ((RemoteViews) cookie).setTextViewText(R.id.text2, 666 PhoneUtils.getCompactNameFromCallerInfo(ci, mContext)); 667 } 668 669 private void cancelInCall() { 670 if (DBG) log("cancelInCall()..."); 671 cancelMute(); 672 cancelSpeakerphone(); 673 mNotificationMgr.cancel(IN_CALL_NOTIFICATION); 674 mInCallResId = 0; 675 } 676 677 void cancelCallInProgressNotification() { 678 if (DBG) log("cancelCallInProgressNotification()..."); 679 if (mInCallResId == 0) { 680 return; 681 } 682 683 if (DBG) log("cancelCallInProgressNotification: " + mInCallResId); 684 cancelInCall(); 685 } 686 687 /** 688 * Updates the message waiting indicator (voicemail) notification. 689 * 690 * @param visible true if there are messages waiting 691 */ 692 /* package */ void updateMwi(boolean visible) { 693 if (DBG) log("updateMwi(): " + visible); 694 if (visible) { 695 int resId = android.R.drawable.stat_notify_voicemail; 696 697 // This Notification can get a lot fancier once we have more 698 // information about the current voicemail messages. 699 // (For example, the current voicemail system can't tell 700 // us the caller-id or timestamp of a message, or tell us the 701 // message count.) 702 703 // But for now, the UI is ultra-simple: if the MWI indication 704 // is supposed to be visible, just show a single generic 705 // notification. 706 707 String notificationTitle = mContext.getString(R.string.notification_voicemail_title); 708 String vmNumber = mPhone.getVoiceMailNumber(); 709 if (DBG) log("- got vm number: '" + vmNumber + "'"); 710 711 // Watch out: vmNumber may be null, for two possible reasons: 712 // 713 // (1) This phone really has no voicemail number 714 // 715 // (2) This phone *does* have a voicemail number, but 716 // the SIM isn't ready yet. 717 // 718 // Case (2) *does* happen in practice if you have voicemail 719 // messages when the device first boots: we get an MWI 720 // notification as soon as we register on the network, but the 721 // SIM hasn't finished loading yet. 722 // 723 // So handle case (2) by retrying the lookup after a short 724 // delay. 725 726 if ((vmNumber == null) && !mPhone.getIccRecordsLoaded()) { 727 if (DBG) log("- Null vm number: SIM records not loaded (yet)..."); 728 729 // TODO: rather than retrying after an arbitrary delay, it 730 // would be cleaner to instead just wait for a 731 // SIM_RECORDS_LOADED notification. 732 // (Unfortunately right now there's no convenient way to 733 // get that notification in phone app code. We'd first 734 // want to add a call like registerForSimRecordsLoaded() 735 // to Phone.java and GSMPhone.java, and *then* we could 736 // listen for that in the CallNotifier class.) 737 738 // Limit the number of retries (in case the SIM is broken 739 // or missing and can *never* load successfully.) 740 if (mVmNumberRetriesRemaining-- > 0) { 741 if (DBG) log(" - Retrying in " + VM_NUMBER_RETRY_DELAY_MILLIS + " msec..."); 742 PhoneApp.getInstance().notifier.sendMwiChangedDelayed( 743 VM_NUMBER_RETRY_DELAY_MILLIS); 744 return; 745 } else { 746 Log.w(LOG_TAG, "NotificationMgr.updateMwi: getVoiceMailNumber() failed after " 747 + MAX_VM_NUMBER_RETRIES + " retries; giving up."); 748 // ...and continue with vmNumber==null, just as if the 749 // phone had no VM number set up in the first place. 750 } 751 } 752 753 if (mPhone.getPhoneType() == Phone.PHONE_TYPE_CDMA) { 754 int vmCount = mPhone.getVoiceMessageCount(); 755 String titleFormat = mContext.getString(R.string.notification_voicemail_title_count); 756 notificationTitle = String.format(titleFormat, vmCount); 757 } 758 759 String notificationText; 760 if (TextUtils.isEmpty(vmNumber)) { 761 notificationText = mContext.getString( 762 R.string.notification_voicemail_no_vm_number); 763 } else { 764 notificationText = String.format( 765 mContext.getString(R.string.notification_voicemail_text_format), 766 PhoneNumberUtils.formatNumber(vmNumber)); 767 } 768 769 Intent intent = new Intent(Intent.ACTION_CALL, 770 Uri.fromParts("voicemail", "", null)); 771 PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0); 772 773 Notification notification = new Notification( 774 resId, // icon 775 null, // tickerText 776 System.currentTimeMillis() // Show the time the MWI notification came in, 777 // since we don't know the actual time of the 778 // most recent voicemail message 779 ); 780 notification.setLatestEventInfo( 781 mContext, // context 782 notificationTitle, // contentTitle 783 notificationText, // contentText 784 pendingIntent // contentIntent 785 ); 786 notification.defaults |= Notification.DEFAULT_SOUND; 787 notification.flags |= Notification.FLAG_NO_CLEAR; 788 configureLedNotification(notification); 789 mNotificationMgr.notify(VOICEMAIL_NOTIFICATION, notification); 790 } else { 791 mNotificationMgr.cancel(VOICEMAIL_NOTIFICATION); 792 } 793 } 794 795 /** 796 * Updates the message call forwarding indicator notification. 797 * 798 * @param visible true if there are messages waiting 799 */ 800 /* package */ void updateCfi(boolean visible) { 801 if (DBG) log("updateCfi(): " + visible); 802 if (visible) { 803 // If Unconditional Call Forwarding (forward all calls) for VOICE 804 // is enabled, just show a notification. We'll default to expanded 805 // view for now, so the there is less confusion about the icon. If 806 // it is deemed too weird to have CF indications as expanded views, 807 // then we'll flip the flag back. 808 809 // TODO: We may want to take a look to see if the notification can 810 // display the target to forward calls to. This will require some 811 // effort though, since there are multiple layers of messages that 812 // will need to propagate that information. 813 814 Notification notification; 815 final boolean showExpandedNotification = true; 816 if (showExpandedNotification) { 817 Intent intent = new Intent(Intent.ACTION_MAIN); 818 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 819 intent.setClassName("com.android.phone", 820 "com.android.phone.CallFeaturesSetting"); 821 822 notification = new Notification( 823 mContext, // context 824 android.R.drawable.stat_sys_phone_call_forward, // icon 825 null, // tickerText 826 0, // The "timestamp" of this notification is meaningless; 827 // we only care about whether CFI is currently on or not. 828 mContext.getString(R.string.labelCF), // expandedTitle 829 mContext.getString(R.string.sum_cfu_enabled_indicator), // expandedText 830 intent // contentIntent 831 ); 832 833 } else { 834 notification = new Notification( 835 android.R.drawable.stat_sys_phone_call_forward, // icon 836 null, // tickerText 837 System.currentTimeMillis() // when 838 ); 839 } 840 841 notification.flags |= Notification.FLAG_ONGOING_EVENT; // also implies FLAG_NO_CLEAR 842 843 mNotificationMgr.notify( 844 CALL_FORWARD_NOTIFICATION, 845 notification); 846 } else { 847 mNotificationMgr.cancel(CALL_FORWARD_NOTIFICATION); 848 } 849 } 850 851 /** 852 * Shows the "data disconnected due to roaming" notification, which 853 * appears when you lose data connectivity because you're roaming and 854 * you have the "data roaming" feature turned off. 855 */ 856 /* package */ void showDataDisconnectedRoaming() { 857 if (DBG) log("showDataDisconnectedRoaming()..."); 858 859 Intent intent = new Intent(mContext, 860 Settings.class); // "Mobile network settings" screen 861 862 Notification notification = new Notification( 863 mContext, // context 864 android.R.drawable.stat_sys_warning, // icon 865 null, // tickerText 866 System.currentTimeMillis(), 867 mContext.getString(R.string.roaming), // expandedTitle 868 mContext.getString(R.string.roaming_reenable_message), // expandedText 869 intent // contentIntent 870 ); 871 mNotificationMgr.notify( 872 DATA_DISCONNECTED_ROAMING_NOTIFICATION, 873 notification); 874 } 875 876 /** 877 * Turns off the "data disconnected due to roaming" notification. 878 */ 879 /* package */ void hideDataDisconnectedRoaming() { 880 if (DBG) log("hideDataDisconnectedRoaming()..."); 881 mNotificationMgr.cancel(DATA_DISCONNECTED_ROAMING_NOTIFICATION); 882 } 883 884 /** 885 * Display the network selection "no service" notification 886 * @param operator is the numeric operator number 887 */ 888 private void showNetworkSelection(String operator) { 889 if (DBG) log("showNetworkSelection(" + operator + ")..."); 890 891 String titleText = mContext.getString( 892 R.string.notification_network_selection_title); 893 String expandedText = mContext.getString( 894 R.string.notification_network_selection_text, operator); 895 896 Notification notification = new Notification(); 897 notification.icon = com.android.internal.R.drawable.stat_sys_warning; 898 notification.when = 0; 899 notification.flags = Notification.FLAG_ONGOING_EVENT; 900 notification.tickerText = null; 901 902 // create the target network operators settings intent 903 Intent intent = new Intent(Intent.ACTION_MAIN); 904 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | 905 Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 906 // Use NetworkSetting to handle the selection intent 907 intent.setComponent(new ComponentName("com.android.phone", 908 "com.android.phone.NetworkSetting")); 909 PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0); 910 911 notification.setLatestEventInfo(mContext, titleText, expandedText, pi); 912 913 mNotificationMgr.notify(SELECTED_OPERATOR_FAIL_NOTIFICATION, notification); 914 } 915 916 /** 917 * Turn off the network selection "no service" notification 918 */ 919 private void cancelNetworkSelection() { 920 if (DBG) log("cancelNetworkSelection()..."); 921 mNotificationMgr.cancel(SELECTED_OPERATOR_FAIL_NOTIFICATION); 922 } 923 924 /** 925 * Update notification about no service of user selected operator 926 * 927 * @param serviceState Phone service state 928 */ 929 void updateNetworkSelection(int serviceState) { 930 if (mPhone.getPhoneType() == Phone.PHONE_TYPE_GSM) { 931 // get the shared preference of network_selection. 932 // empty is auto mode, otherwise it is the operator alpha name 933 // in case there is no operator name, check the operator numeric 934 SharedPreferences sp = 935 PreferenceManager.getDefaultSharedPreferences(mContext); 936 String networkSelection = 937 sp.getString(PhoneBase.NETWORK_SELECTION_NAME_KEY, ""); 938 if (TextUtils.isEmpty(networkSelection)) { 939 networkSelection = 940 sp.getString(PhoneBase.NETWORK_SELECTION_KEY, ""); 941 } 942 943 if (DBG) log("updateNetworkSelection()..." + "state = " + 944 serviceState + " new network " + networkSelection); 945 946 if (serviceState == ServiceState.STATE_OUT_OF_SERVICE 947 && !TextUtils.isEmpty(networkSelection)) { 948 if (!mSelectedUnavailableNotify) { 949 showNetworkSelection(networkSelection); 950 mSelectedUnavailableNotify = true; 951 } 952 } else { 953 if (mSelectedUnavailableNotify) { 954 cancelNetworkSelection(); 955 mSelectedUnavailableNotify = false; 956 } 957 } 958 } 959 } 960 961 /* package */ void postTransientNotification(int notifyId, CharSequence msg) { 962 if (mToast != null) { 963 mToast.cancel(); 964 } 965 966 mToast = Toast.makeText(mContext, msg, Toast.LENGTH_LONG); 967 mToast.show(); 968 } 969 970 private void log(String msg) { 971 Log.d(LOG_TAG, msg); 972 } 973} 974