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