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