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 {
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 Toast mToast;
107    private boolean mShowingSpeakerphoneIcon;
108    private boolean mShowingMuteIcon;
109
110    public StatusBarHelper statusBarHelper;
111
112    // used to track the missed call counter, default to 0.
113    private int mNumberMissedCalls = 0;
114
115    // used to track the notification of selected network unavailable
116    private boolean mSelectedUnavailableNotify = false;
117
118    // Retry params for the getVoiceMailNumber() call; see updateMwi().
119    private static final int MAX_VM_NUMBER_RETRIES = 5;
120    private static final int VM_NUMBER_RETRY_DELAY_MILLIS = 10000;
121    private int mVmNumberRetriesRemaining = MAX_VM_NUMBER_RETRIES;
122
123    // Query used to look up caller-id info for the "call log" notification.
124    private QueryHandler mQueryHandler = null;
125    private static final int CALL_LOG_TOKEN = -1;
126    private static final int CONTACT_TOKEN = -2;
127
128    /**
129     * Private constructor (this is a singleton).
130     * @see init()
131     */
132    private NotificationMgr(PhoneGlobals app) {
133        mApp = app;
134        mContext = app;
135        mNotificationManager =
136                (NotificationManager) app.getSystemService(Context.NOTIFICATION_SERVICE);
137        mStatusBarManager =
138                (StatusBarManager) app.getSystemService(Context.STATUS_BAR_SERVICE);
139        mPhone = app.phone;  // TODO: better style to use mCM.getDefaultPhone() everywhere instead
140        mCM = app.mCM;
141        statusBarHelper = new StatusBarHelper();
142    }
143
144    /**
145     * Initialize the singleton NotificationMgr instance.
146     *
147     * This is only done once, at startup, from PhoneApp.onCreate().
148     * From then on, the NotificationMgr instance is available via the
149     * PhoneApp's public "notificationMgr" field, which is why there's no
150     * getInstance() method here.
151     */
152    /* package */ static NotificationMgr init(PhoneGlobals app) {
153        synchronized (NotificationMgr.class) {
154            if (sInstance == null) {
155                sInstance = new NotificationMgr(app);
156                // Update the notifications that need to be touched at startup.
157                sInstance.updateNotificationsAtStartup();
158            } else {
159                Log.wtf(LOG_TAG, "init() called multiple times!  sInstance = " + sInstance);
160            }
161            return sInstance;
162        }
163    }
164
165    /**
166     * Helper class that's a wrapper around the framework's
167     * StatusBarManager.disable() API.
168     *
169     * This class is used to control features like:
170     *
171     *   - Disabling the status bar "notification windowshade"
172     *     while the in-call UI is up
173     *
174     *   - Disabling notification alerts (audible or vibrating)
175     *     while a phone call is active
176     *
177     *   - Disabling navigation via the system bar (the "soft buttons" at
178     *     the bottom of the screen on devices with no hard buttons)
179     *
180     * We control these features through a single point of control to make
181     * sure that the various StatusBarManager.disable() calls don't
182     * interfere with each other.
183     */
184    public class StatusBarHelper {
185        // Current desired state of status bar / system bar behavior
186        private boolean mIsNotificationEnabled = true;
187        private boolean mIsExpandedViewEnabled = true;
188        private boolean mIsSystemBarNavigationEnabled = true;
189
190        private StatusBarHelper () {
191        }
192
193        /**
194         * Enables or disables auditory / vibrational alerts.
195         *
196         * (We disable these any time a voice call is active, regardless
197         * of whether or not the in-call UI is visible.)
198         */
199        public void enableNotificationAlerts(boolean enable) {
200            if (mIsNotificationEnabled != enable) {
201                mIsNotificationEnabled = enable;
202                updateStatusBar();
203            }
204        }
205
206        /**
207         * Enables or disables the expanded view of the status bar
208         * (i.e. the ability to pull down the "notification windowshade").
209         *
210         * (This feature is disabled by the InCallScreen while the in-call
211         * UI is active.)
212         */
213        public void enableExpandedView(boolean enable) {
214            if (mIsExpandedViewEnabled != enable) {
215                mIsExpandedViewEnabled = enable;
216                updateStatusBar();
217            }
218        }
219
220        /**
221         * Enables or disables the navigation via the system bar (the
222         * "soft buttons" at the bottom of the screen)
223         *
224         * (This feature is disabled while an incoming call is ringing,
225         * because it's easy to accidentally touch the system bar while
226         * pulling the phone out of your pocket.)
227         */
228        public void enableSystemBarNavigation(boolean enable) {
229            if (mIsSystemBarNavigationEnabled != enable) {
230                mIsSystemBarNavigationEnabled = enable;
231                updateStatusBar();
232            }
233        }
234
235        /**
236         * Updates the status bar to reflect the current desired state.
237         */
238        private void updateStatusBar() {
239            int state = StatusBarManager.DISABLE_NONE;
240
241            if (!mIsExpandedViewEnabled) {
242                state |= StatusBarManager.DISABLE_EXPAND;
243            }
244            if (!mIsNotificationEnabled) {
245                state |= StatusBarManager.DISABLE_NOTIFICATION_ALERTS;
246            }
247            if (!mIsSystemBarNavigationEnabled) {
248                // Disable *all* possible navigation via the system bar.
249                state |= StatusBarManager.DISABLE_HOME;
250                state |= StatusBarManager.DISABLE_RECENT;
251                state |= StatusBarManager.DISABLE_BACK;
252                state |= StatusBarManager.DISABLE_SEARCH;
253            }
254
255            if (DBG) log("updateStatusBar: state = 0x" + Integer.toHexString(state));
256            mStatusBarManager.disable(state);
257        }
258    }
259
260    /**
261     * Makes sure phone-related notifications are up to date on a
262     * freshly-booted device.
263     */
264    private void updateNotificationsAtStartup() {
265        if (DBG) log("updateNotificationsAtStartup()...");
266
267        // instantiate query handler
268        mQueryHandler = new QueryHandler(mContext.getContentResolver());
269
270        // setup query spec, look for all Missed calls that are new.
271        StringBuilder where = new StringBuilder("type=");
272        where.append(Calls.MISSED_TYPE);
273        where.append(" AND new=1");
274
275        // start the query
276        if (DBG) log("- start call log query...");
277        mQueryHandler.startQuery(CALL_LOG_TOKEN, null, Calls.CONTENT_URI,  CALL_LOG_PROJECTION,
278                where.toString(), null, Calls.DEFAULT_SORT_ORDER);
279
280        // Depend on android.app.StatusBarManager to be set to
281        // disable(DISABLE_NONE) upon startup.  This will be the
282        // case even if the phone app crashes.
283    }
284
285    /** The projection to use when querying the phones table */
286    static final String[] PHONES_PROJECTION = new String[] {
287        PhoneLookup.NUMBER,
288        PhoneLookup.DISPLAY_NAME,
289        PhoneLookup._ID
290    };
291
292    /**
293     * Class used to run asynchronous queries to re-populate the notifications we care about.
294     * There are really 3 steps to this:
295     *  1. Find the list of missed calls
296     *  2. For each call, run a query to retrieve the caller's name.
297     *  3. For each caller, try obtaining photo.
298     */
299    private class QueryHandler extends AsyncQueryHandler
300            implements ContactsAsyncHelper.OnImageLoadCompleteListener {
301
302        /**
303         * Used to store relevant fields for the Missed Call
304         * notifications.
305         */
306        private class NotificationInfo {
307            public String name;
308            public String number;
309            public int presentation;
310            /**
311             * Type of the call. {@link android.provider.CallLog.Calls#INCOMING_TYPE}
312             * {@link android.provider.CallLog.Calls#OUTGOING_TYPE}, or
313             * {@link android.provider.CallLog.Calls#MISSED_TYPE}.
314             */
315            public String type;
316            public long date;
317        }
318
319        public QueryHandler(ContentResolver cr) {
320            super(cr);
321        }
322
323        /**
324         * Handles the query results.
325         */
326        @Override
327        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
328            // TODO: it would be faster to use a join here, but for the purposes
329            // of this small record set, it should be ok.
330
331            // Note that CursorJoiner is not useable here because the number
332            // comparisons are not strictly equals; the comparisons happen in
333            // the SQL function PHONE_NUMBERS_EQUAL, which is not available for
334            // the CursorJoiner.
335
336            // Executing our own query is also feasible (with a join), but that
337            // will require some work (possibly destabilizing) in Contacts
338            // Provider.
339
340            // At this point, we will execute subqueries on each row just as
341            // CallLogActivity.java does.
342            switch (token) {
343                case CALL_LOG_TOKEN:
344                    if (DBG) log("call log query complete.");
345
346                    // initial call to retrieve the call list.
347                    if (cursor != null) {
348                        while (cursor.moveToNext()) {
349                            // for each call in the call log list, create
350                            // the notification object and query contacts
351                            NotificationInfo n = getNotificationInfo (cursor);
352
353                            if (DBG) log("query contacts for number: " + n.number);
354
355                            mQueryHandler.startQuery(CONTACT_TOKEN, n,
356                                    Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, n.number),
357                                    PHONES_PROJECTION, null, null, PhoneLookup.NUMBER);
358                        }
359
360                        if (DBG) log("closing call log cursor.");
361                        cursor.close();
362                    }
363                    break;
364                case CONTACT_TOKEN:
365                    if (DBG) log("contact query complete.");
366
367                    // subqueries to get the caller name.
368                    if ((cursor != null) && (cookie != null)){
369                        NotificationInfo n = (NotificationInfo) cookie;
370
371                        Uri personUri = null;
372                        if (cursor.moveToFirst()) {
373                            n.name = cursor.getString(
374                                    cursor.getColumnIndexOrThrow(PhoneLookup.DISPLAY_NAME));
375                            long person_id = cursor.getLong(
376                                    cursor.getColumnIndexOrThrow(PhoneLookup._ID));
377                            if (DBG) {
378                                log("contact :" + n.name + " found for phone: " + n.number
379                                        + ". id : " + person_id);
380                            }
381                            personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, person_id);
382                        }
383
384                        if (personUri != null) {
385                            if (DBG) {
386                                log("Start obtaining picture for the missed call. Uri: "
387                                        + personUri);
388                            }
389                            // Now try to obtain a photo for this person.
390                            // ContactsAsyncHelper will do that and call onImageLoadComplete()
391                            // after that.
392                            ContactsAsyncHelper.startObtainPhotoAsync(
393                                    0, mContext, personUri, this, n);
394                        } else {
395                            if (DBG) {
396                                log("Failed to find Uri for obtaining photo."
397                                        + " Just send notification without it.");
398                            }
399                            // We couldn't find person Uri, so we're sure we cannot obtain a photo.
400                            // Call notifyMissedCall() right now.
401                            notifyMissedCall(n.name, n.number, n.type, null, null, n.date);
402                        }
403
404                        if (DBG) log("closing contact cursor.");
405                        cursor.close();
406                    }
407                    break;
408                default:
409            }
410        }
411
412        @Override
413        public void onImageLoadComplete(
414                int token, Drawable photo, Bitmap photoIcon, Object cookie) {
415            if (DBG) log("Finished loading image: " + photo);
416            NotificationInfo n = (NotificationInfo) cookie;
417            notifyMissedCall(n.name, n.number, n.type, photo, photoIcon, n.date);
418        }
419
420        /**
421         * Factory method to generate a NotificationInfo object given a
422         * cursor from the call log table.
423         */
424        private final NotificationInfo getNotificationInfo(Cursor cursor) {
425            NotificationInfo n = new NotificationInfo();
426            n.name = null;
427            n.number = cursor.getString(cursor.getColumnIndexOrThrow(Calls.NUMBER));
428            n.presentation = cursor.getInt(cursor.getColumnIndexOrThrow(Calls.NUMBER_PRESENTATION));
429            n.type = cursor.getString(cursor.getColumnIndexOrThrow(Calls.TYPE));
430            n.date = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DATE));
431
432            // make sure we update the number depending upon saved values in
433            // CallLog.addCall().  If either special values for unknown or
434            // private number are detected, we need to hand off the message
435            // to the missed call notification.
436            if (n.presentation != Calls.PRESENTATION_ALLOWED) {
437                n.number = null;
438            }
439
440            if (DBG) log("NotificationInfo constructed for number: " + n.number);
441
442            return n;
443        }
444    }
445
446    /**
447     * Configures a Notification to emit the blinky green message-waiting/
448     * missed-call signal.
449     */
450    private static void configureLedNotification(Notification note) {
451        note.flags |= Notification.FLAG_SHOW_LIGHTS;
452        note.defaults |= Notification.DEFAULT_LIGHTS;
453    }
454
455    /**
456     * Displays a notification about a missed call.
457     *
458     * @param name the contact name.
459     * @param number the phone number. Note that this may be a non-callable String like "Unknown",
460     * or "Private Number", which possibly come from methods like
461     * {@link PhoneUtils#modifyForSpecialCnapCases(Context, CallerInfo, String, int)}.
462     * @param type the type of the call. {@link android.provider.CallLog.Calls#INCOMING_TYPE}
463     * {@link android.provider.CallLog.Calls#OUTGOING_TYPE}, or
464     * {@link android.provider.CallLog.Calls#MISSED_TYPE}
465     * @param photo picture which may be used for the notification (when photoIcon is null).
466     * This also can be null when the picture itself isn't available. If photoIcon is available
467     * it should be prioritized (because this may be too huge for notification).
468     * See also {@link ContactsAsyncHelper}.
469     * @param photoIcon picture which should be used for the notification. Can be null. This is
470     * the most suitable for {@link android.app.Notification.Builder#setLargeIcon(Bitmap)}, this
471     * should be used when non-null.
472     * @param date the time when the missed call happened
473     */
474    /* package */ void notifyMissedCall(
475            String name, String number, String type, Drawable photo, Bitmap photoIcon, long date) {
476
477        // When the user clicks this notification, we go to the call log.
478        final PendingIntent pendingCallLogIntent = PhoneGlobals.createPendingCallLogIntent(
479                mContext);
480
481        // Never display the missed call notification on non-voice-capable
482        // devices, even if the device does somehow manage to get an
483        // incoming call.
484        if (!PhoneGlobals.sVoiceCapable) {
485            if (DBG) log("notifyMissedCall: non-voice-capable device, not posting notification");
486            return;
487        }
488
489        if (VDBG) {
490            log("notifyMissedCall(). name: " + name + ", number: " + number
491                + ", label: " + type + ", photo: " + photo + ", photoIcon: " + photoIcon
492                + ", date: " + date);
493        }
494
495        // title resource id
496        int titleResId;
497        // the text in the notification's line 1 and 2.
498        String expandedText, callName;
499
500        // increment number of missed calls.
501        mNumberMissedCalls++;
502
503        // get the name for the ticker text
504        // i.e. "Missed call from <caller name or number>"
505        if (name != null && TextUtils.isGraphic(name)) {
506            callName = name;
507        } else if (!TextUtils.isEmpty(number)){
508            final BidiFormatter bidiFormatter = BidiFormatter.getInstance();
509            // A number should always be displayed LTR using {@link BidiFormatter}
510            // regardless of the content of the rest of the notification.
511            callName = bidiFormatter.unicodeWrap(number, TextDirectionHeuristics.LTR);
512        } else {
513            // use "unknown" if the caller is unidentifiable.
514            callName = mContext.getString(R.string.unknown);
515        }
516
517        // display the first line of the notification:
518        // 1 missed call: call name
519        // more than 1 missed call: <number of calls> + "missed calls"
520        if (mNumberMissedCalls == 1) {
521            titleResId = R.string.notification_missedCallTitle;
522            expandedText = callName;
523        } else {
524            titleResId = R.string.notification_missedCallsTitle;
525            expandedText = mContext.getString(R.string.notification_missedCallsMsg,
526                    mNumberMissedCalls);
527        }
528
529        Notification.Builder builder = new Notification.Builder(mContext);
530        builder.setSmallIcon(android.R.drawable.stat_notify_missed_call)
531                .setTicker(mContext.getString(R.string.notification_missedCallTicker, callName))
532                .setWhen(date)
533                .setContentTitle(mContext.getText(titleResId))
534                .setContentText(expandedText)
535                .setContentIntent(pendingCallLogIntent)
536                .setAutoCancel(true)
537                .setDeleteIntent(createClearMissedCallsIntent());
538
539        // Simple workaround for issue 6476275; refrain having actions when the given number seems
540        // not a real one but a non-number which was embedded by methods outside (like
541        // PhoneUtils#modifyForSpecialCnapCases()).
542        // TODO: consider removing equals() checks here, and modify callers of this method instead.
543        if (mNumberMissedCalls == 1
544                && !TextUtils.isEmpty(number)
545                && !TextUtils.equals(number, mContext.getString(R.string.private_num))
546                && !TextUtils.equals(number, mContext.getString(R.string.unknown))){
547            if (DBG) log("Add actions with the number " + number);
548
549            builder.addAction(R.drawable.stat_sys_phone_call,
550                    mContext.getString(R.string.notification_missedCall_call_back),
551                    PhoneGlobals.getCallBackPendingIntent(mContext, number));
552
553            builder.addAction(R.drawable.ic_text_holo_dark,
554                    mContext.getString(R.string.notification_missedCall_message),
555                    PhoneGlobals.getSendSmsFromNotificationPendingIntent(mContext, number));
556
557            if (photoIcon != null) {
558                builder.setLargeIcon(photoIcon);
559            } else if (photo instanceof BitmapDrawable) {
560                builder.setLargeIcon(((BitmapDrawable) photo).getBitmap());
561            }
562        } else {
563            if (DBG) {
564                log("Suppress actions. number: " + number + ", missedCalls: " + mNumberMissedCalls);
565            }
566        }
567
568        Notification notification = builder.getNotification();
569        configureLedNotification(notification);
570        mNotificationManager.notify(MISSED_CALL_NOTIFICATION, notification);
571    }
572
573    /** Returns an intent to be invoked when the missed call notification is cleared. */
574    private PendingIntent createClearMissedCallsIntent() {
575        Intent intent = new Intent(mContext, ClearMissedCallsService.class);
576        intent.setAction(ClearMissedCallsService.ACTION_CLEAR_MISSED_CALLS);
577        return PendingIntent.getService(mContext, 0, intent, 0);
578    }
579
580    /**
581     * Cancels the "missed call" notification.
582     *
583     * @see ITelephony.cancelMissedCallsNotification()
584     */
585    void cancelMissedCallNotification() {
586        // reset the number of missed calls to 0.
587        mNumberMissedCalls = 0;
588        mNotificationManager.cancel(MISSED_CALL_NOTIFICATION);
589    }
590
591    private void notifySpeakerphone() {
592        if (!mShowingSpeakerphoneIcon) {
593            mStatusBarManager.setIcon("speakerphone", android.R.drawable.stat_sys_speakerphone, 0,
594                    mContext.getString(R.string.accessibility_speakerphone_enabled));
595            mShowingSpeakerphoneIcon = true;
596        }
597    }
598
599    private void cancelSpeakerphone() {
600        if (mShowingSpeakerphoneIcon) {
601            mStatusBarManager.removeIcon("speakerphone");
602            mShowingSpeakerphoneIcon = false;
603        }
604    }
605
606    /**
607     * Shows or hides the "speakerphone" notification in the status bar,
608     * based on the actual current state of the speaker.
609     *
610     * If you already know the current speaker state (e.g. if you just
611     * called AudioManager.setSpeakerphoneOn() yourself) then you should
612     * directly call {@link #updateSpeakerNotification(boolean)} instead.
613     *
614     * (But note that the status bar icon is *never* shown while the in-call UI
615     * is active; it only appears if you bail out to some other activity.)
616     */
617    private void updateSpeakerNotification() {
618        AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
619        boolean showNotification =
620                (mPhone.getState() == PhoneConstants.State.OFFHOOK) && audioManager.isSpeakerphoneOn();
621
622        if (DBG) log(showNotification
623                     ? "updateSpeakerNotification: speaker ON"
624                     : "updateSpeakerNotification: speaker OFF (or not offhook)");
625
626        updateSpeakerNotification(showNotification);
627    }
628
629    /**
630     * Shows or hides the "speakerphone" notification in the status bar.
631     *
632     * @param showNotification if true, call notifySpeakerphone();
633     *                         if false, call cancelSpeakerphone().
634     *
635     * Use {@link updateSpeakerNotification()} to update the status bar
636     * based on the actual current state of the speaker.
637     *
638     * (But note that the status bar icon is *never* shown while the in-call UI
639     * is active; it only appears if you bail out to some other activity.)
640     */
641    public void updateSpeakerNotification(boolean showNotification) {
642        if (DBG) log("updateSpeakerNotification(" + showNotification + ")...");
643
644        // Regardless of the value of the showNotification param, suppress
645        // the status bar icon if the the InCallScreen is the foreground
646        // activity, since the in-call UI already provides an onscreen
647        // indication of the speaker state.  (This reduces clutter in the
648        // status bar.)
649
650        if (showNotification) {
651            notifySpeakerphone();
652        } else {
653            cancelSpeakerphone();
654        }
655    }
656
657    private void notifyMute() {
658        if (!mShowingMuteIcon) {
659            mStatusBarManager.setIcon("mute", android.R.drawable.stat_notify_call_mute, 0,
660                    mContext.getString(R.string.accessibility_call_muted));
661            mShowingMuteIcon = true;
662        }
663    }
664
665    private void cancelMute() {
666        if (mShowingMuteIcon) {
667            mStatusBarManager.removeIcon("mute");
668            mShowingMuteIcon = false;
669        }
670    }
671
672    /**
673     * Shows or hides the "mute" notification in the status bar,
674     * based on the current mute state of the Phone.
675     *
676     * (But note that the status bar icon is *never* shown while the in-call UI
677     * is active; it only appears if you bail out to some other activity.)
678     */
679    void updateMuteNotification() {
680        // Suppress the status bar icon if the the InCallScreen is the
681        // foreground activity, since the in-call UI already provides an
682        // onscreen indication of the mute state.  (This reduces clutter
683        // in the status bar.)
684
685        if ((mCM.getState() == PhoneConstants.State.OFFHOOK) && PhoneUtils.getMute()) {
686            if (DBG) log("updateMuteNotification: MUTED");
687            notifyMute();
688        } else {
689            if (DBG) log("updateMuteNotification: not muted (or not offhook)");
690            cancelMute();
691        }
692    }
693
694    /**
695     * Completely take down the in-call notification *and* the mute/speaker
696     * notifications as well, to indicate that the phone is now idle.
697     */
698    /* package */ void cancelCallInProgressNotifications() {
699        if (DBG) log("cancelCallInProgressNotifications");
700        cancelMute();
701        cancelSpeakerphone();
702    }
703
704    /**
705     * Updates the message waiting indicator (voicemail) notification.
706     *
707     * @param visible true if there are messages waiting
708     */
709    /* package */ void updateMwi(boolean visible) {
710        if (DBG) log("updateMwi(): " + visible);
711
712        if (visible) {
713            int resId = android.R.drawable.stat_notify_voicemail;
714
715            // This Notification can get a lot fancier once we have more
716            // information about the current voicemail messages.
717            // (For example, the current voicemail system can't tell
718            // us the caller-id or timestamp of a message, or tell us the
719            // message count.)
720
721            // But for now, the UI is ultra-simple: if the MWI indication
722            // is supposed to be visible, just show a single generic
723            // notification.
724
725            String notificationTitle = mContext.getString(R.string.notification_voicemail_title);
726            String vmNumber = mPhone.getVoiceMailNumber();
727            if (DBG) log("- got vm number: '" + vmNumber + "'");
728
729            // Watch out: vmNumber may be null, for two possible reasons:
730            //
731            //   (1) This phone really has no voicemail number
732            //
733            //   (2) This phone *does* have a voicemail number, but
734            //       the SIM isn't ready yet.
735            //
736            // Case (2) *does* happen in practice if you have voicemail
737            // messages when the device first boots: we get an MWI
738            // notification as soon as we register on the network, but the
739            // SIM hasn't finished loading yet.
740            //
741            // So handle case (2) by retrying the lookup after a short
742            // delay.
743
744            if ((vmNumber == null) && !mPhone.getIccRecordsLoaded()) {
745                if (DBG) log("- Null vm number: SIM records not loaded (yet)...");
746
747                // TODO: rather than retrying after an arbitrary delay, it
748                // would be cleaner to instead just wait for a
749                // SIM_RECORDS_LOADED notification.
750                // (Unfortunately right now there's no convenient way to
751                // get that notification in phone app code.  We'd first
752                // want to add a call like registerForSimRecordsLoaded()
753                // to Phone.java and GSMPhone.java, and *then* we could
754                // listen for that in the CallNotifier class.)
755
756                // Limit the number of retries (in case the SIM is broken
757                // or missing and can *never* load successfully.)
758                if (mVmNumberRetriesRemaining-- > 0) {
759                    if (DBG) log("  - Retrying in " + VM_NUMBER_RETRY_DELAY_MILLIS + " msec...");
760                    mApp.notifier.sendMwiChangedDelayed(VM_NUMBER_RETRY_DELAY_MILLIS);
761                    return;
762                } else {
763                    Log.w(LOG_TAG, "NotificationMgr.updateMwi: getVoiceMailNumber() failed after "
764                          + MAX_VM_NUMBER_RETRIES + " retries; giving up.");
765                    // ...and continue with vmNumber==null, just as if the
766                    // phone had no VM number set up in the first place.
767                }
768            }
769
770            if (TelephonyCapabilities.supportsVoiceMessageCount(mPhone)) {
771                int vmCount = mPhone.getVoiceMessageCount();
772                String titleFormat = mContext.getString(R.string.notification_voicemail_title_count);
773                notificationTitle = String.format(titleFormat, vmCount);
774            }
775
776            String notificationText;
777            if (TextUtils.isEmpty(vmNumber)) {
778                notificationText = mContext.getString(
779                        R.string.notification_voicemail_no_vm_number);
780            } else {
781                notificationText = String.format(
782                        mContext.getString(R.string.notification_voicemail_text_format),
783                        PhoneNumberUtils.formatNumber(vmNumber));
784            }
785
786            Intent intent = new Intent(Intent.ACTION_CALL,
787                    Uri.fromParts(Constants.SCHEME_VOICEMAIL, "", null));
788            PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
789
790            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
791            Uri ringtoneUri;
792            String uriString = prefs.getString(
793                    CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_RINGTONE_KEY, null);
794            if (!TextUtils.isEmpty(uriString)) {
795                ringtoneUri = Uri.parse(uriString);
796            } else {
797                ringtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI;
798            }
799
800            Notification.Builder builder = new Notification.Builder(mContext);
801            builder.setSmallIcon(resId)
802                    .setWhen(System.currentTimeMillis())
803                    .setContentTitle(notificationTitle)
804                    .setContentText(notificationText)
805                    .setContentIntent(pendingIntent)
806                    .setSound(ringtoneUri);
807            Notification notification = builder.getNotification();
808
809            CallFeaturesSetting.migrateVoicemailVibrationSettingsIfNeeded(prefs);
810            final boolean vibrate = prefs.getBoolean(
811                    CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_VIBRATE_KEY, false);
812            if (vibrate) {
813                notification.defaults |= Notification.DEFAULT_VIBRATE;
814            }
815            notification.flags |= Notification.FLAG_NO_CLEAR;
816            configureLedNotification(notification);
817            mNotificationManager.notify(VOICEMAIL_NOTIFICATION, notification);
818        } else {
819            mNotificationManager.cancel(VOICEMAIL_NOTIFICATION);
820        }
821    }
822
823    /**
824     * Updates the message call forwarding indicator notification.
825     *
826     * @param visible true if there are messages waiting
827     */
828    /* package */ void updateCfi(boolean visible) {
829        if (DBG) log("updateCfi(): " + visible);
830        if (visible) {
831            // If Unconditional Call Forwarding (forward all calls) for VOICE
832            // is enabled, just show a notification.  We'll default to expanded
833            // view for now, so the there is less confusion about the icon.  If
834            // it is deemed too weird to have CF indications as expanded views,
835            // then we'll flip the flag back.
836
837            // TODO: We may want to take a look to see if the notification can
838            // display the target to forward calls to.  This will require some
839            // effort though, since there are multiple layers of messages that
840            // will need to propagate that information.
841
842            Notification notification;
843            final boolean showExpandedNotification = true;
844            if (showExpandedNotification) {
845                Intent intent = new Intent(Intent.ACTION_MAIN);
846                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
847                intent.setClassName("com.android.phone",
848                        "com.android.phone.CallFeaturesSetting");
849
850                notification = new Notification(
851                        R.drawable.stat_sys_phone_call_forward,  // icon
852                        null, // tickerText
853                        0); // The "timestamp" of this notification is meaningless;
854                            // we only care about whether CFI is currently on or not.
855                notification.setLatestEventInfo(
856                        mContext, // context
857                        mContext.getString(R.string.labelCF), // expandedTitle
858                        mContext.getString(R.string.sum_cfu_enabled_indicator), // expandedText
859                        PendingIntent.getActivity(mContext, 0, intent, 0)); // contentIntent
860            } else {
861                notification = new Notification(
862                        R.drawable.stat_sys_phone_call_forward,  // icon
863                        null,  // tickerText
864                        System.currentTimeMillis()  // when
865                        );
866            }
867
868            notification.flags |= Notification.FLAG_ONGOING_EVENT;  // also implies FLAG_NO_CLEAR
869
870            mNotificationManager.notify(
871                    CALL_FORWARD_NOTIFICATION,
872                    notification);
873        } else {
874            mNotificationManager.cancel(CALL_FORWARD_NOTIFICATION);
875        }
876    }
877
878    /**
879     * Shows the "data disconnected due to roaming" notification, which
880     * appears when you lose data connectivity because you're roaming and
881     * you have the "data roaming" feature turned off.
882     */
883    /* package */ void showDataDisconnectedRoaming() {
884        if (DBG) log("showDataDisconnectedRoaming()...");
885
886        // "Mobile network settings" screen / dialog
887        Intent intent = new Intent(mContext, com.android.phone.MobileNetworkSettings.class);
888
889        final CharSequence contentText = mContext.getText(R.string.roaming_reenable_message);
890
891        final Notification.Builder builder = new Notification.Builder(mContext);
892        builder.setSmallIcon(android.R.drawable.stat_sys_warning);
893        builder.setContentTitle(mContext.getText(R.string.roaming));
894        builder.setContentText(contentText);
895        builder.setContentIntent(PendingIntent.getActivity(mContext, 0, intent, 0));
896
897        final Notification notif = new Notification.BigTextStyle(builder).bigText(contentText)
898                .build();
899
900        mNotificationManager.notify(DATA_DISCONNECTED_ROAMING_NOTIFICATION, notif);
901    }
902
903    /**
904     * Turns off the "data disconnected due to roaming" notification.
905     */
906    /* package */ void hideDataDisconnectedRoaming() {
907        if (DBG) log("hideDataDisconnectedRoaming()...");
908        mNotificationManager.cancel(DATA_DISCONNECTED_ROAMING_NOTIFICATION);
909    }
910
911    /**
912     * Display the network selection "no service" notification
913     * @param operator is the numeric operator number
914     */
915    private void showNetworkSelection(String operator) {
916        if (DBG) log("showNetworkSelection(" + operator + ")...");
917
918        String titleText = mContext.getString(
919                R.string.notification_network_selection_title);
920        String expandedText = mContext.getString(
921                R.string.notification_network_selection_text, operator);
922
923        Notification notification = new Notification();
924        notification.icon = android.R.drawable.stat_sys_warning;
925        notification.when = 0;
926        notification.flags = Notification.FLAG_ONGOING_EVENT;
927        notification.tickerText = null;
928
929        // create the target network operators settings intent
930        Intent intent = new Intent(Intent.ACTION_MAIN);
931        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
932                Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
933        // Use NetworkSetting to handle the selection intent
934        intent.setComponent(new ComponentName("com.android.phone",
935                "com.android.phone.NetworkSetting"));
936        PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0);
937
938        notification.setLatestEventInfo(mContext, titleText, expandedText, pi);
939
940        mNotificationManager.notify(SELECTED_OPERATOR_FAIL_NOTIFICATION, notification);
941    }
942
943    /**
944     * Turn off the network selection "no service" notification
945     */
946    private void cancelNetworkSelection() {
947        if (DBG) log("cancelNetworkSelection()...");
948        mNotificationManager.cancel(SELECTED_OPERATOR_FAIL_NOTIFICATION);
949    }
950
951    /**
952     * Update notification about no service of user selected operator
953     *
954     * @param serviceState Phone service state
955     */
956    void updateNetworkSelection(int serviceState) {
957        if (TelephonyCapabilities.supportsNetworkSelection(mPhone)) {
958            // get the shared preference of network_selection.
959            // empty is auto mode, otherwise it is the operator alpha name
960            // in case there is no operator name, check the operator numeric
961            SharedPreferences sp =
962                    PreferenceManager.getDefaultSharedPreferences(mContext);
963            String networkSelection =
964                    sp.getString(PhoneBase.NETWORK_SELECTION_NAME_KEY, "");
965            if (TextUtils.isEmpty(networkSelection)) {
966                networkSelection =
967                        sp.getString(PhoneBase.NETWORK_SELECTION_KEY, "");
968            }
969
970            if (DBG) log("updateNetworkSelection()..." + "state = " +
971                    serviceState + " new network " + networkSelection);
972
973            if (serviceState == ServiceState.STATE_OUT_OF_SERVICE
974                    && !TextUtils.isEmpty(networkSelection)) {
975                if (!mSelectedUnavailableNotify) {
976                    showNetworkSelection(networkSelection);
977                    mSelectedUnavailableNotify = true;
978                }
979            } else {
980                if (mSelectedUnavailableNotify) {
981                    cancelNetworkSelection();
982                    mSelectedUnavailableNotify = false;
983                }
984            }
985        }
986    }
987
988    /* package */ void postTransientNotification(int notifyId, CharSequence msg) {
989        if (mToast != null) {
990            mToast.cancel();
991        }
992
993        mToast = Toast.makeText(mContext, msg, Toast.LENGTH_LONG);
994        mToast.show();
995    }
996
997    private void log(String msg) {
998        Log.d(LOG_TAG, msg);
999    }
1000}
1001