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