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.ContentUris; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.SharedPreferences; 30 import android.database.Cursor; 31 import android.graphics.Bitmap; 32 import android.graphics.drawable.BitmapDrawable; 33 import android.graphics.drawable.Drawable; 34 import android.media.AudioManager; 35 import android.net.Uri; 36 import android.os.PowerManager; 37 import android.os.SystemProperties; 38 import android.preference.PreferenceManager; 39 import android.provider.CallLog.Calls; 40 import android.provider.ContactsContract.Contacts; 41 import android.provider.ContactsContract.PhoneLookup; 42 import android.provider.Settings; 43 import android.telephony.PhoneNumberUtils; 44 import android.telephony.ServiceState; 45 import android.text.TextUtils; 46 import android.util.Log; 47 import android.widget.ImageView; 48 import android.widget.Toast; 49 50 import com.android.internal.telephony.Call; 51 import com.android.internal.telephony.CallManager; 52 import com.android.internal.telephony.CallerInfo; 53 import com.android.internal.telephony.CallerInfoAsyncQuery; 54 import com.android.internal.telephony.Connection; 55 import com.android.internal.telephony.Phone; 56 import com.android.internal.telephony.PhoneBase; 57 import com.android.internal.telephony.TelephonyCapabilities; 58 59 /** 60 * NotificationManager-related utility code for the Phone app. 61 * 62 * This is a singleton object which acts as the interface to the 63 * framework's NotificationManager, and is used to display status bar 64 * icons and control other status bar-related behavior. 65 * 66 * @see PhoneApp.notificationMgr 67 */ 68 public class NotificationMgr implements CallerInfoAsyncQuery.OnQueryCompleteListener{ 69 private static final String LOG_TAG = "NotificationMgr"; 70 private static final boolean DBG = 71 (PhoneApp.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1); 72 // Do not check in with VDBG = true, since that may write PII to the system log. 73 private static final boolean VDBG = false; 74 75 private static final String[] CALL_LOG_PROJECTION = new String[] { 76 Calls._ID, 77 Calls.NUMBER, 78 Calls.DATE, 79 Calls.DURATION, 80 Calls.TYPE, 81 }; 82 83 // notification types 84 static final int MISSED_CALL_NOTIFICATION = 1; 85 static final int IN_CALL_NOTIFICATION = 2; 86 static final int MMI_NOTIFICATION = 3; 87 static final int NETWORK_SELECTION_NOTIFICATION = 4; 88 static final int VOICEMAIL_NOTIFICATION = 5; 89 static final int CALL_FORWARD_NOTIFICATION = 6; 90 static final int DATA_DISCONNECTED_ROAMING_NOTIFICATION = 7; 91 static final int SELECTED_OPERATOR_FAIL_NOTIFICATION = 8; 92 93 /** The singleton NotificationMgr instance. */ 94 private static NotificationMgr sInstance; 95 96 private PhoneApp mApp; 97 private Phone mPhone; 98 private CallManager mCM; 99 100 private Context mContext; 101 private NotificationManager mNotificationManager; 102 private StatusBarManager mStatusBarManager; 103 private PowerManager mPowerManager; 104 private Toast mToast; 105 private boolean mShowingSpeakerphoneIcon; 106 private boolean mShowingMuteIcon; 107 108 public StatusBarHelper statusBarHelper; 109 110 // used to track the missed call counter, default to 0. 111 private int mNumberMissedCalls = 0; 112 113 // Currently-displayed resource IDs for some status bar icons (or zero 114 // if no notification is active): 115 private int mInCallResId; 116 117 // used to track the notification of selected network unavailable 118 private boolean mSelectedUnavailableNotify = false; 119 120 // Retry params for the getVoiceMailNumber() call; see updateMwi(). 121 private static final int MAX_VM_NUMBER_RETRIES = 5; 122 private static final int VM_NUMBER_RETRY_DELAY_MILLIS = 10000; 123 private int mVmNumberRetriesRemaining = MAX_VM_NUMBER_RETRIES; 124 125 // Query used to look up caller-id info for the "call log" notification. 126 private QueryHandler mQueryHandler = null; 127 private static final int CALL_LOG_TOKEN = -1; 128 private static final int CONTACT_TOKEN = -2; 129 130 /** 131 * Private constructor (this is a singleton). 132 * @see init() 133 */ 134 private NotificationMgr(PhoneApp app) { 135 mApp = app; 136 mContext = app; 137 mNotificationManager = 138 (NotificationManager) app.getSystemService(Context.NOTIFICATION_SERVICE); 139 mStatusBarManager = 140 (StatusBarManager) app.getSystemService(Context.STATUS_BAR_SERVICE); 141 mPowerManager = 142 (PowerManager) app.getSystemService(Context.POWER_SERVICE); 143 mPhone = app.phone; // TODO: better style to use mCM.getDefaultPhone() everywhere instead 144 mCM = app.mCM; 145 statusBarHelper = new StatusBarHelper(); 146 } 147 148 /** 149 * Initialize the singleton NotificationMgr instance. 150 * 151 * This is only done once, at startup, from PhoneApp.onCreate(). 152 * From then on, the NotificationMgr instance is available via the 153 * PhoneApp's public "notificationMgr" field, which is why there's no 154 * getInstance() method here. 155 */ 156 /* package */ static NotificationMgr init(PhoneApp app) { 157 synchronized (NotificationMgr.class) { 158 if (sInstance == null) { 159 sInstance = new NotificationMgr(app); 160 // Update the notifications that need to be touched at startup. 161 sInstance.updateNotificationsAtStartup(); 162 } else { 163 Log.wtf(LOG_TAG, "init() called multiple times! sInstance = " + sInstance); 164 } 165 return sInstance; 166 } 167 } 168 169 /** 170 * Helper class that's a wrapper around the framework's 171 * StatusBarManager.disable() API. 172 * 173 * This class is used to control features like: 174 * 175 * - Disabling the status bar "notification windowshade" 176 * while the in-call UI is up 177 * 178 * - Disabling notification alerts (audible or vibrating) 179 * while a phone call is active 180 * 181 * - Disabling navigation via the system bar (the "soft buttons" at 182 * the bottom of the screen on devices with no hard buttons) 183 * 184 * We control these features through a single point of control to make 185 * sure that the various StatusBarManager.disable() calls don't 186 * interfere with each other. 187 */ 188 public class StatusBarHelper { 189 // Current desired state of status bar / system bar behavior 190 private boolean mIsNotificationEnabled = true; 191 private boolean mIsExpandedViewEnabled = true; 192 private boolean mIsSystemBarNavigationEnabled = true; 193 194 private StatusBarHelper () { 195 } 196 197 /** 198 * Enables or disables auditory / vibrational alerts. 199 * 200 * (We disable these any time a voice call is active, regardless 201 * of whether or not the in-call UI is visible.) 202 */ 203 public void enableNotificationAlerts(boolean enable) { 204 if (mIsNotificationEnabled != enable) { 205 mIsNotificationEnabled = enable; 206 updateStatusBar(); 207 } 208 } 209 210 /** 211 * Enables or disables the expanded view of the status bar 212 * (i.e. the ability to pull down the "notification windowshade"). 213 * 214 * (This feature is disabled by the InCallScreen while the in-call 215 * UI is active.) 216 */ 217 public void enableExpandedView(boolean enable) { 218 if (mIsExpandedViewEnabled != enable) { 219 mIsExpandedViewEnabled = enable; 220 updateStatusBar(); 221 } 222 } 223 224 /** 225 * Enables or disables the navigation via the system bar (the 226 * "soft buttons" at the bottom of the screen) 227 * 228 * (This feature is disabled while an incoming call is ringing, 229 * because it's easy to accidentally touch the system bar while 230 * pulling the phone out of your pocket.) 231 */ 232 public void enableSystemBarNavigation(boolean enable) { 233 if (mIsSystemBarNavigationEnabled != enable) { 234 mIsSystemBarNavigationEnabled = enable; 235 updateStatusBar(); 236 } 237 } 238 239 /** 240 * Updates the status bar to reflect the current desired state. 241 */ 242 private void updateStatusBar() { 243 int state = StatusBarManager.DISABLE_NONE; 244 245 if (!mIsExpandedViewEnabled) { 246 state |= StatusBarManager.DISABLE_EXPAND; 247 } 248 if (!mIsNotificationEnabled) { 249 state |= StatusBarManager.DISABLE_NOTIFICATION_ALERTS; 250 } 251 if (!mIsSystemBarNavigationEnabled) { 252 // Disable *all* possible navigation via the system bar. 253 state |= StatusBarManager.DISABLE_HOME; 254 state |= StatusBarManager.DISABLE_RECENT; 255 state |= StatusBarManager.DISABLE_BACK; 256 } 257 258 if (DBG) log("updateStatusBar: state = 0x" + Integer.toHexString(state)); 259 mStatusBarManager.disable(state); 260 } 261 } 262 263 /** 264 * Makes sure phone-related notifications are up to date on a 265 * freshly-booted device. 266 */ 267 private void updateNotificationsAtStartup() { 268 if (DBG) log("updateNotificationsAtStartup()..."); 269 270 // instantiate query handler 271 mQueryHandler = new QueryHandler(mContext.getContentResolver()); 272 273 // setup query spec, look for all Missed calls that are new. 274 StringBuilder where = new StringBuilder("type="); 275 where.append(Calls.MISSED_TYPE); 276 where.append(" AND new=1"); 277 278 // start the query 279 if (DBG) log("- start call log query..."); 280 mQueryHandler.startQuery(CALL_LOG_TOKEN, null, Calls.CONTENT_URI, CALL_LOG_PROJECTION, 281 where.toString(), null, Calls.DEFAULT_SORT_ORDER); 282 283 // Update (or cancel) the in-call notification 284 if (DBG) log("- updating in-call notification at startup..."); 285 updateInCallNotification(); 286 287 // Depend on android.app.StatusBarManager to be set to 288 // disable(DISABLE_NONE) upon startup. This will be the 289 // case even if the phone app crashes. 290 } 291 292 /** The projection to use when querying the phones table */ 293 static final String[] PHONES_PROJECTION = new String[] { 294 PhoneLookup.NUMBER, 295 PhoneLookup.DISPLAY_NAME, 296 PhoneLookup._ID 297 }; 298 299 /** 300 * Class used to run asynchronous queries to re-populate the notifications we care about. 301 * There are really 3 steps to this: 302 * 1. Find the list of missed calls 303 * 2. For each call, run a query to retrieve the caller's name. 304 * 3. For each caller, try obtaining photo. 305 */ 306 private class QueryHandler extends AsyncQueryHandler 307 implements ContactsAsyncHelper.OnImageLoadCompleteListener { 308 309 /** 310 * Used to store relevant fields for the Missed Call 311 * notifications. 312 */ 313 private class NotificationInfo { 314 public String name; 315 public String number; 316 /** 317 * Type of the call. {@link android.provider.CallLog.Calls#INCOMING_TYPE} 318 * {@link android.provider.CallLog.Calls#OUTGOING_TYPE}, or 319 * {@link android.provider.CallLog.Calls#MISSED_TYPE}. 320 */ 321 public String type; 322 public long date; 323 } 324 325 public QueryHandler(ContentResolver cr) { 326 super(cr); 327 } 328 329 /** 330 * Handles the query results. 331 */ 332 @Override 333 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 334 // TODO: it would be faster to use a join here, but for the purposes 335 // of this small record set, it should be ok. 336 337 // Note that CursorJoiner is not useable here because the number 338 // comparisons are not strictly equals; the comparisons happen in 339 // the SQL function PHONE_NUMBERS_EQUAL, which is not available for 340 // the CursorJoiner. 341 342 // Executing our own query is also feasible (with a join), but that 343 // will require some work (possibly destabilizing) in Contacts 344 // Provider. 345 346 // At this point, we will execute subqueries on each row just as 347 // CallLogActivity.java does. 348 switch (token) { 349 case CALL_LOG_TOKEN: 350 if (DBG) log("call log query complete."); 351 352 // initial call to retrieve the call list. 353 if (cursor != null) { 354 while (cursor.moveToNext()) { 355 // for each call in the call log list, create 356 // the notification object and query contacts 357 NotificationInfo n = getNotificationInfo (cursor); 358 359 if (DBG) log("query contacts for number: " + n.number); 360 361 mQueryHandler.startQuery(CONTACT_TOKEN, n, 362 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, n.number), 363 PHONES_PROJECTION, null, null, PhoneLookup.NUMBER); 364 } 365 366 if (DBG) log("closing call log cursor."); 367 cursor.close(); 368 } 369 break; 370 case CONTACT_TOKEN: 371 if (DBG) log("contact query complete."); 372 373 // subqueries to get the caller name. 374 if ((cursor != null) && (cookie != null)){ 375 NotificationInfo n = (NotificationInfo) cookie; 376 377 Uri personUri = null; 378 if (cursor.moveToFirst()) { 379 n.name = cursor.getString( 380 cursor.getColumnIndexOrThrow(PhoneLookup.DISPLAY_NAME)); 381 long person_id = cursor.getLong( 382 cursor.getColumnIndexOrThrow(PhoneLookup._ID)); 383 if (DBG) { 384 log("contact :" + n.name + " found for phone: " + n.number 385 + ". id : " + person_id); 386 } 387 personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, person_id); 388 } 389 390 if (personUri != null) { 391 if (DBG) { 392 log("Start obtaining picture for the missed call. Uri: " 393 + personUri); 394 } 395 // Now try to obtain a photo for this person. 396 // ContactsAsyncHelper will do that and call onImageLoadComplete() 397 // after that. 398 ContactsAsyncHelper.startObtainPhotoAsync( 399 0, mContext, personUri, this, n); 400 } else { 401 if (DBG) { 402 log("Failed to find Uri for obtaining photo." 403 + " Just send notification without it."); 404 } 405 // We couldn't find person Uri, so we're sure we cannot obtain a photo. 406 // Call notifyMissedCall() right now. 407 notifyMissedCall(n.name, n.number, n.type, null, null, n.date); 408 } 409 410 if (DBG) log("closing contact cursor."); 411 cursor.close(); 412 } 413 break; 414 default: 415 } 416 } 417 418 @Override 419 public void onImageLoadComplete( 420 int token, Drawable photo, Bitmap photoIcon, Object cookie) { 421 if (DBG) log("Finished loading image: " + photo); 422 NotificationInfo n = (NotificationInfo) cookie; 423 notifyMissedCall(n.name, n.number, n.type, photo, photoIcon, n.date); 424 } 425 426 /** 427 * Factory method to generate a NotificationInfo object given a 428 * cursor from the call log table. 429 */ 430 private final NotificationInfo getNotificationInfo(Cursor cursor) { 431 NotificationInfo n = new NotificationInfo(); 432 n.name = null; 433 n.number = cursor.getString(cursor.getColumnIndexOrThrow(Calls.NUMBER)); 434 n.type = cursor.getString(cursor.getColumnIndexOrThrow(Calls.TYPE)); 435 n.date = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DATE)); 436 437 // make sure we update the number depending upon saved values in 438 // CallLog.addCall(). If either special values for unknown or 439 // private number are detected, we need to hand off the message 440 // to the missed call notification. 441 if ( (n.number.equals(CallerInfo.UNKNOWN_NUMBER)) || 442 (n.number.equals(CallerInfo.PRIVATE_NUMBER)) || 443 (n.number.equals(CallerInfo.PAYPHONE_NUMBER)) ) { 444 n.number = null; 445 } 446 447 if (DBG) log("NotificationInfo constructed for number: " + n.number); 448 449 return n; 450 } 451 } 452 453 /** 454 * Configures a Notification to emit the blinky green message-waiting/ 455 * missed-call signal. 456 */ 457 private static void configureLedNotification(Notification note) { 458 note.flags |= Notification.FLAG_SHOW_LIGHTS; 459 note.defaults |= Notification.DEFAULT_LIGHTS; 460 } 461 462 /** 463 * Displays a notification about a missed call. 464 * 465 * @param name the contact name. 466 * @param number the phone number. Note that this may be a non-callable String like "Unknown", 467 * or "Private Number", which possibly come from methods like 468 * {@link PhoneUtils#modifyForSpecialCnapCases(Context, CallerInfo, String, int)}. 469 * @param type the type of the call. {@link android.provider.CallLog.Calls#INCOMING_TYPE} 470 * {@link android.provider.CallLog.Calls#OUTGOING_TYPE}, or 471 * {@link android.provider.CallLog.Calls#MISSED_TYPE} 472 * @param photo picture which may be used for the notification (when photoIcon is null). 473 * This also can be null when the picture itself isn't available. If photoIcon is available 474 * it should be prioritized (because this may be too huge for notification). 475 * See also {@link ContactsAsyncHelper}. 476 * @param photoIcon picture which should be used for the notification. Can be null. This is 477 * the most suitable for {@link android.app.Notification.Builder#setLargeIcon(Bitmap)}, this 478 * should be used when non-null. 479 * @param date the time when the missed call happened 480 */ 481 /* package */ void notifyMissedCall( 482 String name, String number, String type, Drawable photo, Bitmap photoIcon, long date) { 483 484 // When the user clicks this notification, we go to the call log. 485 final Intent callLogIntent = PhoneApp.createCallLogIntent(); 486 487 // Never display the missed call notification on non-voice-capable 488 // devices, even if the device does somehow manage to get an 489 // incoming call. 490 if (!PhoneApp.sVoiceCapable) { 491 if (DBG) log("notifyMissedCall: non-voice-capable device, not posting notification"); 492 return; 493 } 494 495 if (VDBG) { 496 log("notifyMissedCall(). name: " + name + ", number: " + number 497 + ", label: " + type + ", photo: " + photo + ", photoIcon: " + photoIcon 498 + ", date: " + date); 499 } 500 501 // title resource id 502 int titleResId; 503 // the text in the notification's line 1 and 2. 504 String expandedText, callName; 505 506 // increment number of missed calls. 507 mNumberMissedCalls++; 508 509 // get the name for the ticker text 510 // i.e. "Missed call from <caller name or number>" 511 if (name != null && TextUtils.isGraphic(name)) { 512 callName = name; 513 } else if (!TextUtils.isEmpty(number)){ 514 callName = number; 515 } else { 516 // use "unknown" if the caller is unidentifiable. 517 callName = mContext.getString(R.string.unknown); 518 } 519 520 // display the first line of the notification: 521 // 1 missed call: call name 522 // more than 1 missed call: <number of calls> + "missed calls" 523 if (mNumberMissedCalls == 1) { 524 titleResId = R.string.notification_missedCallTitle; 525 expandedText = callName; 526 } else { 527 titleResId = R.string.notification_missedCallsTitle; 528 expandedText = mContext.getString(R.string.notification_missedCallsMsg, 529 mNumberMissedCalls); 530 } 531 532 Notification.Builder builder = new Notification.Builder(mContext); 533 builder.setSmallIcon(android.R.drawable.stat_notify_missed_call) 534 .setTicker(mContext.getString(R.string.notification_missedCallTicker, callName)) 535 .setWhen(date) 536 .setContentTitle(mContext.getText(titleResId)) 537 .setContentText(expandedText) 538 .setContentIntent(PendingIntent.getActivity(mContext, 0, callLogIntent, 0)) 539 .setAutoCancel(true) 540 .setDeleteIntent(createClearMissedCallsIntent()); 541 542 // Simple workaround for issue 6476275; refrain having actions when the given number seems 543 // not a real one but a non-number which was embedded by methods outside (like 544 // PhoneUtils#modifyForSpecialCnapCases()). 545 // TODO: consider removing equals() checks here, and modify callers of this method instead. 546 if (mNumberMissedCalls == 1 547 && !TextUtils.isEmpty(number) 548 && !TextUtils.equals(number, mContext.getString(R.string.private_num)) 549 && !TextUtils.equals(number, mContext.getString(R.string.unknown))){ 550 if (DBG) log("Add actions with the number " + number); 551 552 builder.addAction(R.drawable.stat_sys_phone_call, 553 mContext.getString(R.string.notification_missedCall_call_back), 554 PhoneApp.getCallBackPendingIntent(mContext, number)); 555 556 builder.addAction(R.drawable.ic_text_holo_dark, 557 mContext.getString(R.string.notification_missedCall_message), 558 PhoneApp.getSendSmsFromNotificationPendingIntent(mContext, number)); 559 560 if (photoIcon != null) { 561 builder.setLargeIcon(photoIcon); 562 } else if (photo instanceof BitmapDrawable) { 563 builder.setLargeIcon(((BitmapDrawable) photo).getBitmap()); 564 } 565 } else { 566 if (DBG) { 567 log("Suppress actions. number: " + number + ", missedCalls: " + mNumberMissedCalls); 568 } 569 } 570 571 Notification notification = builder.getNotification(); 572 configureLedNotification(notification); 573 mNotificationManager.notify(MISSED_CALL_NOTIFICATION, notification); 574 } 575 576 /** Returns an intent to be invoked when the missed call notification is cleared. */ 577 private PendingIntent createClearMissedCallsIntent() { 578 Intent intent = new Intent(mContext, ClearMissedCallsService.class); 579 intent.setAction(ClearMissedCallsService.ACTION_CLEAR_MISSED_CALLS); 580 return PendingIntent.getService(mContext, 0, intent, 0); 581 } 582 583 /** 584 * Cancels the "missed call" notification. 585 * 586 * @see ITelephony.cancelMissedCallsNotification() 587 */ 588 void cancelMissedCallNotification() { 589 // reset the number of missed calls to 0. 590 mNumberMissedCalls = 0; 591 mNotificationManager.cancel(MISSED_CALL_NOTIFICATION); 592 } 593 594 private void notifySpeakerphone() { 595 if (!mShowingSpeakerphoneIcon) { 596 mStatusBarManager.setIcon("speakerphone", android.R.drawable.stat_sys_speakerphone, 0, 597 mContext.getString(R.string.accessibility_speakerphone_enabled)); 598 mShowingSpeakerphoneIcon = true; 599 } 600 } 601 602 private void cancelSpeakerphone() { 603 if (mShowingSpeakerphoneIcon) { 604 mStatusBarManager.removeIcon("speakerphone"); 605 mShowingSpeakerphoneIcon = false; 606 } 607 } 608 609 /** 610 * Shows or hides the "speakerphone" notification in the status bar, 611 * based on the actual current state of the speaker. 612 * 613 * If you already know the current speaker state (e.g. if you just 614 * called AudioManager.setSpeakerphoneOn() yourself) then you should 615 * directly call {@link #updateSpeakerNotification(boolean)} instead. 616 * 617 * (But note that the status bar icon is *never* shown while the in-call UI 618 * is active; it only appears if you bail out to some other activity.) 619 */ 620 private void updateSpeakerNotification() { 621 AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); 622 boolean showNotification = 623 (mPhone.getState() == Phone.State.OFFHOOK) && audioManager.isSpeakerphoneOn(); 624 625 if (DBG) log(showNotification 626 ? "updateSpeakerNotification: speaker ON" 627 : "updateSpeakerNotification: speaker OFF (or not offhook)"); 628 629 updateSpeakerNotification(showNotification); 630 } 631 632 /** 633 * Shows or hides the "speakerphone" notification in the status bar. 634 * 635 * @param showNotification if true, call notifySpeakerphone(); 636 * if false, call cancelSpeakerphone(). 637 * 638 * Use {@link updateSpeakerNotification()} to update the status bar 639 * based on the actual current state of the speaker. 640 * 641 * (But note that the status bar icon is *never* shown while the in-call UI 642 * is active; it only appears if you bail out to some other activity.) 643 */ 644 public void updateSpeakerNotification(boolean showNotification) { 645 if (DBG) log("updateSpeakerNotification(" + showNotification + ")..."); 646 647 // Regardless of the value of the showNotification param, suppress 648 // the status bar icon if the the InCallScreen is the foreground 649 // activity, since the in-call UI already provides an onscreen 650 // indication of the speaker state. (This reduces clutter in the 651 // status bar.) 652 if (mApp.isShowingCallScreen()) { 653 cancelSpeakerphone(); 654 return; 655 } 656 657 if (showNotification) { 658 notifySpeakerphone(); 659 } else { 660 cancelSpeakerphone(); 661 } 662 } 663 664 private void notifyMute() { 665 if (!mShowingMuteIcon) { 666 mStatusBarManager.setIcon("mute", android.R.drawable.stat_notify_call_mute, 0, 667 mContext.getString(R.string.accessibility_call_muted)); 668 mShowingMuteIcon = true; 669 } 670 } 671 672 private void cancelMute() { 673 if (mShowingMuteIcon) { 674 mStatusBarManager.removeIcon("mute"); 675 mShowingMuteIcon = false; 676 } 677 } 678 679 /** 680 * Shows or hides the "mute" notification in the status bar, 681 * based on the current mute state of the Phone. 682 * 683 * (But note that the status bar icon is *never* shown while the in-call UI 684 * is active; it only appears if you bail out to some other activity.) 685 */ 686 void updateMuteNotification() { 687 // Suppress the status bar icon if the the InCallScreen is the 688 // foreground activity, since the in-call UI already provides an 689 // onscreen indication of the mute state. (This reduces clutter 690 // in the status bar.) 691 if (mApp.isShowingCallScreen()) { 692 cancelMute(); 693 return; 694 } 695 696 if ((mCM.getState() == Phone.State.OFFHOOK) && PhoneUtils.getMute()) { 697 if (DBG) log("updateMuteNotification: MUTED"); 698 notifyMute(); 699 } else { 700 if (DBG) log("updateMuteNotification: not muted (or not offhook)"); 701 cancelMute(); 702 } 703 } 704 705 /** 706 * Updates the phone app's status bar notification based on the 707 * current telephony state, or cancels the notification if the phone 708 * is totally idle. 709 * 710 * This method will never actually launch the incoming-call UI. 711 * (Use updateNotificationAndLaunchIncomingCallUi() for that.) 712 */ 713 public void updateInCallNotification() { 714 // allowFullScreenIntent=false means *don't* allow the incoming 715 // call UI to be launched. 716 updateInCallNotification(false); 717 } 718 719 /** 720 * Updates the phone app's status bar notification *and* launches the 721 * incoming call UI in response to a new incoming call. 722 * 723 * This is just like updateInCallNotification(), with one exception: 724 * If an incoming call is ringing (or call-waiting), the notification 725 * will also include a "fullScreenIntent" that will cause the 726 * InCallScreen to be launched immediately, unless the current 727 * foreground activity is marked as "immersive". 728 * 729 * (This is the mechanism that actually brings up the incoming call UI 730 * when we receive a "new ringing connection" event from the telephony 731 * layer.) 732 * 733 * Watch out: this method should ONLY be called directly from the code 734 * path in CallNotifier that handles the "new ringing connection" 735 * event from the telephony layer. All other places that update the 736 * in-call notification (like for phone state changes) should call 737 * updateInCallNotification() instead. (This ensures that we don't 738 * end up launching the InCallScreen multiple times for a single 739 * incoming call, which could cause slow responsiveness and/or visible 740 * glitches.) 741 * 742 * Also note that this method is safe to call even if the phone isn't 743 * actually ringing (or, more likely, if an incoming call *was* 744 * ringing briefly but then disconnected). In that case, we'll simply 745 * update or cancel the in-call notification based on the current 746 * phone state. 747 * 748 * @see #updateInCallNotification(boolean) 749 */ 750 public void updateNotificationAndLaunchIncomingCallUi() { 751 // Set allowFullScreenIntent=true to indicate that we *should* 752 // launch the incoming call UI if necessary. 753 updateInCallNotification(true); 754 } 755 756 /** 757 * Helper method for updateInCallNotification() and 758 * updateNotificationAndLaunchIncomingCallUi(): Update the phone app's 759 * status bar notification based on the current telephony state, or 760 * cancels the notification if the phone is totally idle. 761 * 762 * @param allowFullScreenIntent If true, *and* an incoming call is 763 * ringing, the notification will include a "fullScreenIntent" 764 * pointing at the InCallScreen (which will cause the InCallScreen 765 * to be launched.) 766 * Watch out: This should be set to true *only* when directly 767 * handling the "new ringing connection" event from the telephony 768 * layer (see updateNotificationAndLaunchIncomingCallUi().) 769 */ 770 private void updateInCallNotification(boolean allowFullScreenIntent) { 771 int resId; 772 if (DBG) log("updateInCallNotification(allowFullScreenIntent = " 773 + allowFullScreenIntent + ")..."); 774 775 // Never display the "ongoing call" notification on 776 // non-voice-capable devices, even if the phone is actually 777 // offhook (like during a non-interactive OTASP call.) 778 if (!PhoneApp.sVoiceCapable) { 779 if (DBG) log("- non-voice-capable device; suppressing notification."); 780 return; 781 } 782 783 // If the phone is idle, completely clean up all call-related 784 // notifications. 785 if (mCM.getState() == Phone.State.IDLE) { 786 cancelInCall(); 787 cancelMute(); 788 cancelSpeakerphone(); 789 return; 790 } 791 792 final boolean hasRingingCall = mCM.hasActiveRingingCall(); 793 final boolean hasActiveCall = mCM.hasActiveFgCall(); 794 final boolean hasHoldingCall = mCM.hasActiveBgCall(); 795 if (DBG) { 796 log(" - hasRingingCall = " + hasRingingCall); 797 log(" - hasActiveCall = " + hasActiveCall); 798 log(" - hasHoldingCall = " + hasHoldingCall); 799 } 800 801 // Suppress the in-call notification if the InCallScreen is the 802 // foreground activity, since it's already obvious that you're on a 803 // call. (The status bar icon is needed only if you navigate *away* 804 // from the in-call UI.) 805 boolean suppressNotification = mApp.isShowingCallScreen(); 806 // if (DBG) log("- suppressNotification: initial value: " + suppressNotification); 807 808 // ...except for a couple of cases where we *never* suppress the 809 // notification: 810 // 811 // - If there's an incoming ringing call: always show the 812 // notification, since the in-call notification is what actually 813 // launches the incoming call UI in the first place (see 814 // notification.fullScreenIntent below.) This makes sure that we'll 815 // correctly handle the case where a new incoming call comes in but 816 // the InCallScreen is already in the foreground. 817 if (hasRingingCall) suppressNotification = false; 818 819 // - If "voice privacy" mode is active: always show the notification, 820 // since that's the only "voice privacy" indication we have. 821 boolean enhancedVoicePrivacy = mApp.notifier.getVoicePrivacyState(); 822 // if (DBG) log("updateInCallNotification: enhancedVoicePrivacy = " + enhancedVoicePrivacy); 823 if (enhancedVoicePrivacy) suppressNotification = false; 824 825 if (suppressNotification) { 826 if (DBG) log("- suppressNotification = true; reducing clutter in status bar..."); 827 cancelInCall(); 828 // Suppress the mute and speaker status bar icons too 829 // (also to reduce clutter in the status bar.) 830 cancelSpeakerphone(); 831 cancelMute(); 832 return; 833 } 834 835 // Display the appropriate icon in the status bar, 836 // based on the current phone and/or bluetooth state. 837 838 if (hasRingingCall) { 839 // There's an incoming ringing call. 840 resId = R.drawable.stat_sys_phone_call; 841 } else if (!hasActiveCall && hasHoldingCall) { 842 // There's only one call, and it's on hold. 843 if (enhancedVoicePrivacy) { 844 resId = R.drawable.stat_sys_vp_phone_call_on_hold; 845 } else { 846 resId = R.drawable.stat_sys_phone_call_on_hold; 847 } 848 } else { 849 if (enhancedVoicePrivacy) { 850 resId = R.drawable.stat_sys_vp_phone_call; 851 } else { 852 resId = R.drawable.stat_sys_phone_call; 853 } 854 } 855 856 // Note we can't just bail out now if (resId == mInCallResId), 857 // since even if the status icon hasn't changed, some *other* 858 // notification-related info may be different from the last time 859 // we were here (like the caller-id info of the foreground call, 860 // if the user swapped calls...) 861 862 if (DBG) log("- Updating status bar icon: resId = " + resId); 863 mInCallResId = resId; 864 865 // Even if both lines are in use, we only show a single item in 866 // the expanded Notifications UI. It's labeled "Ongoing call" 867 // (or "On hold" if there's only one call, and it's on hold.) 868 // Also, we don't have room to display caller-id info from two 869 // different calls. So if both lines are in use, display info 870 // from the foreground call. And if there's a ringing call, 871 // display that regardless of the state of the other calls. 872 873 Call currentCall; 874 if (hasRingingCall) { 875 currentCall = mCM.getFirstActiveRingingCall(); 876 } else if (hasActiveCall) { 877 currentCall = mCM.getActiveFgCall(); 878 } else { 879 currentCall = mCM.getFirstActiveBgCall(); 880 } 881 Connection currentConn = currentCall.getEarliestConnection(); 882 883 final Notification.Builder builder = new Notification.Builder(mContext); 884 builder.setSmallIcon(mInCallResId).setOngoing(true); 885 886 // PendingIntent that can be used to launch the InCallScreen. The 887 // system fires off this intent if the user pulls down the windowshade 888 // and clicks the notification's expanded view. It's also used to 889 // launch the InCallScreen immediately when when there's an incoming 890 // call (see the "fullScreenIntent" field below). 891 PendingIntent inCallPendingIntent = 892 PendingIntent.getActivity(mContext, 0, 893 PhoneApp.createInCallIntent(), 0); 894 builder.setContentIntent(inCallPendingIntent); 895 896 // Update icon on the left of the notification. 897 // - If it is directly available from CallerInfo, we'll just use that. 898 // - If it is not, use the same icon as in the status bar. 899 CallerInfo callerInfo = null; 900 if (currentConn != null) { 901 Object o = currentConn.getUserData(); 902 if (o instanceof CallerInfo) { 903 callerInfo = (CallerInfo) o; 904 } else if (o instanceof PhoneUtils.CallerInfoToken) { 905 callerInfo = ((PhoneUtils.CallerInfoToken) o).currentInfo; 906 } else { 907 Log.w(LOG_TAG, "CallerInfo isn't available while Call object is available."); 908 } 909 } 910 boolean largeIconWasSet = false; 911 if (callerInfo != null) { 912 // In most cases, the user will see the notification after CallerInfo is already 913 // available, so photo will be available from this block. 914 if (callerInfo.isCachedPhotoCurrent) { 915 // .. and in that case CallerInfo's cachedPhotoIcon should also be available. 916 // If it happens not, then try using cachedPhoto, assuming Drawable coming from 917 // ContactProvider will be BitmapDrawable. 918 if (callerInfo.cachedPhotoIcon != null) { 919 builder.setLargeIcon(callerInfo.cachedPhotoIcon); 920 largeIconWasSet = true; 921 } else if (callerInfo.cachedPhoto instanceof BitmapDrawable) { 922 if (DBG) log("- BitmapDrawable found for large icon"); 923 Bitmap bitmap = ((BitmapDrawable) callerInfo.cachedPhoto).getBitmap(); 924 builder.setLargeIcon(bitmap); 925 largeIconWasSet = true; 926 } else { 927 if (DBG) { 928 log("- Failed to fetch icon from CallerInfo's cached photo." 929 + " (cachedPhotoIcon: " + callerInfo.cachedPhotoIcon 930 + ", cachedPhoto: " + callerInfo.cachedPhoto + ")." 931 + " Ignore it."); 932 } 933 } 934 } 935 936 if (!largeIconWasSet && callerInfo.photoResource > 0) { 937 if (DBG) { 938 log("- BitmapDrawable nor person Id not found for large icon." 939 + " Use photoResource: " + callerInfo.photoResource); 940 } 941 Drawable drawable = 942 mContext.getResources().getDrawable(callerInfo.photoResource); 943 if (drawable instanceof BitmapDrawable) { 944 Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap(); 945 builder.setLargeIcon(bitmap); 946 largeIconWasSet = true; 947 } else { 948 if (DBG) { 949 log("- PhotoResource was found but it didn't return BitmapDrawable." 950 + " Ignore it"); 951 } 952 } 953 } 954 } else { 955 if (DBG) log("- CallerInfo not found. Use the same icon as in the status bar."); 956 } 957 958 // Failed to fetch Bitmap. 959 if (!largeIconWasSet && DBG) { 960 log("- No useful Bitmap was found for the photo." 961 + " Use the same icon as in the status bar."); 962 } 963 964 // If the connection is valid, then build what we need for the 965 // content text of notification, and start the chronometer. 966 // Otherwise, don't bother and just stick with content title. 967 if (currentConn != null) { 968 if (DBG) log("- Updating context text and chronometer."); 969 if (hasRingingCall) { 970 // Incoming call is ringing. 971 builder.setContentText(mContext.getString(R.string.notification_incoming_call)); 972 builder.setUsesChronometer(false); 973 } else if (hasHoldingCall && !hasActiveCall) { 974 // Only one call, and it's on hold. 975 builder.setContentText(mContext.getString(R.string.notification_on_hold)); 976 builder.setUsesChronometer(false); 977 } else { 978 // We show the elapsed time of the current call using Chronometer. 979 builder.setUsesChronometer(true); 980 981 // Determine the "start time" of the current connection. 982 // We can't use currentConn.getConnectTime(), because (1) that's 983 // in the currentTimeMillis() time base, and (2) it's zero when 984 // the phone first goes off hook, since the getConnectTime counter 985 // doesn't start until the DIALING -> ACTIVE transition. 986 // Instead we start with the current connection's duration, 987 // and translate that into the elapsedRealtime() timebase. 988 long callDurationMsec = currentConn.getDurationMillis(); 989 builder.setWhen(System.currentTimeMillis() - callDurationMsec); 990 builder.setContentText(mContext.getString(R.string.notification_ongoing_call)); 991 } 992 } else if (DBG) { 993 Log.w(LOG_TAG, "updateInCallNotification: null connection, can't set exp view line 1."); 994 } 995 996 // display conference call string if this call is a conference 997 // call, otherwise display the connection information. 998 999 // Line 2 of the expanded view (smaller text). This is usually a 1000 // contact name or phone number. 1001 String expandedViewLine2 = ""; 1002 // TODO: it may not make sense for every point to make separate 1003 // checks for isConferenceCall, so we need to think about 1004 // possibly including this in startGetCallerInfo or some other 1005 // common point. 1006 if (PhoneUtils.isConferenceCall(currentCall)) { 1007 // if this is a conference call, just use that as the caller name. 1008 expandedViewLine2 = mContext.getString(R.string.card_title_conf_call); 1009 } else { 1010 // If necessary, start asynchronous query to do the caller-id lookup. 1011 PhoneUtils.CallerInfoToken cit = 1012 PhoneUtils.startGetCallerInfo(mContext, currentCall, this, this); 1013 expandedViewLine2 = PhoneUtils.getCompactNameFromCallerInfo(cit.currentInfo, mContext); 1014 // Note: For an incoming call, the very first time we get here we 1015 // won't have a contact name yet, since we only just started the 1016 // caller-id query. So expandedViewLine2 will start off as a raw 1017 // phone number, but we'll update it very quickly when the query 1018 // completes (see onQueryComplete() below.) 1019 } 1020 1021 if (DBG) log("- Updating expanded view: line 2 '" + /*expandedViewLine2*/ "xxxxxxx" + "'"); 1022 builder.setContentTitle(expandedViewLine2); 1023 1024 // TODO: We also need to *update* this notification in some cases, 1025 // like when a call ends on one line but the other is still in use 1026 // (ie. make sure the caller info here corresponds to the active 1027 // line), and maybe even when the user swaps calls (ie. if we only 1028 // show info here for the "current active call".) 1029 1030 // Activate a couple of special Notification features if an 1031 // incoming call is ringing: 1032 if (hasRingingCall) { 1033 if (DBG) log("- Using hi-pri notification for ringing call!"); 1034 1035 // This is a high-priority event that should be shown even if the 1036 // status bar is hidden or if an immersive activity is running. 1037 builder.setPriority(Notification.PRIORITY_HIGH); 1038 1039 // If an immersive activity is running, we have room for a single 1040 // line of text in the small notification popup window. 1041 // We use expandedViewLine2 for this (i.e. the name or number of 1042 // the incoming caller), since that's more relevant than 1043 // expandedViewLine1 (which is something generic like "Incoming 1044 // call".) 1045 builder.setTicker(expandedViewLine2); 1046 1047 if (allowFullScreenIntent) { 1048 // Ok, we actually want to launch the incoming call 1049 // UI at this point (in addition to simply posting a notification 1050 // to the status bar). Setting fullScreenIntent will cause 1051 // the InCallScreen to be launched immediately *unless* the 1052 // current foreground activity is marked as "immersive". 1053 if (DBG) log("- Setting fullScreenIntent: " + inCallPendingIntent); 1054 builder.setFullScreenIntent(inCallPendingIntent, true); 1055 1056 // Ugly hack alert: 1057 // 1058 // The NotificationManager has the (undocumented) behavior 1059 // that it will *ignore* the fullScreenIntent field if you 1060 // post a new Notification that matches the ID of one that's 1061 // already active. Unfortunately this is exactly what happens 1062 // when you get an incoming call-waiting call: the 1063 // "ongoing call" notification is already visible, so the 1064 // InCallScreen won't get launched in this case! 1065 // (The result: if you bail out of the in-call UI while on a 1066 // call and then get a call-waiting call, the incoming call UI 1067 // won't come up automatically.) 1068 // 1069 // The workaround is to just notice this exact case (this is a 1070 // call-waiting call *and* the InCallScreen is not in the 1071 // foreground) and manually cancel the in-call notification 1072 // before (re)posting it. 1073 // 1074 // TODO: there should be a cleaner way of avoiding this 1075 // problem (see discussion in bug 3184149.) 1076 Call ringingCall = mCM.getFirstActiveRingingCall(); 1077 if ((ringingCall.getState() == Call.State.WAITING) && !mApp.isShowingCallScreen()) { 1078 Log.i(LOG_TAG, "updateInCallNotification: call-waiting! force relaunch..."); 1079 // Cancel the IN_CALL_NOTIFICATION immediately before 1080 // (re)posting it; this seems to force the 1081 // NotificationManager to launch the fullScreenIntent. 1082 mNotificationManager.cancel(IN_CALL_NOTIFICATION); 1083 } 1084 } 1085 } else { // not ringing call 1086 // Make the notification prioritized over the other normal notifications. 1087 builder.setPriority(Notification.PRIORITY_HIGH); 1088 1089 // TODO: use "if (DBG)" for this comment. 1090 log("Will show \"hang-up\" action in the ongoing active call Notification"); 1091 // TODO: use better asset. 1092 builder.addAction(R.drawable.stat_sys_phone_call_end, 1093 mContext.getText(R.string.notification_action_end_call), 1094 PhoneApp.createHangUpOngoingCallPendingIntent(mContext)); 1095 } 1096 1097 Notification notification = builder.getNotification(); 1098 if (DBG) log("Notifying IN_CALL_NOTIFICATION: " + notification); 1099 mNotificationManager.notify(IN_CALL_NOTIFICATION, notification); 1100 1101 // Finally, refresh the mute and speakerphone notifications (since 1102 // some phone state changes can indirectly affect the mute and/or 1103 // speaker state). 1104 updateSpeakerNotification(); 1105 updateMuteNotification(); 1106 } 1107 1108 /** 1109 * Implemented for CallerInfoAsyncQuery.OnQueryCompleteListener interface. 1110 * refreshes the contentView when called. 1111 */ 1112 @Override 1113 public void onQueryComplete(int token, Object cookie, CallerInfo ci){ 1114 if (DBG) log("CallerInfo query complete (for NotificationMgr), " 1115 + "updating in-call notification.."); 1116 if (DBG) log("- cookie: " + cookie); 1117 if (DBG) log("- ci: " + ci); 1118 1119 if (cookie == this) { 1120 // Ok, this is the caller-id query we fired off in 1121 // updateInCallNotification(), presumably when an incoming call 1122 // first appeared. If the caller-id info matched any contacts, 1123 // compactName should now be a real person name rather than a raw 1124 // phone number: 1125 if (DBG) log("- compactName is now: " 1126 + PhoneUtils.getCompactNameFromCallerInfo(ci, mContext)); 1127 1128 // Now that our CallerInfo object has been fully filled-in, 1129 // refresh the in-call notification. 1130 if (DBG) log("- updating notification after query complete..."); 1131 updateInCallNotification(); 1132 } else { 1133 Log.w(LOG_TAG, "onQueryComplete: caller-id query from unknown source! " 1134 + "cookie = " + cookie); 1135 } 1136 } 1137 1138 /** 1139 * Take down the in-call notification. 1140 * @see updateInCallNotification() 1141 */ 1142 private void cancelInCall() { 1143 if (DBG) log("cancelInCall()..."); 1144 mNotificationManager.cancel(IN_CALL_NOTIFICATION); 1145 mInCallResId = 0; 1146 } 1147 1148 /** 1149 * Completely take down the in-call notification *and* the mute/speaker 1150 * notifications as well, to indicate that the phone is now idle. 1151 */ 1152 /* package */ void cancelCallInProgressNotifications() { 1153 if (DBG) log("cancelCallInProgressNotifications()..."); 1154 if (mInCallResId == 0) { 1155 return; 1156 } 1157 1158 if (DBG) log("cancelCallInProgressNotifications: " + mInCallResId); 1159 cancelInCall(); 1160 cancelMute(); 1161 cancelSpeakerphone(); 1162 } 1163 1164 /** 1165 * Updates the message waiting indicator (voicemail) notification. 1166 * 1167 * @param visible true if there are messages waiting 1168 */ 1169 /* package */ void updateMwi(boolean visible) { 1170 if (DBG) log("updateMwi(): " + visible); 1171 1172 if (visible) { 1173 int resId = android.R.drawable.stat_notify_voicemail; 1174 1175 // This Notification can get a lot fancier once we have more 1176 // information about the current voicemail messages. 1177 // (For example, the current voicemail system can't tell 1178 // us the caller-id or timestamp of a message, or tell us the 1179 // message count.) 1180 1181 // But for now, the UI is ultra-simple: if the MWI indication 1182 // is supposed to be visible, just show a single generic 1183 // notification. 1184 1185 String notificationTitle = mContext.getString(R.string.notification_voicemail_title); 1186 String vmNumber = mPhone.getVoiceMailNumber(); 1187 if (DBG) log("- got vm number: '" + vmNumber + "'"); 1188 1189 // Watch out: vmNumber may be null, for two possible reasons: 1190 // 1191 // (1) This phone really has no voicemail number 1192 // 1193 // (2) This phone *does* have a voicemail number, but 1194 // the SIM isn't ready yet. 1195 // 1196 // Case (2) *does* happen in practice if you have voicemail 1197 // messages when the device first boots: we get an MWI 1198 // notification as soon as we register on the network, but the 1199 // SIM hasn't finished loading yet. 1200 // 1201 // So handle case (2) by retrying the lookup after a short 1202 // delay. 1203 1204 if ((vmNumber == null) && !mPhone.getIccRecordsLoaded()) { 1205 if (DBG) log("- Null vm number: SIM records not loaded (yet)..."); 1206 1207 // TODO: rather than retrying after an arbitrary delay, it 1208 // would be cleaner to instead just wait for a 1209 // SIM_RECORDS_LOADED notification. 1210 // (Unfortunately right now there's no convenient way to 1211 // get that notification in phone app code. We'd first 1212 // want to add a call like registerForSimRecordsLoaded() 1213 // to Phone.java and GSMPhone.java, and *then* we could 1214 // listen for that in the CallNotifier class.) 1215 1216 // Limit the number of retries (in case the SIM is broken 1217 // or missing and can *never* load successfully.) 1218 if (mVmNumberRetriesRemaining-- > 0) { 1219 if (DBG) log(" - Retrying in " + VM_NUMBER_RETRY_DELAY_MILLIS + " msec..."); 1220 mApp.notifier.sendMwiChangedDelayed(VM_NUMBER_RETRY_DELAY_MILLIS); 1221 return; 1222 } else { 1223 Log.w(LOG_TAG, "NotificationMgr.updateMwi: getVoiceMailNumber() failed after " 1224 + MAX_VM_NUMBER_RETRIES + " retries; giving up."); 1225 // ...and continue with vmNumber==null, just as if the 1226 // phone had no VM number set up in the first place. 1227 } 1228 } 1229 1230 if (TelephonyCapabilities.supportsVoiceMessageCount(mPhone)) { 1231 int vmCount = mPhone.getVoiceMessageCount(); 1232 String titleFormat = mContext.getString(R.string.notification_voicemail_title_count); 1233 notificationTitle = String.format(titleFormat, vmCount); 1234 } 1235 1236 String notificationText; 1237 if (TextUtils.isEmpty(vmNumber)) { 1238 notificationText = mContext.getString( 1239 R.string.notification_voicemail_no_vm_number); 1240 } else { 1241 notificationText = String.format( 1242 mContext.getString(R.string.notification_voicemail_text_format), 1243 PhoneNumberUtils.formatNumber(vmNumber)); 1244 } 1245 1246 Intent intent = new Intent(Intent.ACTION_CALL, 1247 Uri.fromParts(Constants.SCHEME_VOICEMAIL, "", null)); 1248 PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0); 1249 1250 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); 1251 Uri ringtoneUri; 1252 String uriString = prefs.getString( 1253 CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_RINGTONE_KEY, null); 1254 if (!TextUtils.isEmpty(uriString)) { 1255 ringtoneUri = Uri.parse(uriString); 1256 } else { 1257 ringtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI; 1258 } 1259 1260 Notification.Builder builder = new Notification.Builder(mContext); 1261 builder.setSmallIcon(resId) 1262 .setWhen(System.currentTimeMillis()) 1263 .setContentTitle(notificationTitle) 1264 .setContentText(notificationText) 1265 .setContentIntent(pendingIntent) 1266 .setSound(ringtoneUri); 1267 Notification notification = builder.getNotification(); 1268 1269 String vibrateWhen = prefs.getString( 1270 CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_VIBRATE_WHEN_KEY, "never"); 1271 boolean vibrateAlways = vibrateWhen.equals("always"); 1272 boolean vibrateSilent = vibrateWhen.equals("silent"); 1273 AudioManager audioManager = 1274 (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); 1275 boolean nowSilent = audioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE; 1276 if (vibrateAlways || (vibrateSilent && nowSilent)) { 1277 notification.defaults |= Notification.DEFAULT_VIBRATE; 1278 } 1279 1280 notification.flags |= Notification.FLAG_NO_CLEAR; 1281 configureLedNotification(notification); 1282 mNotificationManager.notify(VOICEMAIL_NOTIFICATION, notification); 1283 } else { 1284 mNotificationManager.cancel(VOICEMAIL_NOTIFICATION); 1285 } 1286 } 1287 1288 /** 1289 * Updates the message call forwarding indicator notification. 1290 * 1291 * @param visible true if there are messages waiting 1292 */ 1293 /* package */ void updateCfi(boolean visible) { 1294 if (DBG) log("updateCfi(): " + visible); 1295 if (visible) { 1296 // If Unconditional Call Forwarding (forward all calls) for VOICE 1297 // is enabled, just show a notification. We'll default to expanded 1298 // view for now, so the there is less confusion about the icon. If 1299 // it is deemed too weird to have CF indications as expanded views, 1300 // then we'll flip the flag back. 1301 1302 // TODO: We may want to take a look to see if the notification can 1303 // display the target to forward calls to. This will require some 1304 // effort though, since there are multiple layers of messages that 1305 // will need to propagate that information. 1306 1307 Notification notification; 1308 final boolean showExpandedNotification = true; 1309 if (showExpandedNotification) { 1310 Intent intent = new Intent(Intent.ACTION_MAIN); 1311 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1312 intent.setClassName("com.android.phone", 1313 "com.android.phone.CallFeaturesSetting"); 1314 1315 notification = new Notification( 1316 R.drawable.stat_sys_phone_call_forward, // icon 1317 null, // tickerText 1318 0); // The "timestamp" of this notification is meaningless; 1319 // we only care about whether CFI is currently on or not. 1320 notification.setLatestEventInfo( 1321 mContext, // context 1322 mContext.getString(R.string.labelCF), // expandedTitle 1323 mContext.getString(R.string.sum_cfu_enabled_indicator), // expandedText 1324 PendingIntent.getActivity(mContext, 0, intent, 0)); // contentIntent 1325 } else { 1326 notification = new Notification( 1327 R.drawable.stat_sys_phone_call_forward, // icon 1328 null, // tickerText 1329 System.currentTimeMillis() // when 1330 ); 1331 } 1332 1333 notification.flags |= Notification.FLAG_ONGOING_EVENT; // also implies FLAG_NO_CLEAR 1334 1335 mNotificationManager.notify( 1336 CALL_FORWARD_NOTIFICATION, 1337 notification); 1338 } else { 1339 mNotificationManager.cancel(CALL_FORWARD_NOTIFICATION); 1340 } 1341 } 1342 1343 /** 1344 * Shows the "data disconnected due to roaming" notification, which 1345 * appears when you lose data connectivity because you're roaming and 1346 * you have the "data roaming" feature turned off. 1347 */ 1348 /* package */ void showDataDisconnectedRoaming() { 1349 if (DBG) log("showDataDisconnectedRoaming()..."); 1350 1351 // "Mobile network settings" screen / dialog 1352 Intent intent = new Intent(mContext, 1353 com.android.phone.MobileNetworkSettings.class); 1354 1355 Notification notification = new Notification( 1356 android.R.drawable.stat_sys_warning, // icon 1357 null, // tickerText 1358 System.currentTimeMillis()); 1359 notification.setLatestEventInfo( 1360 mContext, // Context 1361 mContext.getString(R.string.roaming), // expandedTitle 1362 mContext.getString(R.string.roaming_reenable_message), // expandedText 1363 PendingIntent.getActivity(mContext, 0, intent, 0)); // contentIntent 1364 1365 mNotificationManager.notify( 1366 DATA_DISCONNECTED_ROAMING_NOTIFICATION, 1367 notification); 1368 } 1369 1370 /** 1371 * Turns off the "data disconnected due to roaming" notification. 1372 */ 1373 /* package */ void hideDataDisconnectedRoaming() { 1374 if (DBG) log("hideDataDisconnectedRoaming()..."); 1375 mNotificationManager.cancel(DATA_DISCONNECTED_ROAMING_NOTIFICATION); 1376 } 1377 1378 /** 1379 * Display the network selection "no service" notification 1380 * @param operator is the numeric operator number 1381 */ 1382 private void showNetworkSelection(String operator) { 1383 if (DBG) log("showNetworkSelection(" + operator + ")..."); 1384 1385 String titleText = mContext.getString( 1386 R.string.notification_network_selection_title); 1387 String expandedText = mContext.getString( 1388 R.string.notification_network_selection_text, operator); 1389 1390 Notification notification = new Notification(); 1391 notification.icon = android.R.drawable.stat_sys_warning; 1392 notification.when = 0; 1393 notification.flags = Notification.FLAG_ONGOING_EVENT; 1394 notification.tickerText = null; 1395 1396 // create the target network operators settings intent 1397 Intent intent = new Intent(Intent.ACTION_MAIN); 1398 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | 1399 Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 1400 // Use NetworkSetting to handle the selection intent 1401 intent.setComponent(new ComponentName("com.android.phone", 1402 "com.android.phone.NetworkSetting")); 1403 PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0); 1404 1405 notification.setLatestEventInfo(mContext, titleText, expandedText, pi); 1406 1407 mNotificationManager.notify(SELECTED_OPERATOR_FAIL_NOTIFICATION, notification); 1408 } 1409 1410 /** 1411 * Turn off the network selection "no service" notification 1412 */ 1413 private void cancelNetworkSelection() { 1414 if (DBG) log("cancelNetworkSelection()..."); 1415 mNotificationManager.cancel(SELECTED_OPERATOR_FAIL_NOTIFICATION); 1416 } 1417 1418 /** 1419 * Update notification about no service of user selected operator 1420 * 1421 * @param serviceState Phone service state 1422 */ 1423 void updateNetworkSelection(int serviceState) { 1424 if (TelephonyCapabilities.supportsNetworkSelection(mPhone)) { 1425 // get the shared preference of network_selection. 1426 // empty is auto mode, otherwise it is the operator alpha name 1427 // in case there is no operator name, check the operator numeric 1428 SharedPreferences sp = 1429 PreferenceManager.getDefaultSharedPreferences(mContext); 1430 String networkSelection = 1431 sp.getString(PhoneBase.NETWORK_SELECTION_NAME_KEY, ""); 1432 if (TextUtils.isEmpty(networkSelection)) { 1433 networkSelection = 1434 sp.getString(PhoneBase.NETWORK_SELECTION_KEY, ""); 1435 } 1436 1437 if (DBG) log("updateNetworkSelection()..." + "state = " + 1438 serviceState + " new network " + networkSelection); 1439 1440 if (serviceState == ServiceState.STATE_OUT_OF_SERVICE 1441 && !TextUtils.isEmpty(networkSelection)) { 1442 if (!mSelectedUnavailableNotify) { 1443 showNetworkSelection(networkSelection); 1444 mSelectedUnavailableNotify = true; 1445 } 1446 } else { 1447 if (mSelectedUnavailableNotify) { 1448 cancelNetworkSelection(); 1449 mSelectedUnavailableNotify = false; 1450 } 1451 } 1452 } 1453 } 1454 1455 /* package */ void postTransientNotification(int notifyId, CharSequence msg) { 1456 if (mToast != null) { 1457 mToast.cancel(); 1458 } 1459 1460 mToast = Toast.makeText(mContext, msg, Toast.LENGTH_LONG); 1461 mToast.show(); 1462 } 1463 1464 private void log(String msg) { 1465 Log.d(LOG_TAG, msg); 1466 } 1467 } 1468