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