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.BidiFormatter; 46import android.text.TextDirectionHeuristics; 47import android.text.TextUtils; 48import android.util.Log; 49import android.widget.Toast; 50 51import com.android.internal.telephony.Call; 52import com.android.internal.telephony.CallManager; 53import com.android.internal.telephony.CallerInfo; 54import com.android.internal.telephony.CallerInfoAsyncQuery; 55import com.android.internal.telephony.Connection; 56import com.android.internal.telephony.Phone; 57import com.android.internal.telephony.PhoneBase; 58import com.android.internal.telephony.PhoneConstants; 59import 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 */ 70public 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.type, null, null, n.date); 402 } 403 404 if (DBG) log("closing contact cursor."); 405 cursor.close(); 406 } 407 break; 408 default: 409 } 410 } 411 412 @Override 413 public void onImageLoadComplete( 414 int token, Drawable photo, Bitmap photoIcon, Object cookie) { 415 if (DBG) log("Finished loading image: " + photo); 416 NotificationInfo n = (NotificationInfo) cookie; 417 notifyMissedCall(n.name, n.number, n.type, photo, photoIcon, n.date); 418 } 419 420 /** 421 * Factory method to generate a NotificationInfo object given a 422 * cursor from the call log table. 423 */ 424 private final NotificationInfo getNotificationInfo(Cursor cursor) { 425 NotificationInfo n = new NotificationInfo(); 426 n.name = null; 427 n.number = cursor.getString(cursor.getColumnIndexOrThrow(Calls.NUMBER)); 428 n.presentation = cursor.getInt(cursor.getColumnIndexOrThrow(Calls.NUMBER_PRESENTATION)); 429 n.type = cursor.getString(cursor.getColumnIndexOrThrow(Calls.TYPE)); 430 n.date = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DATE)); 431 432 // make sure we update the number depending upon saved values in 433 // CallLog.addCall(). If either special values for unknown or 434 // private number are detected, we need to hand off the message 435 // to the missed call notification. 436 if (n.presentation != Calls.PRESENTATION_ALLOWED) { 437 n.number = null; 438 } 439 440 if (DBG) log("NotificationInfo constructed for number: " + n.number); 441 442 return n; 443 } 444 } 445 446 /** 447 * Configures a Notification to emit the blinky green message-waiting/ 448 * missed-call signal. 449 */ 450 private static void configureLedNotification(Notification note) { 451 note.flags |= Notification.FLAG_SHOW_LIGHTS; 452 note.defaults |= Notification.DEFAULT_LIGHTS; 453 } 454 455 /** 456 * Displays a notification about a missed call. 457 * 458 * @param name the contact name. 459 * @param number the phone number. Note that this may be a non-callable String like "Unknown", 460 * or "Private Number", which possibly come from methods like 461 * {@link PhoneUtils#modifyForSpecialCnapCases(Context, CallerInfo, String, int)}. 462 * @param type the type of the call. {@link android.provider.CallLog.Calls#INCOMING_TYPE} 463 * {@link android.provider.CallLog.Calls#OUTGOING_TYPE}, or 464 * {@link android.provider.CallLog.Calls#MISSED_TYPE} 465 * @param photo picture which may be used for the notification (when photoIcon is null). 466 * This also can be null when the picture itself isn't available. If photoIcon is available 467 * it should be prioritized (because this may be too huge for notification). 468 * See also {@link ContactsAsyncHelper}. 469 * @param photoIcon picture which should be used for the notification. Can be null. This is 470 * the most suitable for {@link android.app.Notification.Builder#setLargeIcon(Bitmap)}, this 471 * should be used when non-null. 472 * @param date the time when the missed call happened 473 */ 474 /* package */ void notifyMissedCall( 475 String name, String number, String type, Drawable photo, Bitmap photoIcon, long date) { 476 477 // When the user clicks this notification, we go to the call log. 478 final PendingIntent pendingCallLogIntent = PhoneGlobals.createPendingCallLogIntent( 479 mContext); 480 481 // Never display the missed call notification on non-voice-capable 482 // devices, even if the device does somehow manage to get an 483 // incoming call. 484 if (!PhoneGlobals.sVoiceCapable) { 485 if (DBG) log("notifyMissedCall: non-voice-capable device, not posting notification"); 486 return; 487 } 488 489 if (VDBG) { 490 log("notifyMissedCall(). name: " + name + ", number: " + number 491 + ", label: " + type + ", photo: " + photo + ", photoIcon: " + photoIcon 492 + ", date: " + date); 493 } 494 495 // title resource id 496 int titleResId; 497 // the text in the notification's line 1 and 2. 498 String expandedText, callName; 499 500 // increment number of missed calls. 501 mNumberMissedCalls++; 502 503 // get the name for the ticker text 504 // i.e. "Missed call from <caller name or number>" 505 if (name != null && TextUtils.isGraphic(name)) { 506 callName = name; 507 } else if (!TextUtils.isEmpty(number)){ 508 final BidiFormatter bidiFormatter = BidiFormatter.getInstance(); 509 // A number should always be displayed LTR using {@link BidiFormatter} 510 // regardless of the content of the rest of the notification. 511 callName = bidiFormatter.unicodeWrap(number, TextDirectionHeuristics.LTR); 512 } else { 513 // use "unknown" if the caller is unidentifiable. 514 callName = mContext.getString(R.string.unknown); 515 } 516 517 // display the first line of the notification: 518 // 1 missed call: call name 519 // more than 1 missed call: <number of calls> + "missed calls" 520 if (mNumberMissedCalls == 1) { 521 titleResId = R.string.notification_missedCallTitle; 522 expandedText = callName; 523 } else { 524 titleResId = R.string.notification_missedCallsTitle; 525 expandedText = mContext.getString(R.string.notification_missedCallsMsg, 526 mNumberMissedCalls); 527 } 528 529 Notification.Builder builder = new Notification.Builder(mContext); 530 builder.setSmallIcon(android.R.drawable.stat_notify_missed_call) 531 .setTicker(mContext.getString(R.string.notification_missedCallTicker, callName)) 532 .setWhen(date) 533 .setContentTitle(mContext.getText(titleResId)) 534 .setContentText(expandedText) 535 .setContentIntent(pendingCallLogIntent) 536 .setAutoCancel(true) 537 .setDeleteIntent(createClearMissedCallsIntent()); 538 539 // Simple workaround for issue 6476275; refrain having actions when the given number seems 540 // not a real one but a non-number which was embedded by methods outside (like 541 // PhoneUtils#modifyForSpecialCnapCases()). 542 // TODO: consider removing equals() checks here, and modify callers of this method instead. 543 if (mNumberMissedCalls == 1 544 && !TextUtils.isEmpty(number) 545 && !TextUtils.equals(number, mContext.getString(R.string.private_num)) 546 && !TextUtils.equals(number, mContext.getString(R.string.unknown))){ 547 if (DBG) log("Add actions with the number " + number); 548 549 builder.addAction(R.drawable.stat_sys_phone_call, 550 mContext.getString(R.string.notification_missedCall_call_back), 551 PhoneGlobals.getCallBackPendingIntent(mContext, number)); 552 553 builder.addAction(R.drawable.ic_text_holo_dark, 554 mContext.getString(R.string.notification_missedCall_message), 555 PhoneGlobals.getSendSmsFromNotificationPendingIntent(mContext, number)); 556 557 if (photoIcon != null) { 558 builder.setLargeIcon(photoIcon); 559 } else if (photo instanceof BitmapDrawable) { 560 builder.setLargeIcon(((BitmapDrawable) photo).getBitmap()); 561 } 562 } else { 563 if (DBG) { 564 log("Suppress actions. number: " + number + ", missedCalls: " + mNumberMissedCalls); 565 } 566 } 567 568 Notification notification = builder.getNotification(); 569 configureLedNotification(notification); 570 mNotificationManager.notify(MISSED_CALL_NOTIFICATION, notification); 571 } 572 573 /** Returns an intent to be invoked when the missed call notification is cleared. */ 574 private PendingIntent createClearMissedCallsIntent() { 575 Intent intent = new Intent(mContext, ClearMissedCallsService.class); 576 intent.setAction(ClearMissedCallsService.ACTION_CLEAR_MISSED_CALLS); 577 return PendingIntent.getService(mContext, 0, intent, 0); 578 } 579 580 /** 581 * Cancels the "missed call" notification. 582 * 583 * @see ITelephony.cancelMissedCallsNotification() 584 */ 585 void cancelMissedCallNotification() { 586 // reset the number of missed calls to 0. 587 mNumberMissedCalls = 0; 588 mNotificationManager.cancel(MISSED_CALL_NOTIFICATION); 589 } 590 591 private void notifySpeakerphone() { 592 if (!mShowingSpeakerphoneIcon) { 593 mStatusBarManager.setIcon("speakerphone", android.R.drawable.stat_sys_speakerphone, 0, 594 mContext.getString(R.string.accessibility_speakerphone_enabled)); 595 mShowingSpeakerphoneIcon = true; 596 } 597 } 598 599 private void cancelSpeakerphone() { 600 if (mShowingSpeakerphoneIcon) { 601 mStatusBarManager.removeIcon("speakerphone"); 602 mShowingSpeakerphoneIcon = false; 603 } 604 } 605 606 /** 607 * Shows or hides the "speakerphone" notification in the status bar, 608 * based on the actual current state of the speaker. 609 * 610 * If you already know the current speaker state (e.g. if you just 611 * called AudioManager.setSpeakerphoneOn() yourself) then you should 612 * directly call {@link #updateSpeakerNotification(boolean)} instead. 613 * 614 * (But note that the status bar icon is *never* shown while the in-call UI 615 * is active; it only appears if you bail out to some other activity.) 616 */ 617 private void updateSpeakerNotification() { 618 AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); 619 boolean showNotification = 620 (mPhone.getState() == PhoneConstants.State.OFFHOOK) && audioManager.isSpeakerphoneOn(); 621 622 if (DBG) log(showNotification 623 ? "updateSpeakerNotification: speaker ON" 624 : "updateSpeakerNotification: speaker OFF (or not offhook)"); 625 626 updateSpeakerNotification(showNotification); 627 } 628 629 /** 630 * Shows or hides the "speakerphone" notification in the status bar. 631 * 632 * @param showNotification if true, call notifySpeakerphone(); 633 * if false, call cancelSpeakerphone(). 634 * 635 * Use {@link updateSpeakerNotification()} to update the status bar 636 * based on the actual current state of the speaker. 637 * 638 * (But note that the status bar icon is *never* shown while the in-call UI 639 * is active; it only appears if you bail out to some other activity.) 640 */ 641 public void updateSpeakerNotification(boolean showNotification) { 642 if (DBG) log("updateSpeakerNotification(" + showNotification + ")..."); 643 644 // Regardless of the value of the showNotification param, suppress 645 // the status bar icon if the the InCallScreen is the foreground 646 // activity, since the in-call UI already provides an onscreen 647 // indication of the speaker state. (This reduces clutter in the 648 // status bar.) 649 650 if (showNotification) { 651 notifySpeakerphone(); 652 } else { 653 cancelSpeakerphone(); 654 } 655 } 656 657 private void notifyMute() { 658 if (!mShowingMuteIcon) { 659 mStatusBarManager.setIcon("mute", android.R.drawable.stat_notify_call_mute, 0, 660 mContext.getString(R.string.accessibility_call_muted)); 661 mShowingMuteIcon = true; 662 } 663 } 664 665 private void cancelMute() { 666 if (mShowingMuteIcon) { 667 mStatusBarManager.removeIcon("mute"); 668 mShowingMuteIcon = false; 669 } 670 } 671 672 /** 673 * Shows or hides the "mute" notification in the status bar, 674 * based on the current mute state of the Phone. 675 * 676 * (But note that the status bar icon is *never* shown while the in-call UI 677 * is active; it only appears if you bail out to some other activity.) 678 */ 679 void updateMuteNotification() { 680 // Suppress the status bar icon if the the InCallScreen is the 681 // foreground activity, since the in-call UI already provides an 682 // onscreen indication of the mute state. (This reduces clutter 683 // in the status bar.) 684 685 if ((mCM.getState() == PhoneConstants.State.OFFHOOK) && PhoneUtils.getMute()) { 686 if (DBG) log("updateMuteNotification: MUTED"); 687 notifyMute(); 688 } else { 689 if (DBG) log("updateMuteNotification: not muted (or not offhook)"); 690 cancelMute(); 691 } 692 } 693 694 /** 695 * Completely take down the in-call notification *and* the mute/speaker 696 * notifications as well, to indicate that the phone is now idle. 697 */ 698 /* package */ void cancelCallInProgressNotifications() { 699 if (DBG) log("cancelCallInProgressNotifications"); 700 cancelMute(); 701 cancelSpeakerphone(); 702 } 703 704 /** 705 * Updates the message waiting indicator (voicemail) notification. 706 * 707 * @param visible true if there are messages waiting 708 */ 709 /* package */ void updateMwi(boolean visible) { 710 if (DBG) log("updateMwi(): " + visible); 711 712 if (visible) { 713 int resId = android.R.drawable.stat_notify_voicemail; 714 715 // This Notification can get a lot fancier once we have more 716 // information about the current voicemail messages. 717 // (For example, the current voicemail system can't tell 718 // us the caller-id or timestamp of a message, or tell us the 719 // message count.) 720 721 // But for now, the UI is ultra-simple: if the MWI indication 722 // is supposed to be visible, just show a single generic 723 // notification. 724 725 String notificationTitle = mContext.getString(R.string.notification_voicemail_title); 726 String vmNumber = mPhone.getVoiceMailNumber(); 727 if (DBG) log("- got vm number: '" + vmNumber + "'"); 728 729 // Watch out: vmNumber may be null, for two possible reasons: 730 // 731 // (1) This phone really has no voicemail number 732 // 733 // (2) This phone *does* have a voicemail number, but 734 // the SIM isn't ready yet. 735 // 736 // Case (2) *does* happen in practice if you have voicemail 737 // messages when the device first boots: we get an MWI 738 // notification as soon as we register on the network, but the 739 // SIM hasn't finished loading yet. 740 // 741 // So handle case (2) by retrying the lookup after a short 742 // delay. 743 744 if ((vmNumber == null) && !mPhone.getIccRecordsLoaded()) { 745 if (DBG) log("- Null vm number: SIM records not loaded (yet)..."); 746 747 // TODO: rather than retrying after an arbitrary delay, it 748 // would be cleaner to instead just wait for a 749 // SIM_RECORDS_LOADED notification. 750 // (Unfortunately right now there's no convenient way to 751 // get that notification in phone app code. We'd first 752 // want to add a call like registerForSimRecordsLoaded() 753 // to Phone.java and GSMPhone.java, and *then* we could 754 // listen for that in the CallNotifier class.) 755 756 // Limit the number of retries (in case the SIM is broken 757 // or missing and can *never* load successfully.) 758 if (mVmNumberRetriesRemaining-- > 0) { 759 if (DBG) log(" - Retrying in " + VM_NUMBER_RETRY_DELAY_MILLIS + " msec..."); 760 mApp.notifier.sendMwiChangedDelayed(VM_NUMBER_RETRY_DELAY_MILLIS); 761 return; 762 } else { 763 Log.w(LOG_TAG, "NotificationMgr.updateMwi: getVoiceMailNumber() failed after " 764 + MAX_VM_NUMBER_RETRIES + " retries; giving up."); 765 // ...and continue with vmNumber==null, just as if the 766 // phone had no VM number set up in the first place. 767 } 768 } 769 770 if (TelephonyCapabilities.supportsVoiceMessageCount(mPhone)) { 771 int vmCount = mPhone.getVoiceMessageCount(); 772 String titleFormat = mContext.getString(R.string.notification_voicemail_title_count); 773 notificationTitle = String.format(titleFormat, vmCount); 774 } 775 776 String notificationText; 777 if (TextUtils.isEmpty(vmNumber)) { 778 notificationText = mContext.getString( 779 R.string.notification_voicemail_no_vm_number); 780 } else { 781 notificationText = String.format( 782 mContext.getString(R.string.notification_voicemail_text_format), 783 PhoneNumberUtils.formatNumber(vmNumber)); 784 } 785 786 Intent intent = new Intent(Intent.ACTION_CALL, 787 Uri.fromParts(Constants.SCHEME_VOICEMAIL, "", null)); 788 PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0); 789 790 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); 791 Uri ringtoneUri; 792 String uriString = prefs.getString( 793 CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_RINGTONE_KEY, null); 794 if (!TextUtils.isEmpty(uriString)) { 795 ringtoneUri = Uri.parse(uriString); 796 } else { 797 ringtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI; 798 } 799 800 Notification.Builder builder = new Notification.Builder(mContext); 801 builder.setSmallIcon(resId) 802 .setWhen(System.currentTimeMillis()) 803 .setContentTitle(notificationTitle) 804 .setContentText(notificationText) 805 .setContentIntent(pendingIntent) 806 .setSound(ringtoneUri); 807 Notification notification = builder.getNotification(); 808 809 CallFeaturesSetting.migrateVoicemailVibrationSettingsIfNeeded(prefs); 810 final boolean vibrate = prefs.getBoolean( 811 CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_VIBRATE_KEY, false); 812 if (vibrate) { 813 notification.defaults |= Notification.DEFAULT_VIBRATE; 814 } 815 notification.flags |= Notification.FLAG_NO_CLEAR; 816 configureLedNotification(notification); 817 mNotificationManager.notify(VOICEMAIL_NOTIFICATION, notification); 818 } else { 819 mNotificationManager.cancel(VOICEMAIL_NOTIFICATION); 820 } 821 } 822 823 /** 824 * Updates the message call forwarding indicator notification. 825 * 826 * @param visible true if there are messages waiting 827 */ 828 /* package */ void updateCfi(boolean visible) { 829 if (DBG) log("updateCfi(): " + visible); 830 if (visible) { 831 // If Unconditional Call Forwarding (forward all calls) for VOICE 832 // is enabled, just show a notification. We'll default to expanded 833 // view for now, so the there is less confusion about the icon. If 834 // it is deemed too weird to have CF indications as expanded views, 835 // then we'll flip the flag back. 836 837 // TODO: We may want to take a look to see if the notification can 838 // display the target to forward calls to. This will require some 839 // effort though, since there are multiple layers of messages that 840 // will need to propagate that information. 841 842 Notification notification; 843 final boolean showExpandedNotification = true; 844 if (showExpandedNotification) { 845 Intent intent = new Intent(Intent.ACTION_MAIN); 846 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 847 intent.setClassName("com.android.phone", 848 "com.android.phone.CallFeaturesSetting"); 849 850 notification = new Notification( 851 R.drawable.stat_sys_phone_call_forward, // icon 852 null, // tickerText 853 0); // The "timestamp" of this notification is meaningless; 854 // we only care about whether CFI is currently on or not. 855 notification.setLatestEventInfo( 856 mContext, // context 857 mContext.getString(R.string.labelCF), // expandedTitle 858 mContext.getString(R.string.sum_cfu_enabled_indicator), // expandedText 859 PendingIntent.getActivity(mContext, 0, intent, 0)); // contentIntent 860 } else { 861 notification = new Notification( 862 R.drawable.stat_sys_phone_call_forward, // icon 863 null, // tickerText 864 System.currentTimeMillis() // when 865 ); 866 } 867 868 notification.flags |= Notification.FLAG_ONGOING_EVENT; // also implies FLAG_NO_CLEAR 869 870 mNotificationManager.notify( 871 CALL_FORWARD_NOTIFICATION, 872 notification); 873 } else { 874 mNotificationManager.cancel(CALL_FORWARD_NOTIFICATION); 875 } 876 } 877 878 /** 879 * Shows the "data disconnected due to roaming" notification, which 880 * appears when you lose data connectivity because you're roaming and 881 * you have the "data roaming" feature turned off. 882 */ 883 /* package */ void showDataDisconnectedRoaming() { 884 if (DBG) log("showDataDisconnectedRoaming()..."); 885 886 // "Mobile network settings" screen / dialog 887 Intent intent = new Intent(mContext, com.android.phone.MobileNetworkSettings.class); 888 889 final CharSequence contentText = mContext.getText(R.string.roaming_reenable_message); 890 891 final Notification.Builder builder = new Notification.Builder(mContext); 892 builder.setSmallIcon(android.R.drawable.stat_sys_warning); 893 builder.setContentTitle(mContext.getText(R.string.roaming)); 894 builder.setContentText(contentText); 895 builder.setContentIntent(PendingIntent.getActivity(mContext, 0, intent, 0)); 896 897 final Notification notif = new Notification.BigTextStyle(builder).bigText(contentText) 898 .build(); 899 900 mNotificationManager.notify(DATA_DISCONNECTED_ROAMING_NOTIFICATION, notif); 901 } 902 903 /** 904 * Turns off the "data disconnected due to roaming" notification. 905 */ 906 /* package */ void hideDataDisconnectedRoaming() { 907 if (DBG) log("hideDataDisconnectedRoaming()..."); 908 mNotificationManager.cancel(DATA_DISCONNECTED_ROAMING_NOTIFICATION); 909 } 910 911 /** 912 * Display the network selection "no service" notification 913 * @param operator is the numeric operator number 914 */ 915 private void showNetworkSelection(String operator) { 916 if (DBG) log("showNetworkSelection(" + operator + ")..."); 917 918 String titleText = mContext.getString( 919 R.string.notification_network_selection_title); 920 String expandedText = mContext.getString( 921 R.string.notification_network_selection_text, operator); 922 923 Notification notification = new Notification(); 924 notification.icon = android.R.drawable.stat_sys_warning; 925 notification.when = 0; 926 notification.flags = Notification.FLAG_ONGOING_EVENT; 927 notification.tickerText = null; 928 929 // create the target network operators settings intent 930 Intent intent = new Intent(Intent.ACTION_MAIN); 931 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | 932 Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 933 // Use NetworkSetting to handle the selection intent 934 intent.setComponent(new ComponentName("com.android.phone", 935 "com.android.phone.NetworkSetting")); 936 PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0); 937 938 notification.setLatestEventInfo(mContext, titleText, expandedText, pi); 939 940 mNotificationManager.notify(SELECTED_OPERATOR_FAIL_NOTIFICATION, notification); 941 } 942 943 /** 944 * Turn off the network selection "no service" notification 945 */ 946 private void cancelNetworkSelection() { 947 if (DBG) log("cancelNetworkSelection()..."); 948 mNotificationManager.cancel(SELECTED_OPERATOR_FAIL_NOTIFICATION); 949 } 950 951 /** 952 * Update notification about no service of user selected operator 953 * 954 * @param serviceState Phone service state 955 */ 956 void updateNetworkSelection(int serviceState) { 957 if (TelephonyCapabilities.supportsNetworkSelection(mPhone)) { 958 // get the shared preference of network_selection. 959 // empty is auto mode, otherwise it is the operator alpha name 960 // in case there is no operator name, check the operator numeric 961 SharedPreferences sp = 962 PreferenceManager.getDefaultSharedPreferences(mContext); 963 String networkSelection = 964 sp.getString(PhoneBase.NETWORK_SELECTION_NAME_KEY, ""); 965 if (TextUtils.isEmpty(networkSelection)) { 966 networkSelection = 967 sp.getString(PhoneBase.NETWORK_SELECTION_KEY, ""); 968 } 969 970 if (DBG) log("updateNetworkSelection()..." + "state = " + 971 serviceState + " new network " + networkSelection); 972 973 if (serviceState == ServiceState.STATE_OUT_OF_SERVICE 974 && !TextUtils.isEmpty(networkSelection)) { 975 if (!mSelectedUnavailableNotify) { 976 showNetworkSelection(networkSelection); 977 mSelectedUnavailableNotify = true; 978 } 979 } else { 980 if (mSelectedUnavailableNotify) { 981 cancelNetworkSelection(); 982 mSelectedUnavailableNotify = false; 983 } 984 } 985 } 986 } 987 988 /* package */ void postTransientNotification(int notifyId, CharSequence msg) { 989 if (mToast != null) { 990 mToast.cancel(); 991 } 992 993 mToast = Toast.makeText(mContext, msg, Toast.LENGTH_LONG); 994 mToast.show(); 995 } 996 997 private void log(String msg) { 998 Log.d(LOG_TAG, msg); 999 } 1000} 1001