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