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 NotificationMgr(Context context)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 init(Context context)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 getDefault()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 */ getStatusBarMgr()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 StatusBarMgr()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 */ enableNotificationAlerts(boolean enable)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 */ enableExpandedView(boolean enable)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 */ updateStatusBar()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 */ updateNotifications()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 QueryHandler(ContentResolver cr)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 onQueryComplete(int token, Object cookie, Cursor cursor)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 */ getNotificationInfo(Cursor cursor)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 * Displays a notification about a missed call. 367 * 368 * @param nameOrNumber either the contact name, or the phone number if no contact 369 * @param label the label of the number if nameOrNumber is a name, null if it is a number 370 */ notifyMissedCall(String name, String number, String label, long date)371 void notifyMissedCall(String name, String number, String label, long date) { 372 // title resource id 373 int titleResId; 374 // the text in the notification's line 1 and 2. 375 String expandedText, callName; 376 377 // increment number of missed calls. 378 mNumberMissedCalls++; 379 380 // get the name for the ticker text 381 // i.e. "Missed call from <caller name or number>" 382 if (name != null && TextUtils.isGraphic(name)) { 383 callName = name; 384 } else if (!TextUtils.isEmpty(number)){ 385 callName = number; 386 } else { 387 // use "unknown" if the caller is unidentifiable. 388 callName = mContext.getString(R.string.unknown); 389 } 390 391 // display the first line of the notification: 392 // 1 missed call: call name 393 // more than 1 missed call: <number of calls> + "missed calls" 394 if (mNumberMissedCalls == 1) { 395 titleResId = R.string.notification_missedCallTitle; 396 expandedText = callName; 397 } else { 398 titleResId = R.string.notification_missedCallsTitle; 399 expandedText = mContext.getString(R.string.notification_missedCallsMsg, 400 mNumberMissedCalls); 401 } 402 403 // create the target call log intent 404 final Intent intent = PhoneApp.createCallLogIntent(); 405 406 // make the notification 407 mNotificationMgr.notify( 408 MISSED_CALL_NOTIFICATION, 409 new Notification( 410 mContext, // context 411 android.R.drawable.stat_notify_missed_call, // icon 412 mContext.getString( 413 R.string.notification_missedCallTicker, callName), // tickerText 414 date, // when 415 mContext.getText(titleResId), // expandedTitle 416 expandedText, // expandedText 417 intent // contentIntent 418 )); 419 } 420 cancelMissedCallNotification()421 void cancelMissedCallNotification() { 422 // reset the number of missed calls to 0. 423 mNumberMissedCalls = 0; 424 mNotificationMgr.cancel(MISSED_CALL_NOTIFICATION); 425 } 426 notifySpeakerphone()427 void notifySpeakerphone() { 428 if (mSpeakerphoneIcon == null) { 429 mSpeakerphoneIcon = mStatusBar.addIcon("speakerphone", 430 android.R.drawable.stat_sys_speakerphone, 0); 431 } 432 } 433 cancelSpeakerphone()434 void cancelSpeakerphone() { 435 if (mSpeakerphoneIcon != null) { 436 mStatusBar.removeIcon(mSpeakerphoneIcon); 437 mSpeakerphoneIcon = null; 438 } 439 } 440 441 /** 442 * Calls either notifySpeakerphone() or cancelSpeakerphone() based on 443 * the actual current state of the speaker. 444 */ updateSpeakerNotification()445 void updateSpeakerNotification() { 446 AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); 447 448 if ((mPhone.getState() == Phone.State.OFFHOOK) && audioManager.isSpeakerphoneOn()) { 449 if (DBG) log("updateSpeakerNotification: speaker ON"); 450 notifySpeakerphone(); 451 } else { 452 if (DBG) log("updateSpeakerNotification: speaker OFF (or not offhook)"); 453 cancelSpeakerphone(); 454 } 455 } 456 notifyMute()457 void notifyMute() { 458 if (mMuteIcon == null) { 459 mMuteIcon = mStatusBar.addIcon("mute", android.R.drawable.stat_notify_call_mute, 0); 460 } 461 } 462 cancelMute()463 void cancelMute() { 464 if (mMuteIcon != null) { 465 mStatusBar.removeIcon(mMuteIcon); 466 mMuteIcon = null; 467 } 468 } 469 470 /** 471 * Calls either notifyMute() or cancelMute() based on 472 * the actual current mute state of the Phone. 473 */ updateMuteNotification()474 void updateMuteNotification() { 475 if ((mPhone.getState() == Phone.State.OFFHOOK) && mPhone.getMute()) { 476 if (DBG) log("updateMuteNotification: MUTED"); 477 notifyMute(); 478 } else { 479 if (DBG) log("updateMuteNotification: not muted (or not offhook)"); 480 cancelMute(); 481 } 482 } 483 updateInCallNotification()484 void updateInCallNotification() { 485 int resId; 486 if (DBG) log("updateInCallNotification()..."); 487 488 if (mPhone.getState() != Phone.State.OFFHOOK) { 489 return; 490 } 491 492 final boolean hasActiveCall = !mPhone.getForegroundCall().isIdle(); 493 final boolean hasHoldingCall = !mPhone.getBackgroundCall().isIdle(); 494 495 // Display the appropriate "in-call" icon in the status bar, 496 // which depends on the current phone and/or bluetooth state. 497 498 499 boolean enhancedVoicePrivacy = PhoneApp.getInstance().notifier.getCdmaVoicePrivacyState(); 500 if (DBG) log("updateInCallNotification: enhancedVoicePrivacy = " + enhancedVoicePrivacy); 501 502 if (!hasActiveCall && hasHoldingCall) { 503 // There's only one call, and it's on hold. 504 if (enhancedVoicePrivacy) { 505 resId = android.R.drawable.stat_sys_vp_phone_call_on_hold; 506 } else { 507 resId = android.R.drawable.stat_sys_phone_call_on_hold; 508 } 509 } else if (PhoneApp.getInstance().showBluetoothIndication()) { 510 // Bluetooth is active. 511 if (enhancedVoicePrivacy) { 512 resId = com.android.internal.R.drawable.stat_sys_vp_phone_call_bluetooth; 513 } else { 514 resId = com.android.internal.R.drawable.stat_sys_phone_call_bluetooth; 515 } 516 } else { 517 if (enhancedVoicePrivacy) { 518 resId = android.R.drawable.stat_sys_vp_phone_call; 519 } else { 520 resId = android.R.drawable.stat_sys_phone_call; 521 } 522 } 523 524 // Note we can't just bail out now if (resId == mInCallResId), 525 // since even if the status icon hasn't changed, some *other* 526 // notification-related info may be different from the last time 527 // we were here (like the caller-id info of the foreground call, 528 // if the user swapped calls...) 529 530 if (DBG) log("- Updating status bar icon: " + resId); 531 mInCallResId = resId; 532 533 // Even if both lines are in use, we only show a single item in 534 // the expanded Notifications UI. It's labeled "Ongoing call" 535 // (or "On hold" if there's only one call, and it's on hold.) 536 537 // The icon in the expanded view is the same as in the status bar. 538 int expandedViewIcon = mInCallResId; 539 540 // Also, we don't have room to display caller-id info from two 541 // different calls. So if there's only one call, use that, but if 542 // both lines are in use we display the caller-id info from the 543 // foreground call and totally ignore the background call. 544 Call currentCall = hasActiveCall ? mPhone.getForegroundCall() 545 : mPhone.getBackgroundCall(); 546 Connection currentConn = currentCall.getEarliestConnection(); 547 548 // When expanded, the "Ongoing call" notification is (visually) 549 // different from most other Notifications, so we need to use a 550 // custom view hierarchy. 551 552 Notification notification = new Notification(); 553 notification.icon = mInCallResId; 554 notification.contentIntent = PendingIntent.getActivity(mContext, 0, 555 PhoneApp.createInCallIntent(), 0); 556 notification.flags |= Notification.FLAG_ONGOING_EVENT; 557 558 // Our custom view, which includes an icon (either "ongoing call" or 559 // "on hold") and 2 lines of text: (1) the label (either "ongoing 560 // call" with time counter, or "on hold), and (2) the compact name of 561 // the current Connection. 562 RemoteViews contentView = new RemoteViews(mContext.getPackageName(), 563 R.layout.ongoing_call_notification); 564 contentView.setImageViewResource(R.id.icon, expandedViewIcon); 565 566 // if the connection is valid, then build what we need for the 567 // first line of notification information, and start the chronometer. 568 // Otherwise, don't bother and just stick with line 2. 569 if (currentConn != null) { 570 // Determine the "start time" of the current connection, in terms 571 // of the SystemClock.elapsedRealtime() timebase (which is what 572 // the Chronometer widget needs.) 573 // We can't use currentConn.getConnectTime(), because (1) that's 574 // in the currentTimeMillis() time base, and (2) it's zero when 575 // the phone first goes off hook, since the getConnectTime counter 576 // doesn't start until the DIALING -> ACTIVE transition. 577 // Instead we start with the current connection's duration, 578 // and translate that into the elapsedRealtime() timebase. 579 long callDurationMsec = currentConn.getDurationMillis(); 580 long chronometerBaseTime = SystemClock.elapsedRealtime() - callDurationMsec; 581 582 // Line 1 of the expanded view (in bold text): 583 String expandedViewLine1; 584 if (hasHoldingCall && !hasActiveCall) { 585 // Only one call, and it's on hold! 586 // Note this isn't a format string! (We want "On hold" here, 587 // not "On hold (1:23)".) That's OK; if you call 588 // String.format() with more arguments than format specifiers, 589 // the extra arguments are ignored. 590 expandedViewLine1 = mContext.getString(R.string.notification_on_hold); 591 } else { 592 // Format string with a "%s" where the current call time should go. 593 expandedViewLine1 = mContext.getString(R.string.notification_ongoing_call_format); 594 } 595 596 if (DBG) log("- Updating expanded view: line 1 '" + expandedViewLine1 + "'"); 597 598 // Text line #1 is actually a Chronometer, not a plain TextView. 599 // We format the elapsed time of the current call into a line like 600 // "Ongoing call (01:23)". 601 contentView.setChronometer(R.id.text1, 602 chronometerBaseTime, 603 expandedViewLine1, 604 true); 605 } else if (DBG) { 606 log("updateInCallNotification: connection is null, call status not updated."); 607 } 608 609 // display conference call string if this call is a conference 610 // call, otherwise display the connection information. 611 612 // TODO: it may not make sense for every point to make separate 613 // checks for isConferenceCall, so we need to think about 614 // possibly including this in startGetCallerInfo or some other 615 // common point. 616 String expandedViewLine2 = ""; 617 if (PhoneUtils.isConferenceCall(currentCall)) { 618 // if this is a conference call, just use that as the caller name. 619 expandedViewLine2 = mContext.getString(R.string.card_title_conf_call); 620 } else { 621 // Start asynchronous call to get the compact name. 622 PhoneUtils.CallerInfoToken cit = 623 PhoneUtils.startGetCallerInfo (mContext, currentCall, this, contentView); 624 // Line 2 of the expanded view (smaller text): 625 expandedViewLine2 = PhoneUtils.getCompactNameFromCallerInfo(cit.currentInfo, mContext); 626 } 627 628 if (DBG) log("- Updating expanded view: line 2 '" + expandedViewLine2 + "'"); 629 contentView.setTextViewText(R.id.text2, expandedViewLine2); 630 notification.contentView = contentView; 631 632 // TODO: We also need to *update* this notification in some cases, 633 // like when a call ends on one line but the other is still in use 634 // (ie. make sure the caller info here corresponds to the active 635 // line), and maybe even when the user swaps calls (ie. if we only 636 // show info here for the "current active call".) 637 638 if (DBG) log("Notifying IN_CALL_NOTIFICATION: " + notification); 639 mNotificationMgr.notify(IN_CALL_NOTIFICATION, 640 notification); 641 642 // Finally, refresh the mute and speakerphone notifications (since 643 // some phone state changes can indirectly affect the mute and/or 644 // speaker state). 645 updateSpeakerNotification(); 646 updateMuteNotification(); 647 } 648 649 /** 650 * Implemented for CallerInfoAsyncQuery.OnQueryCompleteListener interface. 651 * refreshes the contentView when called. 652 */ onQueryComplete(int token, Object cookie, CallerInfo ci)653 public void onQueryComplete(int token, Object cookie, CallerInfo ci){ 654 if (DBG) log("callerinfo query complete, updating ui."); 655 656 ((RemoteViews) cookie).setTextViewText(R.id.text2, 657 PhoneUtils.getCompactNameFromCallerInfo(ci, mContext)); 658 } 659 cancelInCall()660 private void cancelInCall() { 661 if (DBG) log("cancelInCall()..."); 662 cancelMute(); 663 cancelSpeakerphone(); 664 mNotificationMgr.cancel(IN_CALL_NOTIFICATION); 665 mInCallResId = 0; 666 } 667 cancelCallInProgressNotification()668 void cancelCallInProgressNotification() { 669 if (DBG) log("cancelCallInProgressNotification()..."); 670 if (mInCallResId == 0) { 671 return; 672 } 673 674 if (DBG) log("cancelCallInProgressNotification: " + mInCallResId); 675 cancelInCall(); 676 } 677 678 /** 679 * Updates the message waiting indicator (voicemail) notification. 680 * 681 * @param visible true if there are messages waiting 682 */ updateMwi(boolean visible)683 /* package */ void updateMwi(boolean visible) { 684 if (DBG) log("updateMwi(): " + visible); 685 if (visible) { 686 int resId = android.R.drawable.stat_notify_voicemail; 687 688 // This Notification can get a lot fancier once we have more 689 // information about the current voicemail messages. 690 // (For example, the current voicemail system can't tell 691 // us the caller-id or timestamp of a message, or tell us the 692 // message count.) 693 694 // But for now, the UI is ultra-simple: if the MWI indication 695 // is supposed to be visible, just show a single generic 696 // notification. 697 698 String notificationTitle = mContext.getString(R.string.notification_voicemail_title); 699 String vmNumber = mPhone.getVoiceMailNumber(); 700 if (DBG) log("- got vm number: '" + vmNumber + "'"); 701 702 // Watch out: vmNumber may be null, for two possible reasons: 703 // 704 // (1) This phone really has no voicemail number 705 // 706 // (2) This phone *does* have a voicemail number, but 707 // the SIM isn't ready yet. 708 // 709 // Case (2) *does* happen in practice if you have voicemail 710 // messages when the device first boots: we get an MWI 711 // notification as soon as we register on the network, but the 712 // SIM hasn't finished loading yet. 713 // 714 // So handle case (2) by retrying the lookup after a short 715 // delay. 716 717 if ((vmNumber == null) && !mPhone.getIccRecordsLoaded()) { 718 if (DBG) log("- Null vm number: SIM records not loaded (yet)..."); 719 720 // TODO: rather than retrying after an arbitrary delay, it 721 // would be cleaner to instead just wait for a 722 // SIM_RECORDS_LOADED notification. 723 // (Unfortunately right now there's no convenient way to 724 // get that notification in phone app code. We'd first 725 // want to add a call like registerForSimRecordsLoaded() 726 // to Phone.java and GSMPhone.java, and *then* we could 727 // listen for that in the CallNotifier class.) 728 729 // Limit the number of retries (in case the SIM is broken 730 // or missing and can *never* load successfully.) 731 if (mVmNumberRetriesRemaining-- > 0) { 732 if (DBG) log(" - Retrying in " + VM_NUMBER_RETRY_DELAY_MILLIS + " msec..."); 733 PhoneApp.getInstance().notifier.sendMwiChangedDelayed( 734 VM_NUMBER_RETRY_DELAY_MILLIS); 735 return; 736 } else { 737 Log.w(LOG_TAG, "NotificationMgr.updateMwi: getVoiceMailNumber() failed after " 738 + MAX_VM_NUMBER_RETRIES + " retries; giving up."); 739 // ...and continue with vmNumber==null, just as if the 740 // phone had no VM number set up in the first place. 741 } 742 } 743 744 if (mPhone.getPhoneType() == Phone.PHONE_TYPE_CDMA) { 745 int vmCount = mPhone.getVoiceMessageCount(); 746 String titleFormat = mContext.getString(R.string.notification_voicemail_title_count); 747 notificationTitle = String.format(titleFormat, vmCount); 748 } 749 750 String notificationText; 751 if (TextUtils.isEmpty(vmNumber)) { 752 notificationText = mContext.getString( 753 R.string.notification_voicemail_no_vm_number); 754 } else { 755 notificationText = String.format( 756 mContext.getString(R.string.notification_voicemail_text_format), 757 PhoneNumberUtils.formatNumber(vmNumber)); 758 } 759 760 Intent intent = new Intent(Intent.ACTION_CALL, 761 Uri.fromParts("voicemail", "", null)); 762 PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0); 763 764 Notification notification = new Notification( 765 resId, // icon 766 null, // tickerText 767 System.currentTimeMillis() // Show the time the MWI notification came in, 768 // since we don't know the actual time of the 769 // most recent voicemail message 770 ); 771 notification.setLatestEventInfo( 772 mContext, // context 773 notificationTitle, // contentTitle 774 notificationText, // contentText 775 pendingIntent // contentIntent 776 ); 777 notification.defaults |= Notification.DEFAULT_SOUND; 778 notification.flags |= Notification.FLAG_NO_CLEAR; 779 notification.flags |= Notification.FLAG_SHOW_LIGHTS; 780 notification.ledARGB = 0xff00ff00; 781 notification.ledOnMS = 500; 782 notification.ledOffMS = 2000; 783 784 mNotificationMgr.notify( 785 VOICEMAIL_NOTIFICATION, 786 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 */ updateCfi(boolean visible)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 */ showDataDisconnectedRoaming()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 */ hideDataDisconnectedRoaming()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 */ showNetworkSelection(String operator)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 */ cancelNetworkSelection()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 */ updateNetworkSelection(int serviceState)926 void updateNetworkSelection(int serviceState) { 927 if (mPhone.getPhoneType() == Phone.PHONE_TYPE_GSM) { 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 postTransientNotification(int notifyId, CharSequence msg)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 log(String msg)967 private void log(String msg) { 968 Log.d(LOG_TAG, msg); 969 } 970 } 971