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.ComponentName; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.SharedPreferences; 27 import android.content.pm.UserInfo; 28 import android.net.Uri; 29 import android.os.SystemProperties; 30 import android.os.UserHandle; 31 import android.os.UserManager; 32 import android.preference.PreferenceManager; 33 import android.provider.ContactsContract.PhoneLookup; 34 import android.provider.Settings; 35 import android.telecom.PhoneAccount; 36 import android.telephony.PhoneNumberUtils; 37 import android.telephony.ServiceState; 38 import android.text.TextUtils; 39 import android.util.Log; 40 import android.widget.Toast; 41 42 import com.android.internal.telephony.Phone; 43 import com.android.internal.telephony.PhoneBase; 44 import com.android.internal.telephony.TelephonyCapabilities; 45 46 import java.util.List; 47 48 /** 49 * NotificationManager-related utility code for the Phone app. 50 * 51 * This is a singleton object which acts as the interface to the 52 * framework's NotificationManager, and is used to display status bar 53 * icons and control other status bar-related behavior. 54 * 55 * @see PhoneGlobals.notificationMgr 56 */ 57 public class NotificationMgr { 58 private static final String LOG_TAG = "NotificationMgr"; 59 private static final boolean DBG = 60 (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1); 61 // Do not check in with VDBG = true, since that may write PII to the system log. 62 private static final boolean VDBG = false; 63 64 // notification types 65 static final int MMI_NOTIFICATION = 1; 66 static final int NETWORK_SELECTION_NOTIFICATION = 2; 67 static final int VOICEMAIL_NOTIFICATION = 3; 68 static final int CALL_FORWARD_NOTIFICATION = 4; 69 static final int DATA_DISCONNECTED_ROAMING_NOTIFICATION = 5; 70 static final int SELECTED_OPERATOR_FAIL_NOTIFICATION = 6; 71 72 /** The singleton NotificationMgr instance. */ 73 private static NotificationMgr sInstance; 74 75 private PhoneGlobals mApp; 76 private Phone mPhone; 77 78 private Context mContext; 79 private NotificationManager mNotificationManager; 80 private StatusBarManager mStatusBarManager; 81 private UserManager mUserManager; 82 private Toast mToast; 83 84 public StatusBarHelper statusBarHelper; 85 86 // used to track the notification of selected network unavailable 87 private boolean mSelectedUnavailableNotify = false; 88 89 // Retry params for the getVoiceMailNumber() call; see updateMwi(). 90 private static final int MAX_VM_NUMBER_RETRIES = 5; 91 private static final int VM_NUMBER_RETRY_DELAY_MILLIS = 10000; 92 private int mVmNumberRetriesRemaining = MAX_VM_NUMBER_RETRIES; 93 94 /** 95 * Private constructor (this is a singleton). 96 * @see #init(PhoneGlobals) 97 */ 98 private NotificationMgr(PhoneGlobals app) { 99 mApp = app; 100 mContext = app; 101 mNotificationManager = 102 (NotificationManager) app.getSystemService(Context.NOTIFICATION_SERVICE); 103 mStatusBarManager = 104 (StatusBarManager) app.getSystemService(Context.STATUS_BAR_SERVICE); 105 mUserManager = (UserManager) app.getSystemService(Context.USER_SERVICE); 106 mPhone = app.phone; // TODO: better style to use mCM.getDefaultPhone() everywhere instead 107 statusBarHelper = new StatusBarHelper(); 108 } 109 110 /** 111 * Initialize the singleton NotificationMgr instance. 112 * 113 * This is only done once, at startup, from PhoneApp.onCreate(). 114 * From then on, the NotificationMgr instance is available via the 115 * PhoneApp's public "notificationMgr" field, which is why there's no 116 * getInstance() method here. 117 */ 118 /* package */ static NotificationMgr init(PhoneGlobals app) { 119 synchronized (NotificationMgr.class) { 120 if (sInstance == null) { 121 sInstance = new NotificationMgr(app); 122 } else { 123 Log.wtf(LOG_TAG, "init() called multiple times! sInstance = " + sInstance); 124 } 125 return sInstance; 126 } 127 } 128 129 /** 130 * Helper class that's a wrapper around the framework's 131 * StatusBarManager.disable() API. 132 * 133 * This class is used to control features like: 134 * 135 * - Disabling the status bar "notification windowshade" 136 * while the in-call UI is up 137 * 138 * - Disabling notification alerts (audible or vibrating) 139 * while a phone call is active 140 * 141 * - Disabling navigation via the system bar (the "soft buttons" at 142 * the bottom of the screen on devices with no hard buttons) 143 * 144 * We control these features through a single point of control to make 145 * sure that the various StatusBarManager.disable() calls don't 146 * interfere with each other. 147 */ 148 public class StatusBarHelper { 149 // Current desired state of status bar / system bar behavior 150 private boolean mIsNotificationEnabled = true; 151 private boolean mIsExpandedViewEnabled = true; 152 private boolean mIsSystemBarNavigationEnabled = true; 153 154 private StatusBarHelper () { 155 } 156 157 /** 158 * Enables or disables auditory / vibrational alerts. 159 * 160 * (We disable these any time a voice call is active, regardless 161 * of whether or not the in-call UI is visible.) 162 */ 163 public void enableNotificationAlerts(boolean enable) { 164 if (mIsNotificationEnabled != enable) { 165 mIsNotificationEnabled = enable; 166 updateStatusBar(); 167 } 168 } 169 170 /** 171 * Enables or disables the expanded view of the status bar 172 * (i.e. the ability to pull down the "notification windowshade"). 173 * 174 * (This feature is disabled by the InCallScreen while the in-call 175 * UI is active.) 176 */ 177 public void enableExpandedView(boolean enable) { 178 if (mIsExpandedViewEnabled != enable) { 179 mIsExpandedViewEnabled = enable; 180 updateStatusBar(); 181 } 182 } 183 184 /** 185 * Enables or disables the navigation via the system bar (the 186 * "soft buttons" at the bottom of the screen) 187 * 188 * (This feature is disabled while an incoming call is ringing, 189 * because it's easy to accidentally touch the system bar while 190 * pulling the phone out of your pocket.) 191 */ 192 public void enableSystemBarNavigation(boolean enable) { 193 if (mIsSystemBarNavigationEnabled != enable) { 194 mIsSystemBarNavigationEnabled = enable; 195 updateStatusBar(); 196 } 197 } 198 199 /** 200 * Updates the status bar to reflect the current desired state. 201 */ 202 private void updateStatusBar() { 203 int state = StatusBarManager.DISABLE_NONE; 204 205 if (!mIsExpandedViewEnabled) { 206 state |= StatusBarManager.DISABLE_EXPAND; 207 } 208 if (!mIsNotificationEnabled) { 209 state |= StatusBarManager.DISABLE_NOTIFICATION_ALERTS; 210 } 211 if (!mIsSystemBarNavigationEnabled) { 212 // Disable *all* possible navigation via the system bar. 213 state |= StatusBarManager.DISABLE_HOME; 214 state |= StatusBarManager.DISABLE_RECENT; 215 state |= StatusBarManager.DISABLE_BACK; 216 state |= StatusBarManager.DISABLE_SEARCH; 217 } 218 219 if (DBG) log("updateStatusBar: state = 0x" + Integer.toHexString(state)); 220 mStatusBarManager.disable(state); 221 } 222 } 223 224 /** The projection to use when querying the phones table */ 225 static final String[] PHONES_PROJECTION = new String[] { 226 PhoneLookup.NUMBER, 227 PhoneLookup.DISPLAY_NAME, 228 PhoneLookup._ID 229 }; 230 231 /** 232 * Updates the message waiting indicator (voicemail) notification. 233 * 234 * @param visible true if there are messages waiting 235 */ 236 /* package */ void updateMwi(boolean visible) { 237 if (DBG) log("updateMwi(): " + visible); 238 239 if (visible) { 240 int resId = android.R.drawable.stat_notify_voicemail; 241 242 // This Notification can get a lot fancier once we have more 243 // information about the current voicemail messages. 244 // (For example, the current voicemail system can't tell 245 // us the caller-id or timestamp of a message, or tell us the 246 // message count.) 247 248 // But for now, the UI is ultra-simple: if the MWI indication 249 // is supposed to be visible, just show a single generic 250 // notification. 251 252 String notificationTitle = mContext.getString(R.string.notification_voicemail_title); 253 String vmNumber = mPhone.getVoiceMailNumber(); 254 if (DBG) log("- got vm number: '" + vmNumber + "'"); 255 256 // Watch out: vmNumber may be null, for two possible reasons: 257 // 258 // (1) This phone really has no voicemail number 259 // 260 // (2) This phone *does* have a voicemail number, but 261 // the SIM isn't ready yet. 262 // 263 // Case (2) *does* happen in practice if you have voicemail 264 // messages when the device first boots: we get an MWI 265 // notification as soon as we register on the network, but the 266 // SIM hasn't finished loading yet. 267 // 268 // So handle case (2) by retrying the lookup after a short 269 // delay. 270 271 if ((vmNumber == null) && !mPhone.getIccRecordsLoaded()) { 272 if (DBG) log("- Null vm number: SIM records not loaded (yet)..."); 273 274 // TODO: rather than retrying after an arbitrary delay, it 275 // would be cleaner to instead just wait for a 276 // SIM_RECORDS_LOADED notification. 277 // (Unfortunately right now there's no convenient way to 278 // get that notification in phone app code. We'd first 279 // want to add a call like registerForSimRecordsLoaded() 280 // to Phone.java and GSMPhone.java, and *then* we could 281 // listen for that in the CallNotifier class.) 282 283 // Limit the number of retries (in case the SIM is broken 284 // or missing and can *never* load successfully.) 285 if (mVmNumberRetriesRemaining-- > 0) { 286 if (DBG) log(" - Retrying in " + VM_NUMBER_RETRY_DELAY_MILLIS + " msec..."); 287 mApp.notifier.sendMwiChangedDelayed(VM_NUMBER_RETRY_DELAY_MILLIS); 288 return; 289 } else { 290 Log.w(LOG_TAG, "NotificationMgr.updateMwi: getVoiceMailNumber() failed after " 291 + MAX_VM_NUMBER_RETRIES + " retries; giving up."); 292 // ...and continue with vmNumber==null, just as if the 293 // phone had no VM number set up in the first place. 294 } 295 } 296 297 if (TelephonyCapabilities.supportsVoiceMessageCount(mPhone)) { 298 int vmCount = mPhone.getVoiceMessageCount(); 299 String titleFormat = mContext.getString(R.string.notification_voicemail_title_count); 300 notificationTitle = String.format(titleFormat, vmCount); 301 } 302 303 String notificationText; 304 if (TextUtils.isEmpty(vmNumber)) { 305 notificationText = mContext.getString( 306 R.string.notification_voicemail_no_vm_number); 307 } else { 308 notificationText = String.format( 309 mContext.getString(R.string.notification_voicemail_text_format), 310 PhoneNumberUtils.formatNumber(vmNumber)); 311 } 312 313 Intent intent = new Intent(Intent.ACTION_CALL, 314 Uri.fromParts(PhoneAccount.SCHEME_VOICEMAIL, "", null)); 315 PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0); 316 317 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext); 318 Uri ringtoneUri; 319 String uriString = prefs.getString( 320 CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_RINGTONE_KEY, null); 321 if (!TextUtils.isEmpty(uriString)) { 322 ringtoneUri = Uri.parse(uriString); 323 } else { 324 ringtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI; 325 } 326 327 Notification.Builder builder = new Notification.Builder(mContext); 328 builder.setSmallIcon(resId) 329 .setWhen(System.currentTimeMillis()) 330 .setContentTitle(notificationTitle) 331 .setContentText(notificationText) 332 .setContentIntent(pendingIntent) 333 .setSound(ringtoneUri) 334 .setColor(mContext.getResources().getColor(R.color.dialer_theme_color)) 335 .setOngoing(true); 336 337 CallFeaturesSetting.migrateVoicemailVibrationSettingsIfNeeded(prefs); 338 final boolean vibrate = prefs.getBoolean( 339 CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_VIBRATE_KEY, false); 340 if (vibrate) { 341 builder.setDefaults(Notification.DEFAULT_VIBRATE); 342 } 343 344 final Notification notification = builder.build(); 345 List<UserInfo> users = mUserManager.getUsers(true); 346 for (int i = 0; i < users.size(); i++) { 347 final UserInfo user = users.get(i); 348 final UserHandle userHandle = user.getUserHandle(); 349 if (!mUserManager.hasUserRestriction( 350 UserManager.DISALLOW_OUTGOING_CALLS, userHandle) 351 && !user.isManagedProfile()) { 352 mNotificationManager.notifyAsUser( 353 null /* tag */, VOICEMAIL_NOTIFICATION, notification, userHandle); 354 } 355 } 356 } else { 357 mNotificationManager.cancelAsUser( 358 null /* tag */, VOICEMAIL_NOTIFICATION, UserHandle.ALL); 359 } 360 } 361 362 /** 363 * Updates the message call forwarding indicator notification. 364 * 365 * @param visible true if there are messages waiting 366 */ 367 /* package */ void updateCfi(boolean visible) { 368 if (DBG) log("updateCfi(): " + visible); 369 if (visible) { 370 // If Unconditional Call Forwarding (forward all calls) for VOICE 371 // is enabled, just show a notification. We'll default to expanded 372 // view for now, so the there is less confusion about the icon. If 373 // it is deemed too weird to have CF indications as expanded views, 374 // then we'll flip the flag back. 375 376 // TODO: We may want to take a look to see if the notification can 377 // display the target to forward calls to. This will require some 378 // effort though, since there are multiple layers of messages that 379 // will need to propagate that information. 380 381 Notification.Builder builder = new Notification.Builder(mContext) 382 .setSmallIcon(R.drawable.stat_sys_phone_call_forward) 383 .setContentTitle(mContext.getString(R.string.labelCF)) 384 .setContentText(mContext.getString(R.string.sum_cfu_enabled_indicator)) 385 .setShowWhen(false) 386 .setOngoing(true); 387 388 Intent intent = new Intent(Intent.ACTION_MAIN); 389 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 390 intent.setClassName("com.android.phone", "com.android.phone.CallFeaturesSetting"); 391 PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0); 392 393 List<UserInfo> users = mUserManager.getUsers(true); 394 for (int i = 0; i < users.size(); i++) { 395 UserHandle userHandle = users.get(i).getUserHandle(); 396 builder.setContentIntent(userHandle.isOwner() ? contentIntent : null); 397 mNotificationManager.notifyAsUser( 398 null /* tag */, CALL_FORWARD_NOTIFICATION, builder.build(), userHandle); 399 } 400 } else { 401 mNotificationManager.cancelAsUser( 402 null /* tag */, CALL_FORWARD_NOTIFICATION, UserHandle.ALL); 403 } 404 } 405 406 /** 407 * Shows the "data disconnected due to roaming" notification, which 408 * appears when you lose data connectivity because you're roaming and 409 * you have the "data roaming" feature turned off. 410 */ 411 /* package */ void showDataDisconnectedRoaming() { 412 if (DBG) log("showDataDisconnectedRoaming()..."); 413 414 // "Mobile network settings" screen / dialog 415 Intent intent = new Intent(mContext, com.android.phone.MobileNetworkSettings.class); 416 PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0); 417 418 final CharSequence contentText = mContext.getText(R.string.roaming_reenable_message); 419 420 final Notification.Builder builder = new Notification.Builder(mContext) 421 .setSmallIcon(android.R.drawable.stat_sys_warning) 422 .setContentTitle(mContext.getText(R.string.roaming)) 423 .setColor(mContext.getResources().getColor(R.color.dialer_theme_color)) 424 .setContentText(contentText); 425 426 List<UserInfo> users = mUserManager.getUsers(true); 427 for (int i = 0; i < users.size(); i++) { 428 UserHandle userHandle = users.get(i).getUserHandle(); 429 builder.setContentIntent(userHandle.isOwner() ? contentIntent : null); 430 final Notification notif = 431 new Notification.BigTextStyle(builder).bigText(contentText).build(); 432 mNotificationManager.notifyAsUser( 433 null /* tag */, DATA_DISCONNECTED_ROAMING_NOTIFICATION, notif, userHandle); 434 } 435 } 436 437 /** 438 * Turns off the "data disconnected due to roaming" notification. 439 */ 440 /* package */ void hideDataDisconnectedRoaming() { 441 if (DBG) log("hideDataDisconnectedRoaming()..."); 442 mNotificationManager.cancel(DATA_DISCONNECTED_ROAMING_NOTIFICATION); 443 } 444 445 /** 446 * Display the network selection "no service" notification 447 * @param operator is the numeric operator number 448 */ 449 private void showNetworkSelection(String operator) { 450 if (DBG) log("showNetworkSelection(" + operator + ")..."); 451 452 Notification.Builder builder = new Notification.Builder(mContext) 453 .setSmallIcon(android.R.drawable.stat_sys_warning) 454 .setContentTitle(mContext.getString(R.string.notification_network_selection_title)) 455 .setContentText( 456 mContext.getString(R.string.notification_network_selection_text, operator)) 457 .setShowWhen(false) 458 .setOngoing(true); 459 460 // create the target network operators settings intent 461 Intent intent = new Intent(Intent.ACTION_MAIN); 462 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | 463 Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 464 // Use NetworkSetting to handle the selection intent 465 intent.setComponent(new ComponentName("com.android.phone", 466 "com.android.phone.NetworkSetting")); 467 PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0); 468 469 List<UserInfo> users = mUserManager.getUsers(true); 470 for (int i = 0; i < users.size(); i++) { 471 UserHandle userHandle = users.get(i).getUserHandle(); 472 builder.setContentIntent(userHandle.isOwner() ? contentIntent : null); 473 mNotificationManager.notifyAsUser( 474 null /* tag */, 475 SELECTED_OPERATOR_FAIL_NOTIFICATION, 476 builder.build(), 477 userHandle); 478 } 479 } 480 481 /** 482 * Turn off the network selection "no service" notification 483 */ 484 private void cancelNetworkSelection() { 485 if (DBG) log("cancelNetworkSelection()..."); 486 mNotificationManager.cancelAsUser( 487 null /* tag */, SELECTED_OPERATOR_FAIL_NOTIFICATION, UserHandle.ALL); 488 } 489 490 /** 491 * Update notification about no service of user selected operator 492 * 493 * @param serviceState Phone service state 494 */ 495 void updateNetworkSelection(int serviceState) { 496 if (TelephonyCapabilities.supportsNetworkSelection(mPhone)) { 497 // get the shared preference of network_selection. 498 // empty is auto mode, otherwise it is the operator alpha name 499 // in case there is no operator name, check the operator numeric 500 SharedPreferences sp = 501 PreferenceManager.getDefaultSharedPreferences(mContext); 502 String networkSelection = 503 sp.getString(PhoneBase.NETWORK_SELECTION_NAME_KEY, ""); 504 if (TextUtils.isEmpty(networkSelection)) { 505 networkSelection = 506 sp.getString(PhoneBase.NETWORK_SELECTION_KEY, ""); 507 } 508 509 if (DBG) log("updateNetworkSelection()..." + "state = " + 510 serviceState + " new network " + networkSelection); 511 512 if (serviceState == ServiceState.STATE_OUT_OF_SERVICE 513 && !TextUtils.isEmpty(networkSelection)) { 514 if (!mSelectedUnavailableNotify) { 515 showNetworkSelection(networkSelection); 516 mSelectedUnavailableNotify = true; 517 } 518 } else { 519 if (mSelectedUnavailableNotify) { 520 cancelNetworkSelection(); 521 mSelectedUnavailableNotify = false; 522 } 523 } 524 } 525 } 526 527 /* package */ void postTransientNotification(int notifyId, CharSequence msg) { 528 if (mToast != null) { 529 mToast.cancel(); 530 } 531 532 mToast = Toast.makeText(mContext, msg, Toast.LENGTH_LONG); 533 mToast.show(); 534 } 535 536 private void log(String msg) { 537 Log.d(LOG_TAG, msg); 538 } 539 } 540