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