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