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