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.BidiFormatter; 46 import android.text.TextDirectionHeuristics; 47 import android.text.TextUtils; 48 import android.util.Log; 49 import android.widget.Toast; 50 51 import com.android.internal.telephony.Call; 52 import com.android.internal.telephony.CallManager; 53 import com.android.internal.telephony.CallerInfo; 54 import com.android.internal.telephony.CallerInfoAsyncQuery; 55 import com.android.internal.telephony.Connection; 56 import com.android.internal.telephony.Phone; 57 import com.android.internal.telephony.PhoneBase; 58 import com.android.internal.telephony.PhoneConstants; 59 import com.android.internal.telephony.TelephonyCapabilities; 60 61 /** 62 * NotificationManager-related utility code for the Phone app. 63 * 64 * This is a singleton object which acts as the interface to the 65 * framework's NotificationManager, and is used to display status bar 66 * icons and control other status bar-related behavior. 67 * 68 * @see PhoneGlobals.notificationMgr 69 */ 70 public class NotificationMgr { 71 private static final String LOG_TAG = "NotificationMgr"; 72 private static final boolean DBG = 73 (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1); 74 // Do not check in with VDBG = true, since that may write PII to the system log. 75 private static final boolean VDBG = false; 76 77 private static final String[] CALL_LOG_PROJECTION = new String[] { 78 Calls._ID, 79 Calls.NUMBER, 80 Calls.NUMBER_PRESENTATION, 81 Calls.DATE, 82 Calls.DURATION, 83 Calls.TYPE, 84 }; 85 86 // notification types 87 static final int MISSED_CALL_NOTIFICATION = 1; 88 static final int IN_CALL_NOTIFICATION = 2; 89 static final int MMI_NOTIFICATION = 3; 90 static final int NETWORK_SELECTION_NOTIFICATION = 4; 91 static final int VOICEMAIL_NOTIFICATION = 5; 92 static final int CALL_FORWARD_NOTIFICATION = 6; 93 static final int DATA_DISCONNECTED_ROAMING_NOTIFICATION = 7; 94 static final int SELECTED_OPERATOR_FAIL_NOTIFICATION = 8; 95 96 /** The singleton NotificationMgr instance. */ 97 private static NotificationMgr sInstance; 98 99 private PhoneGlobals mApp; 100 private Phone mPhone; 101 private CallManager mCM; 102 103 private Context mContext; 104 private NotificationManager mNotificationManager; 105 private StatusBarManager mStatusBarManager; 106 private Toast mToast; 107 private boolean mShowingSpeakerphoneIcon; 108 private boolean mShowingMuteIcon; 109 110 public StatusBarHelper statusBarHelper; 111 112 // used to track the missed call counter, default to 0. 113 private int mNumberMissedCalls = 0; 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(PhoneGlobals 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 mPhone = app.phone; // TODO: better style to use mCM.getDefaultPhone() everywhere instead 140 mCM = app.mCM; 141 statusBarHelper = new StatusBarHelper(); 142 } 143 144 /** 145 * Initialize the singleton NotificationMgr instance. 146 * 147 * This is only done once, at startup, from PhoneApp.onCreate(). 148 * From then on, the NotificationMgr instance is available via the 149 * PhoneApp's public "notificationMgr" field, which is why there's no 150 * getInstance() method here. 151 */ 152 /* package */ static NotificationMgr init(PhoneGlobals app) { 153 synchronized (NotificationMgr.class) { 154 if (sInstance == null) { 155 sInstance = new NotificationMgr(app); 156 // Update the notifications that need to be touched at startup. 157 sInstance.updateNotificationsAtStartup(); 158 } else { 159 Log.wtf(LOG_TAG, "init() called multiple times! sInstance = " + sInstance); 160 } 161 return sInstance; 162 } 163 } 164 165 /** 166 * Helper class that's a wrapper around the framework's 167 * StatusBarManager.disable() API. 168 * 169 * This class is used to control features like: 170 * 171 * - Disabling the status bar "notification windowshade" 172 * while the in-call UI is up 173 * 174 * - Disabling notification alerts (audible or vibrating) 175 * while a phone call is active 176 * 177 * - Disabling navigation via the system bar (the "soft buttons" at 178 * the bottom of the screen on devices with no hard buttons) 179 * 180 * We control these features through a single point of control to make 181 * sure that the various StatusBarManager.disable() calls don't 182 * interfere with each other. 183 */ 184 public class StatusBarHelper { 185 // Current desired state of status bar / system bar behavior 186 private boolean mIsNotificationEnabled = true; 187 private boolean mIsExpandedViewEnabled = true; 188 private boolean mIsSystemBarNavigationEnabled = true; 189 190 private StatusBarHelper () { 191 } 192 193 /** 194 * Enables or disables auditory / vibrational alerts. 195 * 196 * (We disable these any time a voice call is active, regardless 197 * of whether or not the in-call UI is visible.) 198 */ 199 public void enableNotificationAlerts(boolean enable) { 200 if (mIsNotificationEnabled != enable) { 201 mIsNotificationEnabled = enable; 202 updateStatusBar(); 203 } 204 } 205 206 /** 207 * Enables or disables the expanded view of the status bar 208 * (i.e. the ability to pull down the "notification windowshade"). 209 * 210 * (This feature is disabled by the InCallScreen while the in-call 211 * UI is active.) 212 */ 213 public void enableExpandedView(boolean enable) { 214 if (mIsExpandedViewEnabled != enable) { 215 mIsExpandedViewEnabled = enable; 216 updateStatusBar(); 217 } 218 } 219 220 /** 221 * Enables or disables the navigation via the system bar (the 222 * "soft buttons" at the bottom of the screen) 223 * 224 * (This feature is disabled while an incoming call is ringing, 225 * because it's easy to accidentally touch the system bar while 226 * pulling the phone out of your pocket.) 227 */ 228 public void enableSystemBarNavigation(boolean enable) { 229 if (mIsSystemBarNavigationEnabled != enable) { 230 mIsSystemBarNavigationEnabled = enable; 231 updateStatusBar(); 232 } 233 } 234 235 /** 236 * Updates the status bar to reflect the current desired state. 237 */ 238 private void updateStatusBar() { 239 int state = StatusBarManager.DISABLE_NONE; 240 241 if (!mIsExpandedViewEnabled) { 242 state |= StatusBarManager.DISABLE_EXPAND; 243 } 244 if (!mIsNotificationEnabled) { 245 state |= StatusBarManager.DISABLE_NOTIFICATION_ALERTS; 246 } 247 if (!mIsSystemBarNavigationEnabled) { 248 // Disable *all* possible navigation via the system bar. 249 state |= StatusBarManager.DISABLE_HOME; 250 state |= StatusBarManager.DISABLE_RECENT; 251 state |= StatusBarManager.DISABLE_BACK; 252 state |= StatusBarManager.DISABLE_SEARCH; 253 } 254 255 if (DBG) log("updateStatusBar: state = 0x" + Integer.toHexString(state)); 256 mStatusBarManager.disable(state); 257 } 258 } 259 260 /** 261 * Makes sure phone-related notifications are up to date on a 262 * freshly-booted device. 263 */ 264 private void updateNotificationsAtStartup() { 265 if (DBG) log("updateNotificationsAtStartup()..."); 266 267 // instantiate query handler 268 mQueryHandler = new QueryHandler(mContext.getContentResolver()); 269 270 // setup query spec, look for all Missed calls that are new. 271 StringBuilder where = new StringBuilder("type="); 272 where.append(Calls.MISSED_TYPE); 273 where.append(" AND new=1"); 274 275 // start the query 276 if (DBG) log("- start call log query..."); 277 mQueryHandler.startQuery(CALL_LOG_TOKEN, null, Calls.CONTENT_URI, CALL_LOG_PROJECTION, 278 where.toString(), null, Calls.DEFAULT_SORT_ORDER); 279 280 // Depend on android.app.StatusBarManager to be set to 281 // disable(DISABLE_NONE) upon startup. This will be the 282 // case even if the phone app crashes. 283 } 284 285 /** The projection to use when querying the phones table */ 286 static final String[] PHONES_PROJECTION = new String[] { 287 PhoneLookup.NUMBER, 288 PhoneLookup.DISPLAY_NAME, 289 PhoneLookup._ID 290 }; 291 292 /** 293 * Class used to run asynchronous queries to re-populate the notifications we care about. 294 * There are really 3 steps to this: 295 * 1. Find the list of missed calls 296 * 2. For each call, run a query to retrieve the caller's name. 297 * 3. For each caller, try obtaining photo. 298 */ 299 private class QueryHandler extends AsyncQueryHandler 300 implements ContactsAsyncHelper.OnImageLoadCompleteListener { 301 302 /** 303 * Used to store relevant fields for the Missed Call 304 * notifications. 305 */ 306 private class NotificationInfo { 307 public String name; 308 public String number; 309 public int presentation; 310 /** 311 * Type of the call. {@link android.provider.CallLog.Calls#INCOMING_TYPE} 312 * {@link android.provider.CallLog.Calls#OUTGOING_TYPE}, or 313 * {@link android.provider.CallLog.Calls#MISSED_TYPE}. 314 */ 315 public String type; 316 public long date; 317 } 318 319 public QueryHandler(ContentResolver cr) { 320 super(cr); 321 } 322 323 /** 324 * Handles the query results. 325 */ 326 @Override 327 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 328 // TODO: it would be faster to use a join here, but for the purposes 329 // of this small record set, it should be ok. 330 331 // Note that CursorJoiner is not useable here because the number 332 // comparisons are not strictly equals; the comparisons happen in 333 // the SQL function PHONE_NUMBERS_EQUAL, which is not available for 334 // the CursorJoiner. 335 336 // Executing our own query is also feasible (with a join), but that 337 // will require some work (possibly destabilizing) in Contacts 338 // Provider. 339 340 // At this point, we will execute subqueries on each row just as 341 // CallLogActivity.java does. 342 switch (token) { 343 case CALL_LOG_TOKEN: 344 if (DBG) log("call log query complete."); 345 346 // initial call to retrieve the call list. 347 if (cursor != null) { 348 while (cursor.moveToNext()) { 349 // for each call in the call log list, create 350 // the notification object and query contacts 351 NotificationInfo n = getNotificationInfo (cursor); 352 353 if (DBG) log("query contacts for number: " + n.number); 354 355 mQueryHandler.startQuery(CONTACT_TOKEN, n, 356 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, n.number), 357 PHONES_PROJECTION, null, null, PhoneLookup.NUMBER); 358 } 359 360 if (DBG) log("closing call log cursor."); 361 cursor.close(); 362 } 363 break; 364 case CONTACT_TOKEN: 365 if (DBG) log("contact query complete."); 366 367 // subqueries to get the caller name. 368 if ((cursor != null) && (cookie != null)){ 369 NotificationInfo n = (NotificationInfo) cookie; 370 371 Uri personUri = null; 372 if (cursor.moveToFirst()) { 373 n.name = cursor.getString( 374 cursor.getColumnIndexOrThrow(PhoneLookup.DISPLAY_NAME)); 375 long person_id = cursor.getLong( 376 cursor.getColumnIndexOrThrow(PhoneLookup._ID)); 377 if (DBG) { 378 log("contact :" + n.name + " found for phone: " + n.number 379 + ". id : " + person_id); 380 } 381 personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, person_id); 382 } 383 384 if (personUri != null) { 385 if (DBG) { 386 log("Start obtaining picture for the missed call. Uri: " 387 + personUri); 388 } 389 // Now try to obtain a photo for this person. 390 // ContactsAsyncHelper will do that and call onImageLoadComplete() 391 // after that. 392 ContactsAsyncHelper.startObtainPhotoAsync( 393 0, mContext, personUri, this, n); 394 } else { 395 if (DBG) { 396 log("Failed to find Uri for obtaining photo." 397 + " Just send notification without it."); 398 } 399 // We couldn't find person Uri, so we're sure we cannot obtain a photo. 400 // Call notifyMissedCall() right now. 401 notifyMissedCall(n.name, n.number, n.presentation, n.type, null, null, 402 n.date); 403 } 404 405 if (DBG) log("closing contact cursor."); 406 cursor.close(); 407 } 408 break; 409 default: 410 } 411 } 412 413 @Override 414 public void onImageLoadComplete( 415 int token, Drawable photo, Bitmap photoIcon, Object cookie) { 416 if (DBG) log("Finished loading image: " + photo); 417 NotificationInfo n = (NotificationInfo) cookie; 418 notifyMissedCall(n.name, n.number, n.presentation, n.type, photo, photoIcon, n.date); 419 } 420 421 /** 422 * Factory method to generate a NotificationInfo object given a 423 * cursor from the call log table. 424 */ 425 private final NotificationInfo getNotificationInfo(Cursor cursor) { 426 NotificationInfo n = new NotificationInfo(); 427 n.name = null; 428 n.number = cursor.getString(cursor.getColumnIndexOrThrow(Calls.NUMBER)); 429 n.presentation = cursor.getInt(cursor.getColumnIndexOrThrow(Calls.NUMBER_PRESENTATION)); 430 n.type = cursor.getString(cursor.getColumnIndexOrThrow(Calls.TYPE)); 431 n.date = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DATE)); 432 433 // make sure we update the number depending upon saved values in 434 // CallLog.addCall(). If either special values for unknown or 435 // private number are detected, we need to hand off the message 436 // to the missed call notification. 437 if (n.presentation != Calls.PRESENTATION_ALLOWED) { 438 n.number = null; 439 } 440 441 if (DBG) log("NotificationInfo constructed for number: " + n.number); 442 443 return n; 444 } 445 } 446 447 /** 448 * Configures a Notification to emit the blinky green message-waiting/ 449 * missed-call signal. 450 */ 451 private static void configureLedNotification(Notification note) { 452 note.flags |= Notification.FLAG_SHOW_LIGHTS; 453 note.defaults |= Notification.DEFAULT_LIGHTS; 454 } 455 456 /** 457 * Displays a notification about a missed call. 458 * 459 * @param name the contact name. 460 * @param number the phone number. Note that this may be a non-callable String like "Unknown", 461 * or "Private Number", which possibly come from methods like 462 * {@link PhoneUtils#modifyForSpecialCnapCases(Context, CallerInfo, String, int)}. 463 * @param type the type of the call. {@link android.provider.CallLog.Calls#INCOMING_TYPE} 464 * {@link android.provider.CallLog.Calls#OUTGOING_TYPE}, or 465 * {@link android.provider.CallLog.Calls#MISSED_TYPE} 466 * @param photo picture which may be used for the notification (when photoIcon is null). 467 * This also can be null when the picture itself isn't available. If photoIcon is available 468 * it should be prioritized (because this may be too huge for notification). 469 * See also {@link ContactsAsyncHelper}. 470 * @param photoIcon picture which should be used for the notification. Can be null. This is 471 * the most suitable for {@link android.app.Notification.Builder#setLargeIcon(Bitmap)}, this 472 * should be used when non-null. 473 * @param date the time when the missed call happened 474 */ 475 /* package */ void notifyMissedCall(String name, String number, int presentation, String type, 476 Drawable photo, Bitmap photoIcon, long date) { 477 478 // When the user clicks this notification, we go to the call log. 479 final PendingIntent pendingCallLogIntent = PhoneGlobals.createPendingCallLogIntent( 480 mContext); 481 482 // Never display the missed call notification on non-voice-capable 483 // devices, even if the device does somehow manage to get an 484 // incoming call. 485 if (!PhoneGlobals.sVoiceCapable) { 486 if (DBG) log("notifyMissedCall: non-voice-capable device, not posting notification"); 487 return; 488 } 489 490 if (VDBG) { 491 log("notifyMissedCall(). name: " + name + ", number: " + number 492 + ", label: " + type + ", photo: " + photo + ", photoIcon: " + photoIcon 493 + ", date: " + date); 494 } 495 496 // title resource id 497 int titleResId; 498 // the text in the notification's line 1 and 2. 499 String expandedText, callName; 500 501 // increment number of missed calls. 502 mNumberMissedCalls++; 503 504 // get the name for the ticker text 505 // i.e. "Missed call from <caller name or number>" 506 if (name != null && TextUtils.isGraphic(name)) { 507 callName = name; 508 } else if (!TextUtils.isEmpty(number)){ 509 final BidiFormatter bidiFormatter = BidiFormatter.getInstance(); 510 // A number should always be displayed LTR using {@link BidiFormatter} 511 // regardless of the content of the rest of the notification. 512 callName = bidiFormatter.unicodeWrap(number, TextDirectionHeuristics.LTR); 513 } else { 514 // use "unknown" if the caller is unidentifiable. 515 callName = mContext.getString(R.string.unknown); 516 } 517 518 // display the first line of the notification: 519 // 1 missed call: call name 520 // more than 1 missed call: <number of calls> + "missed calls" 521 if (mNumberMissedCalls == 1) { 522 titleResId = R.string.notification_missedCallTitle; 523 expandedText = callName; 524 } else { 525 titleResId = R.string.notification_missedCallsTitle; 526 expandedText = mContext.getString(R.string.notification_missedCallsMsg, 527 mNumberMissedCalls); 528 } 529 530 Notification.Builder builder = new Notification.Builder(mContext); 531 builder.setSmallIcon(android.R.drawable.stat_notify_missed_call) 532 .setTicker(mContext.getString(R.string.notification_missedCallTicker, callName)) 533 .setWhen(date) 534 .setContentTitle(mContext.getText(titleResId)) 535 .setContentText(expandedText) 536 .setContentIntent(pendingCallLogIntent) 537 .setAutoCancel(true) 538 .setDeleteIntent(createClearMissedCallsIntent()); 539 540 // Simple workaround for issue 6476275; refrain having actions when the given number seems 541 // not a real one but a non-number which was embedded by methods outside (like 542 // PhoneUtils#modifyForSpecialCnapCases()). 543 // TODO: consider removing equals() checks here, and modify callers of this method instead. 544 if (mNumberMissedCalls == 1 545 && !TextUtils.isEmpty(number) 546 && (presentation == PhoneConstants.PRESENTATION_ALLOWED || 547 presentation == PhoneConstants.PRESENTATION_PAYPHONE)) { 548 if (DBG) log("Add actions with the number " + number); 549 550 builder.addAction(R.drawable.stat_sys_phone_call, 551 mContext.getString(R.string.notification_missedCall_call_back), 552 PhoneGlobals.getCallBackPendingIntent(mContext, number)); 553 554 builder.addAction(R.drawable.ic_text_holo_dark, 555 mContext.getString(R.string.notification_missedCall_message), 556 PhoneGlobals.getSendSmsFromNotificationPendingIntent(mContext, number)); 557 558 if (photoIcon != null) { 559 builder.setLargeIcon(photoIcon); 560 } else if (photo instanceof BitmapDrawable) { 561 builder.setLargeIcon(((BitmapDrawable) photo).getBitmap()); 562 } 563 } else { 564 if (DBG) { 565 log("Suppress actions. number: " + number + ", missedCalls: " + mNumberMissedCalls); 566 } 567 } 568 569 Notification notification = builder.getNotification(); 570 configureLedNotification(notification); 571 mNotificationManager.notify(MISSED_CALL_NOTIFICATION, notification); 572 } 573 574 /** Returns an intent to be invoked when the missed call notification is cleared. */ 575 private PendingIntent createClearMissedCallsIntent() { 576 Intent intent = new Intent(mContext, ClearMissedCallsService.class); 577 intent.setAction(ClearMissedCallsService.ACTION_CLEAR_MISSED_CALLS); 578 return PendingIntent.getService(mContext, 0, intent, 0); 579 } 580 581 /** 582 * Cancels the "missed call" notification. 583 * 584 * @see ITelephony.cancelMissedCallsNotification() 585 */ 586 void cancelMissedCallNotification() { 587 // reset the number of missed calls to 0. 588 mNumberMissedCalls = 0; 589 mNotificationManager.cancel(MISSED_CALL_NOTIFICATION); 590 } 591 592 private void notifySpeakerphone() { 593 if (!mShowingSpeakerphoneIcon) { 594 mStatusBarManager.setIcon("speakerphone", android.R.drawable.stat_sys_speakerphone, 0, 595 mContext.getString(R.string.accessibility_speakerphone_enabled)); 596 mShowingSpeakerphoneIcon = true; 597 } 598 } 599 600 private void cancelSpeakerphone() { 601 if (mShowingSpeakerphoneIcon) { 602 mStatusBarManager.removeIcon("speakerphone"); 603 mShowingSpeakerphoneIcon = false; 604 } 605 } 606 607 /** 608 * Shows or hides the "speakerphone" notification in the status bar, 609 * based on the actual current state of the speaker. 610 * 611 * If you already know the current speaker state (e.g. if you just 612 * called AudioManager.setSpeakerphoneOn() yourself) then you should 613 * directly call {@link #updateSpeakerNotification(boolean)} instead. 614 * 615 * (But note that the status bar icon is *never* shown while the in-call UI 616 * is active; it only appears if you bail out to some other activity.) 617 */ 618 private void updateSpeakerNotification() { 619 AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); 620 boolean showNotification = 621 (mPhone.getState() == PhoneConstants.State.OFFHOOK) && audioManager.isSpeakerphoneOn(); 622 623 if (DBG) log(showNotification 624 ? "updateSpeakerNotification: speaker ON" 625 : "updateSpeakerNotification: speaker OFF (or not offhook)"); 626 627 updateSpeakerNotification(showNotification); 628 } 629 630 /** 631 * Shows or hides the "speakerphone" notification in the status bar. 632 * 633 * @param showNotification if true, call notifySpeakerphone(); 634 * if false, call cancelSpeakerphone(). 635 * 636 * Use {@link updateSpeakerNotification()} to update the status bar 637 * based on the actual current state of the speaker. 638 * 639 * (But note that the status bar icon is *never* shown while the in-call UI 640 * is active; it only appears if you bail out to some other activity.) 641 */ 642 public void updateSpeakerNotification(boolean showNotification) { 643 if (DBG) log("updateSpeakerNotification(" + showNotification + ")..."); 644 645 // Regardless of the value of the showNotification param, suppress 646 // the status bar icon if the the InCallScreen is the foreground 647 // activity, since the in-call UI already provides an onscreen 648 // indication of the speaker state. (This reduces clutter in the 649 // status bar.) 650 651 if (showNotification) { 652 notifySpeakerphone(); 653 } else { 654 cancelSpeakerphone(); 655 } 656 } 657 658 private void notifyMute() { 659 if (!mShowingMuteIcon) { 660 mStatusBarManager.setIcon("mute", android.R.drawable.stat_notify_call_mute, 0, 661 mContext.getString(R.string.accessibility_call_muted)); 662 mShowingMuteIcon = true; 663 } 664 } 665 666 private void cancelMute() { 667 if (mShowingMuteIcon) { 668 mStatusBarManager.removeIcon("mute"); 669 mShowingMuteIcon = false; 670 } 671 } 672 673 /** 674 * Shows or hides the "mute" notification in the status bar, 675 * based on the current mute state of the Phone. 676 * 677 * (But note that the status bar icon is *never* shown while the in-call UI 678 * is active; it only appears if you bail out to some other activity.) 679 */ 680 void updateMuteNotification() { 681 // Suppress the status bar icon if the the InCallScreen is the 682 // foreground activity, since the in-call UI already provides an 683 // onscreen indication of the mute state. (This reduces clutter 684 // in the status bar.) 685 686 if ((mCM.getState() == PhoneConstants.State.OFFHOOK) && PhoneUtils.getMute()) { 687 if (DBG) log("updateMuteNotification: MUTED"); 688 notifyMute(); 689 } else { 690 if (DBG) log("updateMuteNotification: not muted (or not offhook)"); 691 cancelMute(); 692 } 693 } 694 695 /** 696 * Completely take down the in-call notification *and* the mute/speaker 697 * notifications as well, to indicate that the phone is now idle. 698 */ 699 /* package */ void cancelCallInProgressNotifications() { 700 if (DBG) log("cancelCallInProgressNotifications"); 701 cancelMute(); 702 cancelSpeakerphone(); 703 } 704 705 /** 706 * Updates the message waiting indicator (voicemail) notification. 707 * 708 * @param visible true if there are messages waiting 709 */ 710 /* package */ void updateMwi(boolean visible) { 711 if (DBG) log("updateMwi(): " + visible); 712 713 if (visible) { 714 int resId = android.R.drawable.stat_notify_voicemail; 715 716 // This Notification can get a lot fancier once we have more 717 // information about the current voicemail messages. 718 // (For example, the current voicemail system can't tell 719 // us the caller-id or timestamp of a message, or tell us the 720 // message count.) 721 722 // But for now, the UI is ultra-simple: if the MWI indication 723 // is supposed to be visible, just show a single generic 724 // notification. 725 726 String notificationTitle = mContext.getString(R.string.notification_voicemail_title); 727 String vmNumber = mPhone.getVoiceMailNumber(); 728 if (DBG) log("- got vm number: '" + vmNumber + "'"); 729 730 // Watch out: vmNumber may be null, for two possible reasons: 731 // 732 // (1) This phone really has no voicemail number 733 // 734 // (2) This phone *does* have a voicemail number, but 735 // the SIM isn't ready yet. 736 // 737 // Case (2) *does* happen in practice if you have voicemail 738 // messages when the device first boots: we get an MWI 739 // notification as soon as we register on the network, but the 740 // SIM hasn't finished loading yet. 741 // 742 // So handle case (2) by retrying the lookup after a short 743 // delay. 744 745 if ((vmNumber == null) && !mPhone.getIccRecordsLoaded()) { 746 if (DBG) log("- Null vm number: SIM records not loaded (yet)..."); 747 748 // TODO: rather than retrying after an arbitrary delay, it 749 // would be cleaner to instead just wait for a 750 // SIM_RECORDS_LOADED notification. 751 // (Unfortunately right now there's no convenient way to 752 // get that notification in phone app code. We'd first 753 // want to add a call like registerForSimRecordsLoaded() 754 // to Phone.java and GSMPhone.java, and *then* we could 755 // listen for that in the CallNotifier class.) 756 757 // Limit the number of retries (in case the SIM is broken 758 // or missing and can *never* load successfully.) 759 if (mVmNumberRetriesRemaining-- > 0) { 760 if (DBG) log(" - Retrying in " + VM_NUMBER_RETRY_DELAY_MILLIS + " msec..."); 761 mApp.notifier.sendMwiChangedDelayed(VM_NUMBER_RETRY_DELAY_MILLIS); 762 return; 763 } else { 764 Log.w(LOG_TAG, "NotificationMgr.updateMwi: getVoiceMailNumber() failed after " 765 + MAX_VM_NUMBER_RETRIES + " retries; giving up."); 766 // ...and continue with vmNumber==null, just as if the 767 // phone had no VM number set up in the first place. 768 } 769 } 770 771 if (TelephonyCapabilities.supportsVoiceMessageCount(mPhone)) { 772 int vmCount = mPhone.getVoiceMessageCount(); 773 String titleFormat = mContext.getString(R.string.notification_voicemail_title_count); 774 notificationTitle = String.format(titleFormat, vmCount); 775 } 776 777 String notificationText; 778 if (TextUtils.isEmpty(vmNumber)) { 779 notificationText = mContext.getString( 780 R.string.notification_voicemail_no_vm_number); 781 } else { 782 notificationText = String.format( 783 mContext.getString(R.string.notification_voicemail_text_format), 784 PhoneNumberUtils.formatNumber(vmNumber)); 785 } 786 787 Intent intent = new Intent(Intent.ACTION_CALL, 788 Uri.fromParts(Constants.SCHEME_VOICEMAIL, "", null)); 789 PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0); 790 791 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); 792 Uri ringtoneUri; 793 String uriString = prefs.getString( 794 CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_RINGTONE_KEY, null); 795 if (!TextUtils.isEmpty(uriString)) { 796 ringtoneUri = Uri.parse(uriString); 797 } else { 798 ringtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI; 799 } 800 801 Notification.Builder builder = new Notification.Builder(mContext); 802 builder.setSmallIcon(resId) 803 .setWhen(System.currentTimeMillis()) 804 .setContentTitle(notificationTitle) 805 .setContentText(notificationText) 806 .setContentIntent(pendingIntent) 807 .setSound(ringtoneUri); 808 Notification notification = builder.getNotification(); 809 810 CallFeaturesSetting.migrateVoicemailVibrationSettingsIfNeeded(prefs); 811 final boolean vibrate = prefs.getBoolean( 812 CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_VIBRATE_KEY, false); 813 if (vibrate) { 814 notification.defaults |= Notification.DEFAULT_VIBRATE; 815 } 816 notification.flags |= Notification.FLAG_NO_CLEAR; 817 configureLedNotification(notification); 818 mNotificationManager.notify(VOICEMAIL_NOTIFICATION, notification); 819 } else { 820 mNotificationManager.cancel(VOICEMAIL_NOTIFICATION); 821 } 822 } 823 824 /** 825 * Updates the message call forwarding indicator notification. 826 * 827 * @param visible true if there are messages waiting 828 */ 829 /* package */ void updateCfi(boolean visible) { 830 if (DBG) log("updateCfi(): " + visible); 831 if (visible) { 832 // If Unconditional Call Forwarding (forward all calls) for VOICE 833 // is enabled, just show a notification. We'll default to expanded 834 // view for now, so the there is less confusion about the icon. If 835 // it is deemed too weird to have CF indications as expanded views, 836 // then we'll flip the flag back. 837 838 // TODO: We may want to take a look to see if the notification can 839 // display the target to forward calls to. This will require some 840 // effort though, since there are multiple layers of messages that 841 // will need to propagate that information. 842 843 Notification notification; 844 final boolean showExpandedNotification = true; 845 if (showExpandedNotification) { 846 Intent intent = new Intent(Intent.ACTION_MAIN); 847 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 848 intent.setClassName("com.android.phone", 849 "com.android.phone.CallFeaturesSetting"); 850 851 notification = new Notification( 852 R.drawable.stat_sys_phone_call_forward, // icon 853 null, // tickerText 854 0); // The "timestamp" of this notification is meaningless; 855 // we only care about whether CFI is currently on or not. 856 notification.setLatestEventInfo( 857 mContext, // context 858 mContext.getString(R.string.labelCF), // expandedTitle 859 mContext.getString(R.string.sum_cfu_enabled_indicator), // expandedText 860 PendingIntent.getActivity(mContext, 0, intent, 0)); // contentIntent 861 } else { 862 notification = new Notification( 863 R.drawable.stat_sys_phone_call_forward, // icon 864 null, // tickerText 865 System.currentTimeMillis() // when 866 ); 867 } 868 869 notification.flags |= Notification.FLAG_ONGOING_EVENT; // also implies FLAG_NO_CLEAR 870 871 mNotificationManager.notify( 872 CALL_FORWARD_NOTIFICATION, 873 notification); 874 } else { 875 mNotificationManager.cancel(CALL_FORWARD_NOTIFICATION); 876 } 877 } 878 879 /** 880 * Shows the "data disconnected due to roaming" notification, which 881 * appears when you lose data connectivity because you're roaming and 882 * you have the "data roaming" feature turned off. 883 */ 884 /* package */ void showDataDisconnectedRoaming() { 885 if (DBG) log("showDataDisconnectedRoaming()..."); 886 887 // "Mobile network settings" screen / dialog 888 Intent intent = new Intent(mContext, com.android.phone.MobileNetworkSettings.class); 889 890 final CharSequence contentText = mContext.getText(R.string.roaming_reenable_message); 891 892 final Notification.Builder builder = new Notification.Builder(mContext); 893 builder.setSmallIcon(android.R.drawable.stat_sys_warning); 894 builder.setContentTitle(mContext.getText(R.string.roaming)); 895 builder.setContentText(contentText); 896 builder.setContentIntent(PendingIntent.getActivity(mContext, 0, intent, 0)); 897 898 final Notification notif = new Notification.BigTextStyle(builder).bigText(contentText) 899 .build(); 900 901 mNotificationManager.notify(DATA_DISCONNECTED_ROAMING_NOTIFICATION, notif); 902 } 903 904 /** 905 * Turns off the "data disconnected due to roaming" notification. 906 */ 907 /* package */ void hideDataDisconnectedRoaming() { 908 if (DBG) log("hideDataDisconnectedRoaming()..."); 909 mNotificationManager.cancel(DATA_DISCONNECTED_ROAMING_NOTIFICATION); 910 } 911 912 /** 913 * Display the network selection "no service" notification 914 * @param operator is the numeric operator number 915 */ 916 private void showNetworkSelection(String operator) { 917 if (DBG) log("showNetworkSelection(" + operator + ")..."); 918 919 String titleText = mContext.getString( 920 R.string.notification_network_selection_title); 921 String expandedText = mContext.getString( 922 R.string.notification_network_selection_text, operator); 923 924 Notification notification = new Notification(); 925 notification.icon = android.R.drawable.stat_sys_warning; 926 notification.when = 0; 927 notification.flags = Notification.FLAG_ONGOING_EVENT; 928 notification.tickerText = null; 929 930 // create the target network operators settings intent 931 Intent intent = new Intent(Intent.ACTION_MAIN); 932 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | 933 Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 934 // Use NetworkSetting to handle the selection intent 935 intent.setComponent(new ComponentName("com.android.phone", 936 "com.android.phone.NetworkSetting")); 937 PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0); 938 939 notification.setLatestEventInfo(mContext, titleText, expandedText, pi); 940 941 mNotificationManager.notify(SELECTED_OPERATOR_FAIL_NOTIFICATION, notification); 942 } 943 944 /** 945 * Turn off the network selection "no service" notification 946 */ 947 private void cancelNetworkSelection() { 948 if (DBG) log("cancelNetworkSelection()..."); 949 mNotificationManager.cancel(SELECTED_OPERATOR_FAIL_NOTIFICATION); 950 } 951 952 /** 953 * Update notification about no service of user selected operator 954 * 955 * @param serviceState Phone service state 956 */ 957 void updateNetworkSelection(int serviceState) { 958 if (TelephonyCapabilities.supportsNetworkSelection(mPhone)) { 959 // get the shared preference of network_selection. 960 // empty is auto mode, otherwise it is the operator alpha name 961 // in case there is no operator name, check the operator numeric 962 SharedPreferences sp = 963 PreferenceManager.getDefaultSharedPreferences(mContext); 964 String networkSelection = 965 sp.getString(PhoneBase.NETWORK_SELECTION_NAME_KEY, ""); 966 if (TextUtils.isEmpty(networkSelection)) { 967 networkSelection = 968 sp.getString(PhoneBase.NETWORK_SELECTION_KEY, ""); 969 } 970 971 if (DBG) log("updateNetworkSelection()..." + "state = " + 972 serviceState + " new network " + networkSelection); 973 974 if (serviceState == ServiceState.STATE_OUT_OF_SERVICE 975 && !TextUtils.isEmpty(networkSelection)) { 976 if (!mSelectedUnavailableNotify) { 977 showNetworkSelection(networkSelection); 978 mSelectedUnavailableNotify = true; 979 } 980 } else { 981 if (mSelectedUnavailableNotify) { 982 cancelNetworkSelection(); 983 mSelectedUnavailableNotify = false; 984 } 985 } 986 } 987 } 988 989 /* package */ void postTransientNotification(int notifyId, CharSequence msg) { 990 if (mToast != null) { 991 mToast.cancel(); 992 } 993 994 mToast = Toast.makeText(mContext, msg, Toast.LENGTH_LONG); 995 mToast.show(); 996 } 997 998 private void log(String msg) { 999 Log.d(LOG_TAG, msg); 1000 } 1001 } 1002