blob: 4c0c8ed1c18e8d33ea8b16e91249f5ad69e4d365 [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 */
70public class NotificationMgr implements CallerInfoAsyncQuery.OnQueryCompleteListener{
71 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;
106 private PowerManager mPowerManager;
107 private Toast mToast;
108 private boolean mShowingSpeakerphoneIcon;
109 private boolean mShowingMuteIcon;
110
111 public StatusBarHelper statusBarHelper;
112
113 // used to track the missed call counter, default to 0.
114 private int mNumberMissedCalls = 0;
115
116 // Currently-displayed resource IDs for some status bar icons (or zero
117 // if no notification is active):
118 private int mInCallResId;
119
120 // used to track the notification of selected network unavailable
121 private boolean mSelectedUnavailableNotify = false;
122
123 // Retry params for the getVoiceMailNumber() call; see updateMwi().
124 private static final int MAX_VM_NUMBER_RETRIES = 5;
125 private static final int VM_NUMBER_RETRY_DELAY_MILLIS = 10000;
126 private int mVmNumberRetriesRemaining = MAX_VM_NUMBER_RETRIES;
127
128 // Query used to look up caller-id info for the "call log" notification.
129 private QueryHandler mQueryHandler = null;
130 private static final int CALL_LOG_TOKEN = -1;
131 private static final int CONTACT_TOKEN = -2;
132
133 /**
134 * Private constructor (this is a singleton).
135 * @see init()
136 */
137 private NotificationMgr(PhoneGlobals app) {
138 mApp = app;
139 mContext = app;
140 mNotificationManager =
141 (NotificationManager) app.getSystemService(Context.NOTIFICATION_SERVICE);
142 mStatusBarManager =
143 (StatusBarManager) app.getSystemService(Context.STATUS_BAR_SERVICE);
144 mPowerManager =
145 (PowerManager) app.getSystemService(Context.POWER_SERVICE);
146 mPhone = app.phone; // TODO: better style to use mCM.getDefaultPhone() everywhere instead
147 mCM = app.mCM;
148 statusBarHelper = new StatusBarHelper();
149 }
150
151 /**
152 * Initialize the singleton NotificationMgr instance.
153 *
154 * This is only done once, at startup, from PhoneApp.onCreate().
155 * From then on, the NotificationMgr instance is available via the
156 * PhoneApp's public "notificationMgr" field, which is why there's no
157 * getInstance() method here.
158 */
159 /* package */ static NotificationMgr init(PhoneGlobals app) {
160 synchronized (NotificationMgr.class) {
161 if (sInstance == null) {
162 sInstance = new NotificationMgr(app);
163 // Update the notifications that need to be touched at startup.
164 sInstance.updateNotificationsAtStartup();
165 } else {
166 Log.wtf(LOG_TAG, "init() called multiple times! sInstance = " + sInstance);
167 }
168 return sInstance;
169 }
170 }
171
172 /**
173 * Helper class that's a wrapper around the framework's
174 * StatusBarManager.disable() API.
175 *
176 * This class is used to control features like:
177 *
178 * - Disabling the status bar "notification windowshade"
179 * while the in-call UI is up
180 *
181 * - Disabling notification alerts (audible or vibrating)
182 * while a phone call is active
183 *
184 * - Disabling navigation via the system bar (the "soft buttons" at
185 * the bottom of the screen on devices with no hard buttons)
186 *
187 * We control these features through a single point of control to make
188 * sure that the various StatusBarManager.disable() calls don't
189 * interfere with each other.
190 */
191 public class StatusBarHelper {
192 // Current desired state of status bar / system bar behavior
193 private boolean mIsNotificationEnabled = true;
194 private boolean mIsExpandedViewEnabled = true;
195 private boolean mIsSystemBarNavigationEnabled = true;
196
197 private StatusBarHelper () {
198 }
199
200 /**
201 * Enables or disables auditory / vibrational alerts.
202 *
203 * (We disable these any time a voice call is active, regardless
204 * of whether or not the in-call UI is visible.)
205 */
206 public void enableNotificationAlerts(boolean enable) {
207 if (mIsNotificationEnabled != enable) {
208 mIsNotificationEnabled = enable;
209 updateStatusBar();
210 }
211 }
212
213 /**
214 * Enables or disables the expanded view of the status bar
215 * (i.e. the ability to pull down the "notification windowshade").
216 *
217 * (This feature is disabled by the InCallScreen while the in-call
218 * UI is active.)
219 */
220 public void enableExpandedView(boolean enable) {
221 if (mIsExpandedViewEnabled != enable) {
222 mIsExpandedViewEnabled = enable;
223 updateStatusBar();
224 }
225 }
226
227 /**
228 * Enables or disables the navigation via the system bar (the
229 * "soft buttons" at the bottom of the screen)
230 *
231 * (This feature is disabled while an incoming call is ringing,
232 * because it's easy to accidentally touch the system bar while
233 * pulling the phone out of your pocket.)
234 */
235 public void enableSystemBarNavigation(boolean enable) {
236 if (mIsSystemBarNavigationEnabled != enable) {
237 mIsSystemBarNavigationEnabled = enable;
238 updateStatusBar();
239 }
240 }
241
242 /**
243 * Updates the status bar to reflect the current desired state.
244 */
245 private void updateStatusBar() {
246 int state = StatusBarManager.DISABLE_NONE;
247
248 if (!mIsExpandedViewEnabled) {
249 state |= StatusBarManager.DISABLE_EXPAND;
250 }
251 if (!mIsNotificationEnabled) {
252 state |= StatusBarManager.DISABLE_NOTIFICATION_ALERTS;
253 }
254 if (!mIsSystemBarNavigationEnabled) {
255 // Disable *all* possible navigation via the system bar.
256 state |= StatusBarManager.DISABLE_HOME;
257 state |= StatusBarManager.DISABLE_RECENT;
258 state |= StatusBarManager.DISABLE_BACK;
259 }
260
261 if (DBG) log("updateStatusBar: state = 0x" + Integer.toHexString(state));
262 mStatusBarManager.disable(state);
263 }
264 }
265
266 /**
267 * Makes sure phone-related notifications are up to date on a
268 * freshly-booted device.
269 */
270 private void updateNotificationsAtStartup() {
271 if (DBG) log("updateNotificationsAtStartup()...");
272
273 // instantiate query handler
274 mQueryHandler = new QueryHandler(mContext.getContentResolver());
275
276 // setup query spec, look for all Missed calls that are new.
277 StringBuilder where = new StringBuilder("type=");
278 where.append(Calls.MISSED_TYPE);
279 where.append(" AND new=1");
280
281 // start the query
282 if (DBG) log("- start call log query...");
283 mQueryHandler.startQuery(CALL_LOG_TOKEN, null, Calls.CONTENT_URI, CALL_LOG_PROJECTION,
284 where.toString(), null, Calls.DEFAULT_SORT_ORDER);
285
286 // Update (or cancel) the in-call notification
287 if (DBG) log("- updating in-call notification at startup...");
288 updateInCallNotification();
289
290 // Depend on android.app.StatusBarManager to be set to
291 // disable(DISABLE_NONE) upon startup. This will be the
292 // case even if the phone app crashes.
293 }
294
295 /** The projection to use when querying the phones table */
296 static final String[] PHONES_PROJECTION = new String[] {
297 PhoneLookup.NUMBER,
298 PhoneLookup.DISPLAY_NAME,
299 PhoneLookup._ID
300 };
301
302 /**
303 * Class used to run asynchronous queries to re-populate the notifications we care about.
304 * There are really 3 steps to this:
305 * 1. Find the list of missed calls
306 * 2. For each call, run a query to retrieve the caller's name.
307 * 3. For each caller, try obtaining photo.
308 */
309 private class QueryHandler extends AsyncQueryHandler
310 implements ContactsAsyncHelper.OnImageLoadCompleteListener {
311
312 /**
313 * Used to store relevant fields for the Missed Call
314 * notifications.
315 */
316 private class NotificationInfo {
317 public String name;
318 public String number;
319 public int presentation;
320 /**
321 * Type of the call. {@link android.provider.CallLog.Calls#INCOMING_TYPE}
322 * {@link android.provider.CallLog.Calls#OUTGOING_TYPE}, or
323 * {@link android.provider.CallLog.Calls#MISSED_TYPE}.
324 */
325 public String type;
326 public long date;
327 }
328
329 public QueryHandler(ContentResolver cr) {
330 super(cr);
331 }
332
333 /**
334 * Handles the query results.
335 */
336 @Override
337 protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
338 // TODO: it would be faster to use a join here, but for the purposes
339 // of this small record set, it should be ok.
340
341 // Note that CursorJoiner is not useable here because the number
342 // comparisons are not strictly equals; the comparisons happen in
343 // the SQL function PHONE_NUMBERS_EQUAL, which is not available for
344 // the CursorJoiner.
345
346 // Executing our own query is also feasible (with a join), but that
347 // will require some work (possibly destabilizing) in Contacts
348 // Provider.
349
350 // At this point, we will execute subqueries on each row just as
351 // CallLogActivity.java does.
352 switch (token) {
353 case CALL_LOG_TOKEN:
354 if (DBG) log("call log query complete.");
355
356 // initial call to retrieve the call list.
357 if (cursor != null) {
358 while (cursor.moveToNext()) {
359 // for each call in the call log list, create
360 // the notification object and query contacts
361 NotificationInfo n = getNotificationInfo (cursor);
362
363 if (DBG) log("query contacts for number: " + n.number);
364
365 mQueryHandler.startQuery(CONTACT_TOKEN, n,
366 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, n.number),
367 PHONES_PROJECTION, null, null, PhoneLookup.NUMBER);
368 }
369
370 if (DBG) log("closing call log cursor.");
371 cursor.close();
372 }
373 break;
374 case CONTACT_TOKEN:
375 if (DBG) log("contact query complete.");
376
377 // subqueries to get the caller name.
378 if ((cursor != null) && (cookie != null)){
379 NotificationInfo n = (NotificationInfo) cookie;
380
381 Uri personUri = null;
382 if (cursor.moveToFirst()) {
383 n.name = cursor.getString(
384 cursor.getColumnIndexOrThrow(PhoneLookup.DISPLAY_NAME));
385 long person_id = cursor.getLong(
386 cursor.getColumnIndexOrThrow(PhoneLookup._ID));
387 if (DBG) {
388 log("contact :" + n.name + " found for phone: " + n.number
389 + ". id : " + person_id);
390 }
391 personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, person_id);
392 }
393
394 if (personUri != null) {
395 if (DBG) {
396 log("Start obtaining picture for the missed call. Uri: "
397 + personUri);
398 }
399 // Now try to obtain a photo for this person.
400 // ContactsAsyncHelper will do that and call onImageLoadComplete()
401 // after that.
402 ContactsAsyncHelper.startObtainPhotoAsync(
403 0, mContext, personUri, this, n);
404 } else {
405 if (DBG) {
406 log("Failed to find Uri for obtaining photo."
407 + " Just send notification without it.");
408 }
409 // We couldn't find person Uri, so we're sure we cannot obtain a photo.
410 // Call notifyMissedCall() right now.
411 notifyMissedCall(n.name, n.number, n.type, null, null, n.date);
412 }
413
414 if (DBG) log("closing contact cursor.");
415 cursor.close();
416 }
417 break;
418 default:
419 }
420 }
421
422 @Override
423 public void onImageLoadComplete(
424 int token, Drawable photo, Bitmap photoIcon, Object cookie) {
425 if (DBG) log("Finished loading image: " + photo);
426 NotificationInfo n = (NotificationInfo) cookie;
427 notifyMissedCall(n.name, n.number, n.type, photo, photoIcon, n.date);
428 }
429
430 /**
431 * Factory method to generate a NotificationInfo object given a
432 * cursor from the call log table.
433 */
434 private final NotificationInfo getNotificationInfo(Cursor cursor) {
435 NotificationInfo n = new NotificationInfo();
436 n.name = null;
437 n.number = cursor.getString(cursor.getColumnIndexOrThrow(Calls.NUMBER));
438 n.presentation = cursor.getInt(cursor.getColumnIndexOrThrow(Calls.NUMBER_PRESENTATION));
439 n.type = cursor.getString(cursor.getColumnIndexOrThrow(Calls.TYPE));
440 n.date = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DATE));
441
442 // make sure we update the number depending upon saved values in
443 // CallLog.addCall(). If either special values for unknown or
444 // private number are detected, we need to hand off the message
445 // to the missed call notification.
446 if (n.presentation != Calls.PRESENTATION_ALLOWED) {
447 n.number = null;
448 }
449
450 if (DBG) log("NotificationInfo constructed for number: " + n.number);
451
452 return n;
453 }
454 }
455
456 /**
457 * Configures a Notification to emit the blinky green message-waiting/
458 * missed-call signal.
459 */
460 private static void configureLedNotification(Notification note) {
461 note.flags |= Notification.FLAG_SHOW_LIGHTS;
462 note.defaults |= Notification.DEFAULT_LIGHTS;
463 }
464
465 /**
466 * Displays a notification about a missed call.
467 *
468 * @param name the contact name.
469 * @param number the phone number. Note that this may be a non-callable String like "Unknown",
470 * or "Private Number", which possibly come from methods like
471 * {@link PhoneUtils#modifyForSpecialCnapCases(Context, CallerInfo, String, int)}.
472 * @param type the type of the call. {@link android.provider.CallLog.Calls#INCOMING_TYPE}
473 * {@link android.provider.CallLog.Calls#OUTGOING_TYPE}, or
474 * {@link android.provider.CallLog.Calls#MISSED_TYPE}
475 * @param photo picture which may be used for the notification (when photoIcon is null).
476 * This also can be null when the picture itself isn't available. If photoIcon is available
477 * it should be prioritized (because this may be too huge for notification).
478 * See also {@link ContactsAsyncHelper}.
479 * @param photoIcon picture which should be used for the notification. Can be null. This is
480 * the most suitable for {@link android.app.Notification.Builder#setLargeIcon(Bitmap)}, this
481 * should be used when non-null.
482 * @param date the time when the missed call happened
483 */
484 /* package */ void notifyMissedCall(
485 String name, String number, String type, Drawable photo, Bitmap photoIcon, long date) {
486
487 // When the user clicks this notification, we go to the call log.
Yorke Leeca6ec3b2013-08-29 14:21:43 -0700488 final PendingIntent pendingCallLogIntent = PhoneGlobals.createPendingCallLogIntent(
489 mContext);
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700490
491 // Never display the missed call notification on non-voice-capable
492 // devices, even if the device does somehow manage to get an
493 // incoming call.
494 if (!PhoneGlobals.sVoiceCapable) {
495 if (DBG) log("notifyMissedCall: non-voice-capable device, not posting notification");
496 return;
497 }
498
499 if (VDBG) {
500 log("notifyMissedCall(). name: " + name + ", number: " + number
501 + ", label: " + type + ", photo: " + photo + ", photoIcon: " + photoIcon
502 + ", date: " + date);
503 }
504
505 // title resource id
506 int titleResId;
507 // the text in the notification's line 1 and 2.
508 String expandedText, callName;
509
510 // increment number of missed calls.
511 mNumberMissedCalls++;
512
513 // get the name for the ticker text
514 // i.e. "Missed call from <caller name or number>"
515 if (name != null && TextUtils.isGraphic(name)) {
516 callName = name;
517 } else if (!TextUtils.isEmpty(number)){
Yorke Lee528bd1e2013-09-04 15:21:56 -0700518 final BidiFormatter bidiFormatter = BidiFormatter.getInstance();
519 // A number should always be displayed LTR using {@link BidiFormatter}
520 // regardless of the content of the rest of the notification.
521 callName = bidiFormatter.unicodeWrap(number, TextDirectionHeuristics.LTR);
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700522 } else {
523 // use "unknown" if the caller is unidentifiable.
524 callName = mContext.getString(R.string.unknown);
525 }
526
527 // display the first line of the notification:
528 // 1 missed call: call name
529 // more than 1 missed call: <number of calls> + "missed calls"
530 if (mNumberMissedCalls == 1) {
531 titleResId = R.string.notification_missedCallTitle;
532 expandedText = callName;
533 } else {
534 titleResId = R.string.notification_missedCallsTitle;
535 expandedText = mContext.getString(R.string.notification_missedCallsMsg,
536 mNumberMissedCalls);
537 }
538
539 Notification.Builder builder = new Notification.Builder(mContext);
540 builder.setSmallIcon(android.R.drawable.stat_notify_missed_call)
541 .setTicker(mContext.getString(R.string.notification_missedCallTicker, callName))
542 .setWhen(date)
543 .setContentTitle(mContext.getText(titleResId))
544 .setContentText(expandedText)
Yorke Leeca6ec3b2013-08-29 14:21:43 -0700545 .setContentIntent(pendingCallLogIntent)
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700546 .setAutoCancel(true)
547 .setDeleteIntent(createClearMissedCallsIntent());
548
549 // Simple workaround for issue 6476275; refrain having actions when the given number seems
550 // not a real one but a non-number which was embedded by methods outside (like
551 // PhoneUtils#modifyForSpecialCnapCases()).
552 // TODO: consider removing equals() checks here, and modify callers of this method instead.
553 if (mNumberMissedCalls == 1
554 && !TextUtils.isEmpty(number)
555 && !TextUtils.equals(number, mContext.getString(R.string.private_num))
556 && !TextUtils.equals(number, mContext.getString(R.string.unknown))){
557 if (DBG) log("Add actions with the number " + number);
558
559 builder.addAction(R.drawable.stat_sys_phone_call,
560 mContext.getString(R.string.notification_missedCall_call_back),
561 PhoneGlobals.getCallBackPendingIntent(mContext, number));
562
563 builder.addAction(R.drawable.ic_text_holo_dark,
564 mContext.getString(R.string.notification_missedCall_message),
565 PhoneGlobals.getSendSmsFromNotificationPendingIntent(mContext, number));
566
567 if (photoIcon != null) {
568 builder.setLargeIcon(photoIcon);
569 } else if (photo instanceof BitmapDrawable) {
570 builder.setLargeIcon(((BitmapDrawable) photo).getBitmap());
571 }
572 } else {
573 if (DBG) {
574 log("Suppress actions. number: " + number + ", missedCalls: " + mNumberMissedCalls);
575 }
576 }
577
578 Notification notification = builder.getNotification();
579 configureLedNotification(notification);
580 mNotificationManager.notify(MISSED_CALL_NOTIFICATION, notification);
581 }
582
583 /** Returns an intent to be invoked when the missed call notification is cleared. */
584 private PendingIntent createClearMissedCallsIntent() {
585 Intent intent = new Intent(mContext, ClearMissedCallsService.class);
586 intent.setAction(ClearMissedCallsService.ACTION_CLEAR_MISSED_CALLS);
587 return PendingIntent.getService(mContext, 0, intent, 0);
588 }
589
590 /**
591 * Cancels the "missed call" notification.
592 *
593 * @see ITelephony.cancelMissedCallsNotification()
594 */
595 void cancelMissedCallNotification() {
596 // reset the number of missed calls to 0.
597 mNumberMissedCalls = 0;
598 mNotificationManager.cancel(MISSED_CALL_NOTIFICATION);
599 }
600
601 private void notifySpeakerphone() {
602 if (!mShowingSpeakerphoneIcon) {
603 mStatusBarManager.setIcon("speakerphone", android.R.drawable.stat_sys_speakerphone, 0,
604 mContext.getString(R.string.accessibility_speakerphone_enabled));
605 mShowingSpeakerphoneIcon = true;
606 }
607 }
608
609 private void cancelSpeakerphone() {
610 if (mShowingSpeakerphoneIcon) {
611 mStatusBarManager.removeIcon("speakerphone");
612 mShowingSpeakerphoneIcon = false;
613 }
614 }
615
616 /**
617 * Shows or hides the "speakerphone" notification in the status bar,
618 * based on the actual current state of the speaker.
619 *
620 * If you already know the current speaker state (e.g. if you just
621 * called AudioManager.setSpeakerphoneOn() yourself) then you should
622 * directly call {@link #updateSpeakerNotification(boolean)} instead.
623 *
624 * (But note that the status bar icon is *never* shown while the in-call UI
625 * is active; it only appears if you bail out to some other activity.)
626 */
627 private void updateSpeakerNotification() {
628 AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
629 boolean showNotification =
630 (mPhone.getState() == PhoneConstants.State.OFFHOOK) && audioManager.isSpeakerphoneOn();
631
632 if (DBG) log(showNotification
633 ? "updateSpeakerNotification: speaker ON"
634 : "updateSpeakerNotification: speaker OFF (or not offhook)");
635
636 updateSpeakerNotification(showNotification);
637 }
638
639 /**
640 * Shows or hides the "speakerphone" notification in the status bar.
641 *
642 * @param showNotification if true, call notifySpeakerphone();
643 * if false, call cancelSpeakerphone().
644 *
645 * Use {@link updateSpeakerNotification()} to update the status bar
646 * based on the actual current state of the speaker.
647 *
648 * (But note that the status bar icon is *never* shown while the in-call UI
649 * is active; it only appears if you bail out to some other activity.)
650 */
651 public void updateSpeakerNotification(boolean showNotification) {
652 if (DBG) log("updateSpeakerNotification(" + showNotification + ")...");
653
654 // Regardless of the value of the showNotification param, suppress
655 // the status bar icon if the the InCallScreen is the foreground
656 // activity, since the in-call UI already provides an onscreen
657 // indication of the speaker state. (This reduces clutter in the
658 // status bar.)
659 if (mApp.isShowingCallScreen()) {
660 cancelSpeakerphone();
661 return;
662 }
663
664 if (showNotification) {
665 notifySpeakerphone();
666 } else {
667 cancelSpeakerphone();
668 }
669 }
670
671 private void notifyMute() {
672 if (!mShowingMuteIcon) {
673 mStatusBarManager.setIcon("mute", android.R.drawable.stat_notify_call_mute, 0,
674 mContext.getString(R.string.accessibility_call_muted));
675 mShowingMuteIcon = true;
676 }
677 }
678
679 private void cancelMute() {
680 if (mShowingMuteIcon) {
681 mStatusBarManager.removeIcon("mute");
682 mShowingMuteIcon = false;
683 }
684 }
685
686 /**
687 * Shows or hides the "mute" notification in the status bar,
688 * based on the current mute state of the Phone.
689 *
690 * (But note that the status bar icon is *never* shown while the in-call UI
691 * is active; it only appears if you bail out to some other activity.)
692 */
693 void updateMuteNotification() {
694 // Suppress the status bar icon if the the InCallScreen is the
695 // foreground activity, since the in-call UI already provides an
696 // onscreen indication of the mute state. (This reduces clutter
697 // in the status bar.)
698 if (mApp.isShowingCallScreen()) {
699 cancelMute();
700 return;
701 }
702
703 if ((mCM.getState() == PhoneConstants.State.OFFHOOK) && PhoneUtils.getMute()) {
704 if (DBG) log("updateMuteNotification: MUTED");
705 notifyMute();
706 } else {
707 if (DBG) log("updateMuteNotification: not muted (or not offhook)");
708 cancelMute();
709 }
710 }
711
712 /**
713 * Updates the phone app's status bar notification based on the
714 * current telephony state, or cancels the notification if the phone
715 * is totally idle.
716 *
717 * This method will never actually launch the incoming-call UI.
718 * (Use updateNotificationAndLaunchIncomingCallUi() for that.)
719 */
720 public void updateInCallNotification() {
721 // allowFullScreenIntent=false means *don't* allow the incoming
722 // call UI to be launched.
723 updateInCallNotification(false);
724 }
725
726 /**
727 * Updates the phone app's status bar notification *and* launches the
728 * incoming call UI in response to a new incoming call.
729 *
730 * This is just like updateInCallNotification(), with one exception:
731 * If an incoming call is ringing (or call-waiting), the notification
732 * will also include a "fullScreenIntent" that will cause the
733 * InCallScreen to be launched immediately, unless the current
734 * foreground activity is marked as "immersive".
735 *
736 * (This is the mechanism that actually brings up the incoming call UI
737 * when we receive a "new ringing connection" event from the telephony
738 * layer.)
739 *
740 * Watch out: this method should ONLY be called directly from the code
741 * path in CallNotifier that handles the "new ringing connection"
742 * event from the telephony layer. All other places that update the
743 * in-call notification (like for phone state changes) should call
744 * updateInCallNotification() instead. (This ensures that we don't
745 * end up launching the InCallScreen multiple times for a single
746 * incoming call, which could cause slow responsiveness and/or visible
747 * glitches.)
748 *
749 * Also note that this method is safe to call even if the phone isn't
750 * actually ringing (or, more likely, if an incoming call *was*
751 * ringing briefly but then disconnected). In that case, we'll simply
752 * update or cancel the in-call notification based on the current
753 * phone state.
754 *
755 * @see #updateInCallNotification(boolean)
756 */
757 public void updateNotificationAndLaunchIncomingCallUi() {
758 // Set allowFullScreenIntent=true to indicate that we *should*
759 // launch the incoming call UI if necessary.
760 updateInCallNotification(true);
761 }
762
763 /**
764 * Helper method for updateInCallNotification() and
765 * updateNotificationAndLaunchIncomingCallUi(): Update the phone app's
766 * status bar notification based on the current telephony state, or
767 * cancels the notification if the phone is totally idle.
768 *
769 * @param allowFullScreenIntent If true, *and* an incoming call is
770 * ringing, the notification will include a "fullScreenIntent"
771 * pointing at the InCallScreen (which will cause the InCallScreen
772 * to be launched.)
773 * Watch out: This should be set to true *only* when directly
774 * handling the "new ringing connection" event from the telephony
775 * layer (see updateNotificationAndLaunchIncomingCallUi().)
776 */
777 private void updateInCallNotification(boolean allowFullScreenIntent) {
778 int resId;
779 if (DBG) log("updateInCallNotification(allowFullScreenIntent = "
780 + allowFullScreenIntent + ")...");
781
782 // Never display the "ongoing call" notification on
783 // non-voice-capable devices, even if the phone is actually
784 // offhook (like during a non-interactive OTASP call.)
785 if (!PhoneGlobals.sVoiceCapable) {
786 if (DBG) log("- non-voice-capable device; suppressing notification.");
787 return;
788 }
789
790 // If the phone is idle, completely clean up all call-related
791 // notifications.
792 if (mCM.getState() == PhoneConstants.State.IDLE) {
793 cancelInCall();
794 cancelMute();
795 cancelSpeakerphone();
796 return;
797 }
798
799 final boolean hasRingingCall = mCM.hasActiveRingingCall();
800 final boolean hasActiveCall = mCM.hasActiveFgCall();
801 final boolean hasHoldingCall = mCM.hasActiveBgCall();
802 if (DBG) {
803 log(" - hasRingingCall = " + hasRingingCall);
804 log(" - hasActiveCall = " + hasActiveCall);
805 log(" - hasHoldingCall = " + hasHoldingCall);
806 }
807
808 // Suppress the in-call notification if the InCallScreen is the
809 // foreground activity, since it's already obvious that you're on a
810 // call. (The status bar icon is needed only if you navigate *away*
811 // from the in-call UI.)
812 boolean suppressNotification = mApp.isShowingCallScreen();
813 // if (DBG) log("- suppressNotification: initial value: " + suppressNotification);
814
815 // ...except for a couple of cases where we *never* suppress the
816 // notification:
817 //
818 // - If there's an incoming ringing call: always show the
819 // notification, since the in-call notification is what actually
820 // launches the incoming call UI in the first place (see
821 // notification.fullScreenIntent below.) This makes sure that we'll
822 // correctly handle the case where a new incoming call comes in but
823 // the InCallScreen is already in the foreground.
824 if (hasRingingCall) suppressNotification = false;
825
826 // - If "voice privacy" mode is active: always show the notification,
827 // since that's the only "voice privacy" indication we have.
828 boolean enhancedVoicePrivacy = mApp.notifier.getVoicePrivacyState();
829 // if (DBG) log("updateInCallNotification: enhancedVoicePrivacy = " + enhancedVoicePrivacy);
830 if (enhancedVoicePrivacy) suppressNotification = false;
831
832 if (suppressNotification) {
833 if (DBG) log("- suppressNotification = true; reducing clutter in status bar...");
834 cancelInCall();
835 // Suppress the mute and speaker status bar icons too
836 // (also to reduce clutter in the status bar.)
837 cancelSpeakerphone();
838 cancelMute();
839 return;
840 }
841
842 // Display the appropriate icon in the status bar,
843 // based on the current phone and/or bluetooth state.
844
845 if (hasRingingCall) {
846 // There's an incoming ringing call.
847 resId = R.drawable.stat_sys_phone_call;
848 } else if (!hasActiveCall && hasHoldingCall) {
849 // There's only one call, and it's on hold.
850 if (enhancedVoicePrivacy) {
851 resId = R.drawable.stat_sys_vp_phone_call_on_hold;
852 } else {
853 resId = R.drawable.stat_sys_phone_call_on_hold;
854 }
855 } else {
856 if (enhancedVoicePrivacy) {
857 resId = R.drawable.stat_sys_vp_phone_call;
858 } else {
859 resId = R.drawable.stat_sys_phone_call;
860 }
861 }
862
863 // Note we can't just bail out now if (resId == mInCallResId),
864 // since even if the status icon hasn't changed, some *other*
865 // notification-related info may be different from the last time
866 // we were here (like the caller-id info of the foreground call,
867 // if the user swapped calls...)
868
869 if (DBG) log("- Updating status bar icon: resId = " + resId);
870 mInCallResId = resId;
871
872 // Even if both lines are in use, we only show a single item in
873 // the expanded Notifications UI. It's labeled "Ongoing call"
874 // (or "On hold" if there's only one call, and it's on hold.)
875 // Also, we don't have room to display caller-id info from two
876 // different calls. So if both lines are in use, display info
877 // from the foreground call. And if there's a ringing call,
878 // display that regardless of the state of the other calls.
879
880 Call currentCall;
881 if (hasRingingCall) {
882 currentCall = mCM.getFirstActiveRingingCall();
883 } else if (hasActiveCall) {
884 currentCall = mCM.getActiveFgCall();
885 } else {
886 currentCall = mCM.getFirstActiveBgCall();
887 }
888 Connection currentConn = currentCall.getEarliestConnection();
889
890 final Notification.Builder builder = new Notification.Builder(mContext);
891 builder.setSmallIcon(mInCallResId).setOngoing(true);
892
893 // PendingIntent that can be used to launch the InCallScreen. The
894 // system fires off this intent if the user pulls down the windowshade
895 // and clicks the notification's expanded view. It's also used to
896 // launch the InCallScreen immediately when when there's an incoming
897 // call (see the "fullScreenIntent" field below).
898 PendingIntent inCallPendingIntent =
899 PendingIntent.getActivity(mContext, 0,
900 PhoneGlobals.createInCallIntent(), 0);
901 builder.setContentIntent(inCallPendingIntent);
902
903 // Update icon on the left of the notification.
904 // - If it is directly available from CallerInfo, we'll just use that.
905 // - If it is not, use the same icon as in the status bar.
906 CallerInfo callerInfo = null;
907 if (currentConn != null) {
908 Object o = currentConn.getUserData();
909 if (o instanceof CallerInfo) {
910 callerInfo = (CallerInfo) o;
911 } else if (o instanceof PhoneUtils.CallerInfoToken) {
912 callerInfo = ((PhoneUtils.CallerInfoToken) o).currentInfo;
913 } else {
914 Log.w(LOG_TAG, "CallerInfo isn't available while Call object is available.");
915 }
916 }
917 boolean largeIconWasSet = false;
918 if (callerInfo != null) {
919 // In most cases, the user will see the notification after CallerInfo is already
920 // available, so photo will be available from this block.
921 if (callerInfo.isCachedPhotoCurrent) {
922 // .. and in that case CallerInfo's cachedPhotoIcon should also be available.
923 // If it happens not, then try using cachedPhoto, assuming Drawable coming from
924 // ContactProvider will be BitmapDrawable.
925 if (callerInfo.cachedPhotoIcon != null) {
926 builder.setLargeIcon(callerInfo.cachedPhotoIcon);
927 largeIconWasSet = true;
928 } else if (callerInfo.cachedPhoto instanceof BitmapDrawable) {
929 if (DBG) log("- BitmapDrawable found for large icon");
930 Bitmap bitmap = ((BitmapDrawable) callerInfo.cachedPhoto).getBitmap();
931 builder.setLargeIcon(bitmap);
932 largeIconWasSet = true;
933 } else {
934 if (DBG) {
935 log("- Failed to fetch icon from CallerInfo's cached photo."
936 + " (cachedPhotoIcon: " + callerInfo.cachedPhotoIcon
937 + ", cachedPhoto: " + callerInfo.cachedPhoto + ")."
938 + " Ignore it.");
939 }
940 }
941 }
942
943 if (!largeIconWasSet && callerInfo.photoResource > 0) {
944 if (DBG) {
945 log("- BitmapDrawable nor person Id not found for large icon."
946 + " Use photoResource: " + callerInfo.photoResource);
947 }
948 Drawable drawable =
949 mContext.getResources().getDrawable(callerInfo.photoResource);
950 if (drawable instanceof BitmapDrawable) {
951 Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
952 builder.setLargeIcon(bitmap);
953 largeIconWasSet = true;
954 } else {
955 if (DBG) {
956 log("- PhotoResource was found but it didn't return BitmapDrawable."
957 + " Ignore it");
958 }
959 }
960 }
961 } else {
962 if (DBG) log("- CallerInfo not found. Use the same icon as in the status bar.");
963 }
964
965 // Failed to fetch Bitmap.
966 if (!largeIconWasSet && DBG) {
967 log("- No useful Bitmap was found for the photo."
968 + " Use the same icon as in the status bar.");
969 }
970
971 // If the connection is valid, then build what we need for the
972 // content text of notification, and start the chronometer.
973 // Otherwise, don't bother and just stick with content title.
974 if (currentConn != null) {
975 if (DBG) log("- Updating context text and chronometer.");
976 if (hasRingingCall) {
977 // Incoming call is ringing.
978 builder.setContentText(mContext.getString(R.string.notification_incoming_call));
979 builder.setUsesChronometer(false);
980 } else if (hasHoldingCall && !hasActiveCall) {
981 // Only one call, and it's on hold.
982 builder.setContentText(mContext.getString(R.string.notification_on_hold));
983 builder.setUsesChronometer(false);
984 } else {
985 // We show the elapsed time of the current call using Chronometer.
986 builder.setUsesChronometer(true);
987
988 // Determine the "start time" of the current connection.
989 // We can't use currentConn.getConnectTime(), because (1) that's
990 // in the currentTimeMillis() time base, and (2) it's zero when
991 // the phone first goes off hook, since the getConnectTime counter
992 // doesn't start until the DIALING -> ACTIVE transition.
993 // Instead we start with the current connection's duration,
994 // and translate that into the elapsedRealtime() timebase.
995 long callDurationMsec = currentConn.getDurationMillis();
996 builder.setWhen(System.currentTimeMillis() - callDurationMsec);
997
998 int contextTextId = R.string.notification_ongoing_call;
999
1000 Call call = mCM.getActiveFgCall();
1001 if (TelephonyCapabilities.canDistinguishDialingAndConnected(
1002 call.getPhone().getPhoneType()) && call.isDialingOrAlerting()) {
1003 contextTextId = R.string.notification_dialing;
1004 }
1005
1006 builder.setContentText(mContext.getString(contextTextId));
1007 }
1008 } else if (DBG) {
1009 Log.w(LOG_TAG, "updateInCallNotification: null connection, can't set exp view line 1.");
1010 }
1011
1012 // display conference call string if this call is a conference
1013 // call, otherwise display the connection information.
1014
1015 // Line 2 of the expanded view (smaller text). This is usually a
1016 // contact name or phone number.
1017 String expandedViewLine2 = "";
1018 // TODO: it may not make sense for every point to make separate
1019 // checks for isConferenceCall, so we need to think about
1020 // possibly including this in startGetCallerInfo or some other
1021 // common point.
1022 if (PhoneUtils.isConferenceCall(currentCall)) {
1023 // if this is a conference call, just use that as the caller name.
1024 expandedViewLine2 = mContext.getString(R.string.card_title_conf_call);
1025 } else {
1026 // If necessary, start asynchronous query to do the caller-id lookup.
1027 PhoneUtils.CallerInfoToken cit =
1028 PhoneUtils.startGetCallerInfo(mContext, currentCall, this, this);
1029 expandedViewLine2 = PhoneUtils.getCompactNameFromCallerInfo(cit.currentInfo, mContext);
1030 // Note: For an incoming call, the very first time we get here we
1031 // won't have a contact name yet, since we only just started the
1032 // caller-id query. So expandedViewLine2 will start off as a raw
1033 // phone number, but we'll update it very quickly when the query
1034 // completes (see onQueryComplete() below.)
1035 }
1036
1037 if (DBG) log("- Updating expanded view: line 2 '" + /*expandedViewLine2*/ "xxxxxxx" + "'");
1038 builder.setContentTitle(expandedViewLine2);
1039
1040 // TODO: We also need to *update* this notification in some cases,
1041 // like when a call ends on one line but the other is still in use
1042 // (ie. make sure the caller info here corresponds to the active
1043 // line), and maybe even when the user swaps calls (ie. if we only
1044 // show info here for the "current active call".)
1045
1046 // Activate a couple of special Notification features if an
1047 // incoming call is ringing:
1048 if (hasRingingCall) {
1049 if (DBG) log("- Using hi-pri notification for ringing call!");
1050
1051 // This is a high-priority event that should be shown even if the
1052 // status bar is hidden or if an immersive activity is running.
1053 builder.setPriority(Notification.PRIORITY_HIGH);
1054
1055 // If an immersive activity is running, we have room for a single
1056 // line of text in the small notification popup window.
1057 // We use expandedViewLine2 for this (i.e. the name or number of
1058 // the incoming caller), since that's more relevant than
1059 // expandedViewLine1 (which is something generic like "Incoming
1060 // call".)
1061 builder.setTicker(expandedViewLine2);
1062
1063 if (allowFullScreenIntent) {
1064 // Ok, we actually want to launch the incoming call
1065 // UI at this point (in addition to simply posting a notification
1066 // to the status bar). Setting fullScreenIntent will cause
1067 // the InCallScreen to be launched immediately *unless* the
1068 // current foreground activity is marked as "immersive".
1069 if (DBG) log("- Setting fullScreenIntent: " + inCallPendingIntent);
1070 builder.setFullScreenIntent(inCallPendingIntent, true);
1071
1072 // Ugly hack alert:
1073 //
1074 // The NotificationManager has the (undocumented) behavior
1075 // that it will *ignore* the fullScreenIntent field if you
1076 // post a new Notification that matches the ID of one that's
1077 // already active. Unfortunately this is exactly what happens
1078 // when you get an incoming call-waiting call: the
1079 // "ongoing call" notification is already visible, so the
1080 // InCallScreen won't get launched in this case!
1081 // (The result: if you bail out of the in-call UI while on a
1082 // call and then get a call-waiting call, the incoming call UI
1083 // won't come up automatically.)
1084 //
1085 // The workaround is to just notice this exact case (this is a
1086 // call-waiting call *and* the InCallScreen is not in the
1087 // foreground) and manually cancel the in-call notification
1088 // before (re)posting it.
1089 //
1090 // TODO: there should be a cleaner way of avoiding this
1091 // problem (see discussion in bug 3184149.)
1092 Call ringingCall = mCM.getFirstActiveRingingCall();
1093 if ((ringingCall.getState() == Call.State.WAITING) && !mApp.isShowingCallScreen()) {
1094 Log.i(LOG_TAG, "updateInCallNotification: call-waiting! force relaunch...");
1095 // Cancel the IN_CALL_NOTIFICATION immediately before
1096 // (re)posting it; this seems to force the
1097 // NotificationManager to launch the fullScreenIntent.
1098 mNotificationManager.cancel(IN_CALL_NOTIFICATION);
1099 }
1100 }
1101 } else { // not ringing call
1102 // Make the notification prioritized over the other normal notifications.
1103 builder.setPriority(Notification.PRIORITY_HIGH);
1104
1105 // TODO: use "if (DBG)" for this comment.
1106 log("Will show \"hang-up\" action in the ongoing active call Notification");
1107 // TODO: use better asset.
1108 builder.addAction(R.drawable.stat_sys_phone_call_end,
1109 mContext.getText(R.string.notification_action_end_call),
1110 PhoneGlobals.createHangUpOngoingCallPendingIntent(mContext));
1111 }
1112
1113 Notification notification = builder.getNotification();
1114 if (DBG) log("Notifying IN_CALL_NOTIFICATION: " + notification);
Chiao Cheng7f7c6522013-07-16 18:39:35 -07001115 // TODO(klp): not needed anymore. Possibly delete this and move notification to incallui.
1116 //mNotificationManager.notify(IN_CALL_NOTIFICATION, notification);
Santos Cordon7d4ddf62013-07-10 11:58:08 -07001117
1118 // Finally, refresh the mute and speakerphone notifications (since
1119 // some phone state changes can indirectly affect the mute and/or
1120 // speaker state).
1121 updateSpeakerNotification();
1122 updateMuteNotification();
1123 }
1124
1125 /**
1126 * Implemented for CallerInfoAsyncQuery.OnQueryCompleteListener interface.
1127 * refreshes the contentView when called.
1128 */
1129 @Override
1130 public void onQueryComplete(int token, Object cookie, CallerInfo ci){
1131 if (DBG) log("CallerInfo query complete (for NotificationMgr), "
1132 + "updating in-call notification..");
1133 if (DBG) log("- cookie: " + cookie);
1134 if (DBG) log("- ci: " + ci);
1135
1136 if (cookie == this) {
1137 // Ok, this is the caller-id query we fired off in
1138 // updateInCallNotification(), presumably when an incoming call
1139 // first appeared. If the caller-id info matched any contacts,
1140 // compactName should now be a real person name rather than a raw
1141 // phone number:
1142 if (DBG) log("- compactName is now: "
1143 + PhoneUtils.getCompactNameFromCallerInfo(ci, mContext));
1144
1145 // Now that our CallerInfo object has been fully filled-in,
1146 // refresh the in-call notification.
1147 if (DBG) log("- updating notification after query complete...");
1148 updateInCallNotification();
1149 } else {
1150 Log.w(LOG_TAG, "onQueryComplete: caller-id query from unknown source! "
1151 + "cookie = " + cookie);
1152 }
1153 }
1154
1155 /**
1156 * Take down the in-call notification.
1157 * @see updateInCallNotification()
1158 */
1159 private void cancelInCall() {
1160 if (DBG) log("cancelInCall()...");
1161 mNotificationManager.cancel(IN_CALL_NOTIFICATION);
1162 mInCallResId = 0;
1163 }
1164
1165 /**
1166 * Completely take down the in-call notification *and* the mute/speaker
1167 * notifications as well, to indicate that the phone is now idle.
1168 */
1169 /* package */ void cancelCallInProgressNotifications() {
1170 if (DBG) log("cancelCallInProgressNotifications()...");
1171 if (mInCallResId == 0) {
1172 return;
1173 }
1174
1175 if (DBG) log("cancelCallInProgressNotifications: " + mInCallResId);
1176 cancelInCall();
1177 cancelMute();
1178 cancelSpeakerphone();
1179 }
1180
1181 /**
1182 * Updates the message waiting indicator (voicemail) notification.
1183 *
1184 * @param visible true if there are messages waiting
1185 */
1186 /* package */ void updateMwi(boolean visible) {
1187 if (DBG) log("updateMwi(): " + visible);
1188
1189 if (visible) {
1190 int resId = android.R.drawable.stat_notify_voicemail;
1191
1192 // This Notification can get a lot fancier once we have more
1193 // information about the current voicemail messages.
1194 // (For example, the current voicemail system can't tell
1195 // us the caller-id or timestamp of a message, or tell us the
1196 // message count.)
1197
1198 // But for now, the UI is ultra-simple: if the MWI indication
1199 // is supposed to be visible, just show a single generic
1200 // notification.
1201
1202 String notificationTitle = mContext.getString(R.string.notification_voicemail_title);
1203 String vmNumber = mPhone.getVoiceMailNumber();
1204 if (DBG) log("- got vm number: '" + vmNumber + "'");
1205
1206 // Watch out: vmNumber may be null, for two possible reasons:
1207 //
1208 // (1) This phone really has no voicemail number
1209 //
1210 // (2) This phone *does* have a voicemail number, but
1211 // the SIM isn't ready yet.
1212 //
1213 // Case (2) *does* happen in practice if you have voicemail
1214 // messages when the device first boots: we get an MWI
1215 // notification as soon as we register on the network, but the
1216 // SIM hasn't finished loading yet.
1217 //
1218 // So handle case (2) by retrying the lookup after a short
1219 // delay.
1220
1221 if ((vmNumber == null) && !mPhone.getIccRecordsLoaded()) {
1222 if (DBG) log("- Null vm number: SIM records not loaded (yet)...");
1223
1224 // TODO: rather than retrying after an arbitrary delay, it
1225 // would be cleaner to instead just wait for a
1226 // SIM_RECORDS_LOADED notification.
1227 // (Unfortunately right now there's no convenient way to
1228 // get that notification in phone app code. We'd first
1229 // want to add a call like registerForSimRecordsLoaded()
1230 // to Phone.java and GSMPhone.java, and *then* we could
1231 // listen for that in the CallNotifier class.)
1232
1233 // Limit the number of retries (in case the SIM is broken
1234 // or missing and can *never* load successfully.)
1235 if (mVmNumberRetriesRemaining-- > 0) {
1236 if (DBG) log(" - Retrying in " + VM_NUMBER_RETRY_DELAY_MILLIS + " msec...");
1237 mApp.notifier.sendMwiChangedDelayed(VM_NUMBER_RETRY_DELAY_MILLIS);
1238 return;
1239 } else {
1240 Log.w(LOG_TAG, "NotificationMgr.updateMwi: getVoiceMailNumber() failed after "
1241 + MAX_VM_NUMBER_RETRIES + " retries; giving up.");
1242 // ...and continue with vmNumber==null, just as if the
1243 // phone had no VM number set up in the first place.
1244 }
1245 }
1246
1247 if (TelephonyCapabilities.supportsVoiceMessageCount(mPhone)) {
1248 int vmCount = mPhone.getVoiceMessageCount();
1249 String titleFormat = mContext.getString(R.string.notification_voicemail_title_count);
1250 notificationTitle = String.format(titleFormat, vmCount);
1251 }
1252
1253 String notificationText;
1254 if (TextUtils.isEmpty(vmNumber)) {
1255 notificationText = mContext.getString(
1256 R.string.notification_voicemail_no_vm_number);
1257 } else {
1258 notificationText = String.format(
1259 mContext.getString(R.string.notification_voicemail_text_format),
1260 PhoneNumberUtils.formatNumber(vmNumber));
1261 }
1262
1263 Intent intent = new Intent(Intent.ACTION_CALL,
1264 Uri.fromParts(Constants.SCHEME_VOICEMAIL, "", null));
1265 PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
1266
1267 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
1268 Uri ringtoneUri;
1269 String uriString = prefs.getString(
1270 CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_RINGTONE_KEY, null);
1271 if (!TextUtils.isEmpty(uriString)) {
1272 ringtoneUri = Uri.parse(uriString);
1273 } else {
1274 ringtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI;
1275 }
1276
1277 Notification.Builder builder = new Notification.Builder(mContext);
1278 builder.setSmallIcon(resId)
1279 .setWhen(System.currentTimeMillis())
1280 .setContentTitle(notificationTitle)
1281 .setContentText(notificationText)
1282 .setContentIntent(pendingIntent)
1283 .setSound(ringtoneUri);
1284 Notification notification = builder.getNotification();
1285
1286 CallFeaturesSetting.migrateVoicemailVibrationSettingsIfNeeded(prefs);
1287 final boolean vibrate = prefs.getBoolean(
1288 CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_VIBRATE_KEY, false);
1289 if (vibrate) {
1290 notification.defaults |= Notification.DEFAULT_VIBRATE;
1291 }
1292 notification.flags |= Notification.FLAG_NO_CLEAR;
1293 configureLedNotification(notification);
1294 mNotificationManager.notify(VOICEMAIL_NOTIFICATION, notification);
1295 } else {
1296 mNotificationManager.cancel(VOICEMAIL_NOTIFICATION);
1297 }
1298 }
1299
1300 /**
1301 * Updates the message call forwarding indicator notification.
1302 *
1303 * @param visible true if there are messages waiting
1304 */
1305 /* package */ void updateCfi(boolean visible) {
1306 if (DBG) log("updateCfi(): " + visible);
1307 if (visible) {
1308 // If Unconditional Call Forwarding (forward all calls) for VOICE
1309 // is enabled, just show a notification. We'll default to expanded
1310 // view for now, so the there is less confusion about the icon. If
1311 // it is deemed too weird to have CF indications as expanded views,
1312 // then we'll flip the flag back.
1313
1314 // TODO: We may want to take a look to see if the notification can
1315 // display the target to forward calls to. This will require some
1316 // effort though, since there are multiple layers of messages that
1317 // will need to propagate that information.
1318
1319 Notification notification;
1320 final boolean showExpandedNotification = true;
1321 if (showExpandedNotification) {
1322 Intent intent = new Intent(Intent.ACTION_MAIN);
1323 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1324 intent.setClassName("com.android.phone",
1325 "com.android.phone.CallFeaturesSetting");
1326
1327 notification = new Notification(
1328 R.drawable.stat_sys_phone_call_forward, // icon
1329 null, // tickerText
1330 0); // The "timestamp" of this notification is meaningless;
1331 // we only care about whether CFI is currently on or not.
1332 notification.setLatestEventInfo(
1333 mContext, // context
1334 mContext.getString(R.string.labelCF), // expandedTitle
1335 mContext.getString(R.string.sum_cfu_enabled_indicator), // expandedText
1336 PendingIntent.getActivity(mContext, 0, intent, 0)); // contentIntent
1337 } else {
1338 notification = new Notification(
1339 R.drawable.stat_sys_phone_call_forward, // icon
1340 null, // tickerText
1341 System.currentTimeMillis() // when
1342 );
1343 }
1344
1345 notification.flags |= Notification.FLAG_ONGOING_EVENT; // also implies FLAG_NO_CLEAR
1346
1347 mNotificationManager.notify(
1348 CALL_FORWARD_NOTIFICATION,
1349 notification);
1350 } else {
1351 mNotificationManager.cancel(CALL_FORWARD_NOTIFICATION);
1352 }
1353 }
1354
1355 /**
1356 * Shows the "data disconnected due to roaming" notification, which
1357 * appears when you lose data connectivity because you're roaming and
1358 * you have the "data roaming" feature turned off.
1359 */
1360 /* package */ void showDataDisconnectedRoaming() {
1361 if (DBG) log("showDataDisconnectedRoaming()...");
1362
1363 // "Mobile network settings" screen / dialog
1364 Intent intent = new Intent(mContext, com.android.phone.MobileNetworkSettings.class);
1365
1366 final CharSequence contentText = mContext.getText(R.string.roaming_reenable_message);
1367
1368 final Notification.Builder builder = new Notification.Builder(mContext);
1369 builder.setSmallIcon(android.R.drawable.stat_sys_warning);
1370 builder.setContentTitle(mContext.getText(R.string.roaming));
1371 builder.setContentText(contentText);
1372 builder.setContentIntent(PendingIntent.getActivity(mContext, 0, intent, 0));
1373
1374 final Notification notif = new Notification.BigTextStyle(builder).bigText(contentText)
1375 .build();
1376
1377 mNotificationManager.notify(DATA_DISCONNECTED_ROAMING_NOTIFICATION, notif);
1378 }
1379
1380 /**
1381 * Turns off the "data disconnected due to roaming" notification.
1382 */
1383 /* package */ void hideDataDisconnectedRoaming() {
1384 if (DBG) log("hideDataDisconnectedRoaming()...");
1385 mNotificationManager.cancel(DATA_DISCONNECTED_ROAMING_NOTIFICATION);
1386 }
1387
1388 /**
1389 * Display the network selection "no service" notification
1390 * @param operator is the numeric operator number
1391 */
1392 private void showNetworkSelection(String operator) {
1393 if (DBG) log("showNetworkSelection(" + operator + ")...");
1394
1395 String titleText = mContext.getString(
1396 R.string.notification_network_selection_title);
1397 String expandedText = mContext.getString(
1398 R.string.notification_network_selection_text, operator);
1399
1400 Notification notification = new Notification();
1401 notification.icon = android.R.drawable.stat_sys_warning;
1402 notification.when = 0;
1403 notification.flags = Notification.FLAG_ONGOING_EVENT;
1404 notification.tickerText = null;
1405
1406 // create the target network operators settings intent
1407 Intent intent = new Intent(Intent.ACTION_MAIN);
1408 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
1409 Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
1410 // Use NetworkSetting to handle the selection intent
1411 intent.setComponent(new ComponentName("com.android.phone",
1412 "com.android.phone.NetworkSetting"));
1413 PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0);
1414
1415 notification.setLatestEventInfo(mContext, titleText, expandedText, pi);
1416
1417 mNotificationManager.notify(SELECTED_OPERATOR_FAIL_NOTIFICATION, notification);
1418 }
1419
1420 /**
1421 * Turn off the network selection "no service" notification
1422 */
1423 private void cancelNetworkSelection() {
1424 if (DBG) log("cancelNetworkSelection()...");
1425 mNotificationManager.cancel(SELECTED_OPERATOR_FAIL_NOTIFICATION);
1426 }
1427
1428 /**
1429 * Update notification about no service of user selected operator
1430 *
1431 * @param serviceState Phone service state
1432 */
1433 void updateNetworkSelection(int serviceState) {
1434 if (TelephonyCapabilities.supportsNetworkSelection(mPhone)) {
1435 // get the shared preference of network_selection.
1436 // empty is auto mode, otherwise it is the operator alpha name
1437 // in case there is no operator name, check the operator numeric
1438 SharedPreferences sp =
1439 PreferenceManager.getDefaultSharedPreferences(mContext);
1440 String networkSelection =
1441 sp.getString(PhoneBase.NETWORK_SELECTION_NAME_KEY, "");
1442 if (TextUtils.isEmpty(networkSelection)) {
1443 networkSelection =
1444 sp.getString(PhoneBase.NETWORK_SELECTION_KEY, "");
1445 }
1446
1447 if (DBG) log("updateNetworkSelection()..." + "state = " +
1448 serviceState + " new network " + networkSelection);
1449
1450 if (serviceState == ServiceState.STATE_OUT_OF_SERVICE
1451 && !TextUtils.isEmpty(networkSelection)) {
1452 if (!mSelectedUnavailableNotify) {
1453 showNetworkSelection(networkSelection);
1454 mSelectedUnavailableNotify = true;
1455 }
1456 } else {
1457 if (mSelectedUnavailableNotify) {
1458 cancelNetworkSelection();
1459 mSelectedUnavailableNotify = false;
1460 }
1461 }
1462 }
1463 }
1464
1465 /* package */ void postTransientNotification(int notifyId, CharSequence msg) {
1466 if (mToast != null) {
1467 mToast.cancel();
1468 }
1469
1470 mToast = Toast.makeText(mContext, msg, Toast.LENGTH_LONG);
1471 mToast.show();
1472 }
1473
1474 private void log(String msg) {
1475 Log.d(LOG_TAG, msg);
1476 }
1477}