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