blob: 5c33ab4ae754fd6847a89947a1713ed770bc1cd8 [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;
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700106 private Toast mToast;
107 private boolean mShowingSpeakerphoneIcon;
108 private boolean mShowingMuteIcon;
109
110 public StatusBarHelper statusBarHelper;
111
112 // used to track the missed call counter, default to 0.
113 private int mNumberMissedCalls = 0;
114
Chiao Cheng2ed66512013-09-15 18:17:23 -0700115 private boolean mHasInCallNotification = false;
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700116
117 // used to track the notification of selected network unavailable
118 private boolean mSelectedUnavailableNotify = false;
119
120 // Retry params for the getVoiceMailNumber() call; see updateMwi().
121 private static final int MAX_VM_NUMBER_RETRIES = 5;
122 private static final int VM_NUMBER_RETRY_DELAY_MILLIS = 10000;
123 private int mVmNumberRetriesRemaining = MAX_VM_NUMBER_RETRIES;
124
125 // Query used to look up caller-id info for the "call log" notification.
126 private QueryHandler mQueryHandler = null;
127 private static final int CALL_LOG_TOKEN = -1;
128 private static final int CONTACT_TOKEN = -2;
129
130 /**
131 * Private constructor (this is a singleton).
132 * @see init()
133 */
134 private NotificationMgr(PhoneGlobals app) {
135 mApp = app;
136 mContext = app;
137 mNotificationManager =
138 (NotificationManager) app.getSystemService(Context.NOTIFICATION_SERVICE);
139 mStatusBarManager =
140 (StatusBarManager) app.getSystemService(Context.STATUS_BAR_SERVICE);
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700141 mPhone = app.phone; // TODO: better style to use mCM.getDefaultPhone() everywhere instead
142 mCM = app.mCM;
143 statusBarHelper = new StatusBarHelper();
144 }
145
146 /**
147 * Initialize the singleton NotificationMgr instance.
148 *
149 * This is only done once, at startup, from PhoneApp.onCreate().
150 * From then on, the NotificationMgr instance is available via the
151 * PhoneApp's public "notificationMgr" field, which is why there's no
152 * getInstance() method here.
153 */
154 /* package */ static NotificationMgr init(PhoneGlobals app) {
155 synchronized (NotificationMgr.class) {
156 if (sInstance == null) {
157 sInstance = new NotificationMgr(app);
158 // Update the notifications that need to be touched at startup.
159 sInstance.updateNotificationsAtStartup();
160 } else {
161 Log.wtf(LOG_TAG, "init() called multiple times! sInstance = " + sInstance);
162 }
163 return sInstance;
164 }
165 }
166
167 /**
168 * Helper class that's a wrapper around the framework's
169 * StatusBarManager.disable() API.
170 *
171 * This class is used to control features like:
172 *
173 * - Disabling the status bar "notification windowshade"
174 * while the in-call UI is up
175 *
176 * - Disabling notification alerts (audible or vibrating)
177 * while a phone call is active
178 *
179 * - Disabling navigation via the system bar (the "soft buttons" at
180 * the bottom of the screen on devices with no hard buttons)
181 *
182 * We control these features through a single point of control to make
183 * sure that the various StatusBarManager.disable() calls don't
184 * interfere with each other.
185 */
186 public class StatusBarHelper {
187 // Current desired state of status bar / system bar behavior
188 private boolean mIsNotificationEnabled = true;
189 private boolean mIsExpandedViewEnabled = true;
190 private boolean mIsSystemBarNavigationEnabled = true;
191
192 private StatusBarHelper () {
193 }
194
195 /**
196 * Enables or disables auditory / vibrational alerts.
197 *
198 * (We disable these any time a voice call is active, regardless
199 * of whether or not the in-call UI is visible.)
200 */
201 public void enableNotificationAlerts(boolean enable) {
202 if (mIsNotificationEnabled != enable) {
203 mIsNotificationEnabled = enable;
204 updateStatusBar();
205 }
206 }
207
208 /**
209 * Enables or disables the expanded view of the status bar
210 * (i.e. the ability to pull down the "notification windowshade").
211 *
212 * (This feature is disabled by the InCallScreen while the in-call
213 * UI is active.)
214 */
215 public void enableExpandedView(boolean enable) {
216 if (mIsExpandedViewEnabled != enable) {
217 mIsExpandedViewEnabled = enable;
218 updateStatusBar();
219 }
220 }
221
222 /**
223 * Enables or disables the navigation via the system bar (the
224 * "soft buttons" at the bottom of the screen)
225 *
226 * (This feature is disabled while an incoming call is ringing,
227 * because it's easy to accidentally touch the system bar while
228 * pulling the phone out of your pocket.)
229 */
230 public void enableSystemBarNavigation(boolean enable) {
231 if (mIsSystemBarNavigationEnabled != enable) {
232 mIsSystemBarNavigationEnabled = enable;
233 updateStatusBar();
234 }
235 }
236
237 /**
238 * Updates the status bar to reflect the current desired state.
239 */
240 private void updateStatusBar() {
241 int state = StatusBarManager.DISABLE_NONE;
242
243 if (!mIsExpandedViewEnabled) {
244 state |= StatusBarManager.DISABLE_EXPAND;
245 }
246 if (!mIsNotificationEnabled) {
247 state |= StatusBarManager.DISABLE_NOTIFICATION_ALERTS;
248 }
249 if (!mIsSystemBarNavigationEnabled) {
250 // Disable *all* possible navigation via the system bar.
251 state |= StatusBarManager.DISABLE_HOME;
252 state |= StatusBarManager.DISABLE_RECENT;
253 state |= StatusBarManager.DISABLE_BACK;
254 }
255
256 if (DBG) log("updateStatusBar: state = 0x" + Integer.toHexString(state));
257 mStatusBarManager.disable(state);
258 }
259 }
260
261 /**
262 * Makes sure phone-related notifications are up to date on a
263 * freshly-booted device.
264 */
265 private void updateNotificationsAtStartup() {
266 if (DBG) log("updateNotificationsAtStartup()...");
267
268 // instantiate query handler
269 mQueryHandler = new QueryHandler(mContext.getContentResolver());
270
271 // setup query spec, look for all Missed calls that are new.
272 StringBuilder where = new StringBuilder("type=");
273 where.append(Calls.MISSED_TYPE);
274 where.append(" AND new=1");
275
276 // start the query
277 if (DBG) log("- start call log query...");
278 mQueryHandler.startQuery(CALL_LOG_TOKEN, null, Calls.CONTENT_URI, CALL_LOG_PROJECTION,
279 where.toString(), null, Calls.DEFAULT_SORT_ORDER);
280
281 // Update (or cancel) the in-call notification
282 if (DBG) log("- updating in-call notification at startup...");
283 updateInCallNotification();
284
285 // Depend on android.app.StatusBarManager to be set to
286 // disable(DISABLE_NONE) upon startup. This will be the
287 // case even if the phone app crashes.
288 }
289
290 /** The projection to use when querying the phones table */
291 static final String[] PHONES_PROJECTION = new String[] {
292 PhoneLookup.NUMBER,
293 PhoneLookup.DISPLAY_NAME,
294 PhoneLookup._ID
295 };
296
297 /**
298 * Class used to run asynchronous queries to re-populate the notifications we care about.
299 * There are really 3 steps to this:
300 * 1. Find the list of missed calls
301 * 2. For each call, run a query to retrieve the caller's name.
302 * 3. For each caller, try obtaining photo.
303 */
304 private class QueryHandler extends AsyncQueryHandler
305 implements ContactsAsyncHelper.OnImageLoadCompleteListener {
306
307 /**
308 * Used to store relevant fields for the Missed Call
309 * notifications.
310 */
311 private class NotificationInfo {
312 public String name;
313 public String number;
314 public int presentation;
315 /**
316 * Type of the call. {@link android.provider.CallLog.Calls#INCOMING_TYPE}
317 * {@link android.provider.CallLog.Calls#OUTGOING_TYPE}, or
318 * {@link android.provider.CallLog.Calls#MISSED_TYPE}.
319 */
320 public String type;
321 public long date;
322 }
323
324 public QueryHandler(ContentResolver cr) {
325 super(cr);
326 }
327
328 /**
329 * Handles the query results.
330 */
331 @Override
332 protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
333 // TODO: it would be faster to use a join here, but for the purposes
334 // of this small record set, it should be ok.
335
336 // Note that CursorJoiner is not useable here because the number
337 // comparisons are not strictly equals; the comparisons happen in
338 // the SQL function PHONE_NUMBERS_EQUAL, which is not available for
339 // the CursorJoiner.
340
341 // Executing our own query is also feasible (with a join), but that
342 // will require some work (possibly destabilizing) in Contacts
343 // Provider.
344
345 // At this point, we will execute subqueries on each row just as
346 // CallLogActivity.java does.
347 switch (token) {
348 case CALL_LOG_TOKEN:
349 if (DBG) log("call log query complete.");
350
351 // initial call to retrieve the call list.
352 if (cursor != null) {
353 while (cursor.moveToNext()) {
354 // for each call in the call log list, create
355 // the notification object and query contacts
356 NotificationInfo n = getNotificationInfo (cursor);
357
358 if (DBG) log("query contacts for number: " + n.number);
359
360 mQueryHandler.startQuery(CONTACT_TOKEN, n,
361 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, n.number),
362 PHONES_PROJECTION, null, null, PhoneLookup.NUMBER);
363 }
364
365 if (DBG) log("closing call log cursor.");
366 cursor.close();
367 }
368 break;
369 case CONTACT_TOKEN:
370 if (DBG) log("contact query complete.");
371
372 // subqueries to get the caller name.
373 if ((cursor != null) && (cookie != null)){
374 NotificationInfo n = (NotificationInfo) cookie;
375
376 Uri personUri = null;
377 if (cursor.moveToFirst()) {
378 n.name = cursor.getString(
379 cursor.getColumnIndexOrThrow(PhoneLookup.DISPLAY_NAME));
380 long person_id = cursor.getLong(
381 cursor.getColumnIndexOrThrow(PhoneLookup._ID));
382 if (DBG) {
383 log("contact :" + n.name + " found for phone: " + n.number
384 + ". id : " + person_id);
385 }
386 personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, person_id);
387 }
388
389 if (personUri != null) {
390 if (DBG) {
391 log("Start obtaining picture for the missed call. Uri: "
392 + personUri);
393 }
394 // Now try to obtain a photo for this person.
395 // ContactsAsyncHelper will do that and call onImageLoadComplete()
396 // after that.
397 ContactsAsyncHelper.startObtainPhotoAsync(
398 0, mContext, personUri, this, n);
399 } else {
400 if (DBG) {
401 log("Failed to find Uri for obtaining photo."
402 + " Just send notification without it.");
403 }
404 // We couldn't find person Uri, so we're sure we cannot obtain a photo.
405 // Call notifyMissedCall() right now.
406 notifyMissedCall(n.name, n.number, n.type, null, null, n.date);
407 }
408
409 if (DBG) log("closing contact cursor.");
410 cursor.close();
411 }
412 break;
413 default:
414 }
415 }
416
417 @Override
418 public void onImageLoadComplete(
419 int token, Drawable photo, Bitmap photoIcon, Object cookie) {
420 if (DBG) log("Finished loading image: " + photo);
421 NotificationInfo n = (NotificationInfo) cookie;
422 notifyMissedCall(n.name, n.number, n.type, photo, photoIcon, n.date);
423 }
424
425 /**
426 * Factory method to generate a NotificationInfo object given a
427 * cursor from the call log table.
428 */
429 private final NotificationInfo getNotificationInfo(Cursor cursor) {
430 NotificationInfo n = new NotificationInfo();
431 n.name = null;
432 n.number = cursor.getString(cursor.getColumnIndexOrThrow(Calls.NUMBER));
433 n.presentation = cursor.getInt(cursor.getColumnIndexOrThrow(Calls.NUMBER_PRESENTATION));
434 n.type = cursor.getString(cursor.getColumnIndexOrThrow(Calls.TYPE));
435 n.date = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DATE));
436
437 // make sure we update the number depending upon saved values in
438 // CallLog.addCall(). If either special values for unknown or
439 // private number are detected, we need to hand off the message
440 // to the missed call notification.
441 if (n.presentation != Calls.PRESENTATION_ALLOWED) {
442 n.number = null;
443 }
444
445 if (DBG) log("NotificationInfo constructed for number: " + n.number);
446
447 return n;
448 }
449 }
450
451 /**
452 * Configures a Notification to emit the blinky green message-waiting/
453 * missed-call signal.
454 */
455 private static void configureLedNotification(Notification note) {
456 note.flags |= Notification.FLAG_SHOW_LIGHTS;
457 note.defaults |= Notification.DEFAULT_LIGHTS;
458 }
459
460 /**
461 * Displays a notification about a missed call.
462 *
463 * @param name the contact name.
464 * @param number the phone number. Note that this may be a non-callable String like "Unknown",
465 * or "Private Number", which possibly come from methods like
466 * {@link PhoneUtils#modifyForSpecialCnapCases(Context, CallerInfo, String, int)}.
467 * @param type the type of the call. {@link android.provider.CallLog.Calls#INCOMING_TYPE}
468 * {@link android.provider.CallLog.Calls#OUTGOING_TYPE}, or
469 * {@link android.provider.CallLog.Calls#MISSED_TYPE}
470 * @param photo picture which may be used for the notification (when photoIcon is null).
471 * This also can be null when the picture itself isn't available. If photoIcon is available
472 * it should be prioritized (because this may be too huge for notification).
473 * See also {@link ContactsAsyncHelper}.
474 * @param photoIcon picture which should be used for the notification. Can be null. This is
475 * the most suitable for {@link android.app.Notification.Builder#setLargeIcon(Bitmap)}, this
476 * should be used when non-null.
477 * @param date the time when the missed call happened
478 */
479 /* package */ void notifyMissedCall(
480 String name, String number, String type, Drawable photo, Bitmap photoIcon, long date) {
481
482 // When the user clicks this notification, we go to the call log.
Yorke Leeca6ec3b2013-08-29 14:21:43 -0700483 final PendingIntent pendingCallLogIntent = PhoneGlobals.createPendingCallLogIntent(
484 mContext);
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700485
486 // Never display the missed call notification on non-voice-capable
487 // devices, even if the device does somehow manage to get an
488 // incoming call.
489 if (!PhoneGlobals.sVoiceCapable) {
490 if (DBG) log("notifyMissedCall: non-voice-capable device, not posting notification");
491 return;
492 }
493
494 if (VDBG) {
495 log("notifyMissedCall(). name: " + name + ", number: " + number
496 + ", label: " + type + ", photo: " + photo + ", photoIcon: " + photoIcon
497 + ", date: " + date);
498 }
499
500 // title resource id
501 int titleResId;
502 // the text in the notification's line 1 and 2.
503 String expandedText, callName;
504
505 // increment number of missed calls.
506 mNumberMissedCalls++;
507
508 // get the name for the ticker text
509 // i.e. "Missed call from <caller name or number>"
510 if (name != null && TextUtils.isGraphic(name)) {
511 callName = name;
512 } else if (!TextUtils.isEmpty(number)){
Yorke Lee528bd1e2013-09-04 15:21:56 -0700513 final BidiFormatter bidiFormatter = BidiFormatter.getInstance();
514 // A number should always be displayed LTR using {@link BidiFormatter}
515 // regardless of the content of the rest of the notification.
516 callName = bidiFormatter.unicodeWrap(number, TextDirectionHeuristics.LTR);
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700517 } 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) {
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700773 if (DBG) log("updateInCallNotification(allowFullScreenIntent = "
774 + allowFullScreenIntent + ")...");
775
776 // Never display the "ongoing call" notification on
777 // non-voice-capable devices, even if the phone is actually
778 // offhook (like during a non-interactive OTASP call.)
779 if (!PhoneGlobals.sVoiceCapable) {
780 if (DBG) log("- non-voice-capable device; suppressing notification.");
781 return;
782 }
783
784 // If the phone is idle, completely clean up all call-related
785 // notifications.
786 if (mCM.getState() == PhoneConstants.State.IDLE) {
787 cancelInCall();
788 cancelMute();
789 cancelSpeakerphone();
790 return;
791 }
792
793 final boolean hasRingingCall = mCM.hasActiveRingingCall();
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700794 if (DBG) {
795 log(" - hasRingingCall = " + hasRingingCall);
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700796 }
797
798 // Suppress the in-call notification if the InCallScreen is the
799 // foreground activity, since it's already obvious that you're on a
800 // call. (The status bar icon is needed only if you navigate *away*
801 // from the in-call UI.)
802 boolean suppressNotification = mApp.isShowingCallScreen();
803 // if (DBG) log("- suppressNotification: initial value: " + suppressNotification);
804
805 // ...except for a couple of cases where we *never* suppress the
806 // notification:
807 //
808 // - If there's an incoming ringing call: always show the
809 // notification, since the in-call notification is what actually
810 // launches the incoming call UI in the first place (see
811 // notification.fullScreenIntent below.) This makes sure that we'll
812 // correctly handle the case where a new incoming call comes in but
813 // the InCallScreen is already in the foreground.
814 if (hasRingingCall) suppressNotification = false;
815
816 // - If "voice privacy" mode is active: always show the notification,
817 // since that's the only "voice privacy" indication we have.
818 boolean enhancedVoicePrivacy = mApp.notifier.getVoicePrivacyState();
819 // if (DBG) log("updateInCallNotification: enhancedVoicePrivacy = " + enhancedVoicePrivacy);
820 if (enhancedVoicePrivacy) suppressNotification = false;
821
822 if (suppressNotification) {
823 if (DBG) log("- suppressNotification = true; reducing clutter in status bar...");
824 cancelInCall();
825 // Suppress the mute and speaker status bar icons too
826 // (also to reduce clutter in the status bar.)
827 cancelSpeakerphone();
828 cancelMute();
829 return;
830 }
831
Chiao Cheng2ed66512013-09-15 18:17:23 -0700832 mHasInCallNotification = true;
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700833
834 // Activate a couple of special Notification features if an
835 // incoming call is ringing:
836 if (hasRingingCall) {
837 if (DBG) log("- Using hi-pri notification for ringing call!");
838
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700839 if (allowFullScreenIntent) {
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700840
841 // Ugly hack alert:
842 //
843 // The NotificationManager has the (undocumented) behavior
844 // that it will *ignore* the fullScreenIntent field if you
845 // post a new Notification that matches the ID of one that's
846 // already active. Unfortunately this is exactly what happens
847 // when you get an incoming call-waiting call: the
848 // "ongoing call" notification is already visible, so the
849 // InCallScreen won't get launched in this case!
850 // (The result: if you bail out of the in-call UI while on a
851 // call and then get a call-waiting call, the incoming call UI
852 // won't come up automatically.)
853 //
854 // The workaround is to just notice this exact case (this is a
855 // call-waiting call *and* the InCallScreen is not in the
856 // foreground) and manually cancel the in-call notification
857 // before (re)posting it.
858 //
859 // TODO: there should be a cleaner way of avoiding this
860 // problem (see discussion in bug 3184149.)
861 Call ringingCall = mCM.getFirstActiveRingingCall();
862 if ((ringingCall.getState() == Call.State.WAITING) && !mApp.isShowingCallScreen()) {
863 Log.i(LOG_TAG, "updateInCallNotification: call-waiting! force relaunch...");
864 // Cancel the IN_CALL_NOTIFICATION immediately before
865 // (re)posting it; this seems to force the
866 // NotificationManager to launch the fullScreenIntent.
867 mNotificationManager.cancel(IN_CALL_NOTIFICATION);
868 }
869 }
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700870 }
871
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700872 // Finally, refresh the mute and speakerphone notifications (since
873 // some phone state changes can indirectly affect the mute and/or
874 // speaker state).
875 updateSpeakerNotification();
876 updateMuteNotification();
877 }
878
879 /**
880 * Implemented for CallerInfoAsyncQuery.OnQueryCompleteListener interface.
881 * refreshes the contentView when called.
882 */
883 @Override
884 public void onQueryComplete(int token, Object cookie, CallerInfo ci){
885 if (DBG) log("CallerInfo query complete (for NotificationMgr), "
886 + "updating in-call notification..");
887 if (DBG) log("- cookie: " + cookie);
888 if (DBG) log("- ci: " + ci);
889
890 if (cookie == this) {
891 // Ok, this is the caller-id query we fired off in
892 // updateInCallNotification(), presumably when an incoming call
893 // first appeared. If the caller-id info matched any contacts,
894 // compactName should now be a real person name rather than a raw
895 // phone number:
896 if (DBG) log("- compactName is now: "
897 + PhoneUtils.getCompactNameFromCallerInfo(ci, mContext));
898
899 // Now that our CallerInfo object has been fully filled-in,
900 // refresh the in-call notification.
901 if (DBG) log("- updating notification after query complete...");
902 updateInCallNotification();
903 } else {
904 Log.w(LOG_TAG, "onQueryComplete: caller-id query from unknown source! "
905 + "cookie = " + cookie);
906 }
907 }
908
909 /**
910 * Take down the in-call notification.
911 * @see updateInCallNotification()
912 */
913 private void cancelInCall() {
914 if (DBG) log("cancelInCall()...");
915 mNotificationManager.cancel(IN_CALL_NOTIFICATION);
Chiao Cheng2ed66512013-09-15 18:17:23 -0700916 mHasInCallNotification = false;
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700917 }
918
919 /**
920 * Completely take down the in-call notification *and* the mute/speaker
921 * notifications as well, to indicate that the phone is now idle.
922 */
923 /* package */ void cancelCallInProgressNotifications() {
924 if (DBG) log("cancelCallInProgressNotifications()...");
Chiao Cheng2ed66512013-09-15 18:17:23 -0700925 if (!mHasInCallNotification) {
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700926 return;
927 }
928
Chiao Cheng2ed66512013-09-15 18:17:23 -0700929 if (DBG) log("cancelCallInProgressNotifications");
Santos Cordon7d4ddf62013-07-10 11:58:08 -0700930 cancelInCall();
931 cancelMute();
932 cancelSpeakerphone();
933 }
934
935 /**
936 * Updates the message waiting indicator (voicemail) notification.
937 *
938 * @param visible true if there are messages waiting
939 */
940 /* package */ void updateMwi(boolean visible) {
941 if (DBG) log("updateMwi(): " + visible);
942
943 if (visible) {
944 int resId = android.R.drawable.stat_notify_voicemail;
945
946 // This Notification can get a lot fancier once we have more
947 // information about the current voicemail messages.
948 // (For example, the current voicemail system can't tell
949 // us the caller-id or timestamp of a message, or tell us the
950 // message count.)
951
952 // But for now, the UI is ultra-simple: if the MWI indication
953 // is supposed to be visible, just show a single generic
954 // notification.
955
956 String notificationTitle = mContext.getString(R.string.notification_voicemail_title);
957 String vmNumber = mPhone.getVoiceMailNumber();
958 if (DBG) log("- got vm number: '" + vmNumber + "'");
959
960 // Watch out: vmNumber may be null, for two possible reasons:
961 //
962 // (1) This phone really has no voicemail number
963 //
964 // (2) This phone *does* have a voicemail number, but
965 // the SIM isn't ready yet.
966 //
967 // Case (2) *does* happen in practice if you have voicemail
968 // messages when the device first boots: we get an MWI
969 // notification as soon as we register on the network, but the
970 // SIM hasn't finished loading yet.
971 //
972 // So handle case (2) by retrying the lookup after a short
973 // delay.
974
975 if ((vmNumber == null) && !mPhone.getIccRecordsLoaded()) {
976 if (DBG) log("- Null vm number: SIM records not loaded (yet)...");
977
978 // TODO: rather than retrying after an arbitrary delay, it
979 // would be cleaner to instead just wait for a
980 // SIM_RECORDS_LOADED notification.
981 // (Unfortunately right now there's no convenient way to
982 // get that notification in phone app code. We'd first
983 // want to add a call like registerForSimRecordsLoaded()
984 // to Phone.java and GSMPhone.java, and *then* we could
985 // listen for that in the CallNotifier class.)
986
987 // Limit the number of retries (in case the SIM is broken
988 // or missing and can *never* load successfully.)
989 if (mVmNumberRetriesRemaining-- > 0) {
990 if (DBG) log(" - Retrying in " + VM_NUMBER_RETRY_DELAY_MILLIS + " msec...");
991 mApp.notifier.sendMwiChangedDelayed(VM_NUMBER_RETRY_DELAY_MILLIS);
992 return;
993 } else {
994 Log.w(LOG_TAG, "NotificationMgr.updateMwi: getVoiceMailNumber() failed after "
995 + MAX_VM_NUMBER_RETRIES + " retries; giving up.");
996 // ...and continue with vmNumber==null, just as if the
997 // phone had no VM number set up in the first place.
998 }
999 }
1000
1001 if (TelephonyCapabilities.supportsVoiceMessageCount(mPhone)) {
1002 int vmCount = mPhone.getVoiceMessageCount();
1003 String titleFormat = mContext.getString(R.string.notification_voicemail_title_count);
1004 notificationTitle = String.format(titleFormat, vmCount);
1005 }
1006
1007 String notificationText;
1008 if (TextUtils.isEmpty(vmNumber)) {
1009 notificationText = mContext.getString(
1010 R.string.notification_voicemail_no_vm_number);
1011 } else {
1012 notificationText = String.format(
1013 mContext.getString(R.string.notification_voicemail_text_format),
1014 PhoneNumberUtils.formatNumber(vmNumber));
1015 }
1016
1017 Intent intent = new Intent(Intent.ACTION_CALL,
1018 Uri.fromParts(Constants.SCHEME_VOICEMAIL, "", null));
1019 PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
1020
1021 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
1022 Uri ringtoneUri;
1023 String uriString = prefs.getString(
1024 CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_RINGTONE_KEY, null);
1025 if (!TextUtils.isEmpty(uriString)) {
1026 ringtoneUri = Uri.parse(uriString);
1027 } else {
1028 ringtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI;
1029 }
1030
1031 Notification.Builder builder = new Notification.Builder(mContext);
1032 builder.setSmallIcon(resId)
1033 .setWhen(System.currentTimeMillis())
1034 .setContentTitle(notificationTitle)
1035 .setContentText(notificationText)
1036 .setContentIntent(pendingIntent)
1037 .setSound(ringtoneUri);
1038 Notification notification = builder.getNotification();
1039
1040 CallFeaturesSetting.migrateVoicemailVibrationSettingsIfNeeded(prefs);
1041 final boolean vibrate = prefs.getBoolean(
1042 CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_VIBRATE_KEY, false);
1043 if (vibrate) {
1044 notification.defaults |= Notification.DEFAULT_VIBRATE;
1045 }
1046 notification.flags |= Notification.FLAG_NO_CLEAR;
1047 configureLedNotification(notification);
1048 mNotificationManager.notify(VOICEMAIL_NOTIFICATION, notification);
1049 } else {
1050 mNotificationManager.cancel(VOICEMAIL_NOTIFICATION);
1051 }
1052 }
1053
1054 /**
1055 * Updates the message call forwarding indicator notification.
1056 *
1057 * @param visible true if there are messages waiting
1058 */
1059 /* package */ void updateCfi(boolean visible) {
1060 if (DBG) log("updateCfi(): " + visible);
1061 if (visible) {
1062 // If Unconditional Call Forwarding (forward all calls) for VOICE
1063 // is enabled, just show a notification. We'll default to expanded
1064 // view for now, so the there is less confusion about the icon. If
1065 // it is deemed too weird to have CF indications as expanded views,
1066 // then we'll flip the flag back.
1067
1068 // TODO: We may want to take a look to see if the notification can
1069 // display the target to forward calls to. This will require some
1070 // effort though, since there are multiple layers of messages that
1071 // will need to propagate that information.
1072
1073 Notification notification;
1074 final boolean showExpandedNotification = true;
1075 if (showExpandedNotification) {
1076 Intent intent = new Intent(Intent.ACTION_MAIN);
1077 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1078 intent.setClassName("com.android.phone",
1079 "com.android.phone.CallFeaturesSetting");
1080
1081 notification = new Notification(
1082 R.drawable.stat_sys_phone_call_forward, // icon
1083 null, // tickerText
1084 0); // The "timestamp" of this notification is meaningless;
1085 // we only care about whether CFI is currently on or not.
1086 notification.setLatestEventInfo(
1087 mContext, // context
1088 mContext.getString(R.string.labelCF), // expandedTitle
1089 mContext.getString(R.string.sum_cfu_enabled_indicator), // expandedText
1090 PendingIntent.getActivity(mContext, 0, intent, 0)); // contentIntent
1091 } else {
1092 notification = new Notification(
1093 R.drawable.stat_sys_phone_call_forward, // icon
1094 null, // tickerText
1095 System.currentTimeMillis() // when
1096 );
1097 }
1098
1099 notification.flags |= Notification.FLAG_ONGOING_EVENT; // also implies FLAG_NO_CLEAR
1100
1101 mNotificationManager.notify(
1102 CALL_FORWARD_NOTIFICATION,
1103 notification);
1104 } else {
1105 mNotificationManager.cancel(CALL_FORWARD_NOTIFICATION);
1106 }
1107 }
1108
1109 /**
1110 * Shows the "data disconnected due to roaming" notification, which
1111 * appears when you lose data connectivity because you're roaming and
1112 * you have the "data roaming" feature turned off.
1113 */
1114 /* package */ void showDataDisconnectedRoaming() {
1115 if (DBG) log("showDataDisconnectedRoaming()...");
1116
1117 // "Mobile network settings" screen / dialog
1118 Intent intent = new Intent(mContext, com.android.phone.MobileNetworkSettings.class);
1119
1120 final CharSequence contentText = mContext.getText(R.string.roaming_reenable_message);
1121
1122 final Notification.Builder builder = new Notification.Builder(mContext);
1123 builder.setSmallIcon(android.R.drawable.stat_sys_warning);
1124 builder.setContentTitle(mContext.getText(R.string.roaming));
1125 builder.setContentText(contentText);
1126 builder.setContentIntent(PendingIntent.getActivity(mContext, 0, intent, 0));
1127
1128 final Notification notif = new Notification.BigTextStyle(builder).bigText(contentText)
1129 .build();
1130
1131 mNotificationManager.notify(DATA_DISCONNECTED_ROAMING_NOTIFICATION, notif);
1132 }
1133
1134 /**
1135 * Turns off the "data disconnected due to roaming" notification.
1136 */
1137 /* package */ void hideDataDisconnectedRoaming() {
1138 if (DBG) log("hideDataDisconnectedRoaming()...");
1139 mNotificationManager.cancel(DATA_DISCONNECTED_ROAMING_NOTIFICATION);
1140 }
1141
1142 /**
1143 * Display the network selection "no service" notification
1144 * @param operator is the numeric operator number
1145 */
1146 private void showNetworkSelection(String operator) {
1147 if (DBG) log("showNetworkSelection(" + operator + ")...");
1148
1149 String titleText = mContext.getString(
1150 R.string.notification_network_selection_title);
1151 String expandedText = mContext.getString(
1152 R.string.notification_network_selection_text, operator);
1153
1154 Notification notification = new Notification();
1155 notification.icon = android.R.drawable.stat_sys_warning;
1156 notification.when = 0;
1157 notification.flags = Notification.FLAG_ONGOING_EVENT;
1158 notification.tickerText = null;
1159
1160 // create the target network operators settings intent
1161 Intent intent = new Intent(Intent.ACTION_MAIN);
1162 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
1163 Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
1164 // Use NetworkSetting to handle the selection intent
1165 intent.setComponent(new ComponentName("com.android.phone",
1166 "com.android.phone.NetworkSetting"));
1167 PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0);
1168
1169 notification.setLatestEventInfo(mContext, titleText, expandedText, pi);
1170
1171 mNotificationManager.notify(SELECTED_OPERATOR_FAIL_NOTIFICATION, notification);
1172 }
1173
1174 /**
1175 * Turn off the network selection "no service" notification
1176 */
1177 private void cancelNetworkSelection() {
1178 if (DBG) log("cancelNetworkSelection()...");
1179 mNotificationManager.cancel(SELECTED_OPERATOR_FAIL_NOTIFICATION);
1180 }
1181
1182 /**
1183 * Update notification about no service of user selected operator
1184 *
1185 * @param serviceState Phone service state
1186 */
1187 void updateNetworkSelection(int serviceState) {
1188 if (TelephonyCapabilities.supportsNetworkSelection(mPhone)) {
1189 // get the shared preference of network_selection.
1190 // empty is auto mode, otherwise it is the operator alpha name
1191 // in case there is no operator name, check the operator numeric
1192 SharedPreferences sp =
1193 PreferenceManager.getDefaultSharedPreferences(mContext);
1194 String networkSelection =
1195 sp.getString(PhoneBase.NETWORK_SELECTION_NAME_KEY, "");
1196 if (TextUtils.isEmpty(networkSelection)) {
1197 networkSelection =
1198 sp.getString(PhoneBase.NETWORK_SELECTION_KEY, "");
1199 }
1200
1201 if (DBG) log("updateNetworkSelection()..." + "state = " +
1202 serviceState + " new network " + networkSelection);
1203
1204 if (serviceState == ServiceState.STATE_OUT_OF_SERVICE
1205 && !TextUtils.isEmpty(networkSelection)) {
1206 if (!mSelectedUnavailableNotify) {
1207 showNetworkSelection(networkSelection);
1208 mSelectedUnavailableNotify = true;
1209 }
1210 } else {
1211 if (mSelectedUnavailableNotify) {
1212 cancelNetworkSelection();
1213 mSelectedUnavailableNotify = false;
1214 }
1215 }
1216 }
1217 }
1218
1219 /* package */ void postTransientNotification(int notifyId, CharSequence msg) {
1220 if (mToast != null) {
1221 mToast.cancel();
1222 }
1223
1224 mToast = Toast.makeText(mContext, msg, Toast.LENGTH_LONG);
1225 mToast.show();
1226 }
1227
1228 private void log(String msg) {
1229 Log.d(LOG_TAG, msg);
1230 }
1231}