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