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