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