NotificationMgr.java revision 6bfbc4db43492e59d9b2050b93024245483e2c63
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.Context;
27import android.content.Intent;
28import android.content.SharedPreferences;
29import android.database.Cursor;
30import android.media.AudioManager;
31import android.net.Uri;
32import android.os.IBinder;
33import android.os.SystemClock;
34import android.os.SystemProperties;
35import android.preference.PreferenceManager;
36import android.provider.Settings;
37import android.provider.CallLog.Calls;
38import android.provider.ContactsContract.PhoneLookup;
39import android.telephony.PhoneNumberUtils;
40import android.telephony.ServiceState;
41import android.text.TextUtils;
42import android.util.Log;
43import android.widget.RemoteViews;
44import android.widget.Toast;
45
46import com.android.internal.telephony.Call;
47import com.android.internal.telephony.CallerInfo;
48import com.android.internal.telephony.CallerInfoAsyncQuery;
49import com.android.internal.telephony.Connection;
50import com.android.internal.telephony.Phone;
51import com.android.internal.telephony.PhoneBase;
52
53
54/**
55 * NotificationManager-related utility code for the Phone app.
56 */
57public class NotificationMgr implements CallerInfoAsyncQuery.OnQueryCompleteListener{
58    private static final String LOG_TAG = "NotificationMgr";
59    private static final boolean DBG = (PhoneApp.DBG_LEVEL >= 2);
60
61    private static final String[] CALL_LOG_PROJECTION = new String[] {
62        Calls._ID,
63        Calls.NUMBER,
64        Calls.DATE,
65        Calls.DURATION,
66        Calls.TYPE,
67    };
68
69    // notification types
70    static final int MISSED_CALL_NOTIFICATION = 1;
71    static final int IN_CALL_NOTIFICATION = 2;
72    static final int MMI_NOTIFICATION = 3;
73    static final int NETWORK_SELECTION_NOTIFICATION = 4;
74    static final int VOICEMAIL_NOTIFICATION = 5;
75    static final int CALL_FORWARD_NOTIFICATION = 6;
76    static final int DATA_DISCONNECTED_ROAMING_NOTIFICATION = 7;
77    static final int SELECTED_OPERATOR_FAIL_NOTIFICATION = 8;
78
79    private static NotificationMgr sMe = null;
80    private Phone mPhone;
81
82    private Context mContext;
83    private NotificationManager mNotificationMgr;
84    private StatusBarManager mStatusBar;
85    private StatusBarMgr mStatusBarMgr;
86    private Toast mToast;
87    private IBinder mSpeakerphoneIcon;
88    private IBinder mMuteIcon;
89
90    // used to track the missed call counter, default to 0.
91    private int mNumberMissedCalls = 0;
92
93    // Currently-displayed resource IDs for some status bar icons (or zero
94    // if no notification is active):
95    private int mInCallResId;
96
97    // used to track the notification of selected network unavailable
98    private boolean mSelectedUnavailableNotify = false;
99
100    // Retry params for the getVoiceMailNumber() call; see updateMwi().
101    private static final int MAX_VM_NUMBER_RETRIES = 5;
102    private static final int VM_NUMBER_RETRY_DELAY_MILLIS = 10000;
103    private int mVmNumberRetriesRemaining = MAX_VM_NUMBER_RETRIES;
104
105    // Query used to look up caller-id info for the "call log" notification.
106    private QueryHandler mQueryHandler = null;
107    private static final int CALL_LOG_TOKEN = -1;
108    private static final int CONTACT_TOKEN = -2;
109
110    NotificationMgr(Context context) {
111        mContext = context;
112        mNotificationMgr = (NotificationManager)
113            context.getSystemService(Context.NOTIFICATION_SERVICE);
114
115        mStatusBar = (StatusBarManager) context.getSystemService(Context.STATUS_BAR_SERVICE);
116
117        PhoneApp app = PhoneApp.getInstance();
118        mPhone = app.phone;
119    }
120
121    static void init(Context context) {
122        sMe = new NotificationMgr(context);
123
124        // update the notifications that need to be touched at startup.
125        sMe.updateNotifications();
126    }
127
128    static NotificationMgr getDefault() {
129        return sMe;
130    }
131
132    /**
133     * Class that controls the status bar.  This class maintains a set
134     * of state and acts as an interface between the Phone process and
135     * the Status bar.  All interaction with the status bar should be
136     * though the methods contained herein.
137     */
138
139    /**
140     * Factory method
141     */
142    StatusBarMgr getStatusBarMgr() {
143        if (mStatusBarMgr == null) {
144            mStatusBarMgr = new StatusBarMgr();
145        }
146        return mStatusBarMgr;
147    }
148
149    /**
150     * StatusBarMgr implementation
151     */
152    class StatusBarMgr {
153        // current settings
154        private boolean mIsNotificationEnabled = true;
155        private boolean mIsExpandedViewEnabled = true;
156
157        private StatusBarMgr () {
158        }
159
160        /**
161         * Sets the notification state (enable / disable
162         * vibrating notifications) for the status bar,
163         * updates the status bar service if there is a change.
164         * Independent of the remaining Status Bar
165         * functionality, including icons and expanded view.
166         */
167        void enableNotificationAlerts(boolean enable) {
168            if (mIsNotificationEnabled != enable) {
169                mIsNotificationEnabled = enable;
170                updateStatusBar();
171            }
172        }
173
174        /**
175         * Sets the ability to expand the notifications for the
176         * status bar, updates the status bar service if there
177         * is a change. Independent of the remaining Status Bar
178         * functionality, including icons and notification
179         * alerts.
180         */
181        void enableExpandedView(boolean enable) {
182            if (mIsExpandedViewEnabled != enable) {
183                mIsExpandedViewEnabled = enable;
184                updateStatusBar();
185            }
186        }
187
188        /**
189         * Method to synchronize status bar state with our current
190         * state.
191         */
192        void updateStatusBar() {
193            int state = StatusBarManager.DISABLE_NONE;
194
195            if (!mIsExpandedViewEnabled) {
196                state |= StatusBarManager.DISABLE_EXPAND;
197            }
198
199            if (!mIsNotificationEnabled) {
200                state |= StatusBarManager.DISABLE_NOTIFICATION_ALERTS;
201            }
202
203            // send the message to the status bar manager.
204            if (DBG) log("updating status bar state: " + state);
205            mStatusBar.disable(state);
206        }
207    }
208
209    /**
210     * Makes sure notifications are up to date.
211     */
212    void updateNotifications() {
213        if (DBG) log("begin querying call log");
214
215        // instantiate query handler
216        mQueryHandler = new QueryHandler(mContext.getContentResolver());
217
218        // setup query spec, look for all Missed calls that are new.
219        StringBuilder where = new StringBuilder("type=");
220        where.append(Calls.MISSED_TYPE);
221        where.append(" AND new=1");
222
223        // start the query
224        mQueryHandler.startQuery(CALL_LOG_TOKEN, null, Calls.CONTENT_URI,  CALL_LOG_PROJECTION,
225                where.toString(), null, Calls.DEFAULT_SORT_ORDER);
226
227        // synchronize the in call notification
228        if (mPhone.getState() != Phone.State.OFFHOOK) {
229            if (DBG) log("Phone is idle, canceling notification.");
230            cancelInCall();
231        } else {
232            if (DBG) log("Phone is offhook, updating notification.");
233            updateInCallNotification();
234        }
235
236        // Depend on android.app.StatusBarManager to be set to
237        // disable(DISABLE_NONE) upon startup.  This will be the
238        // case even if the phone app crashes.
239    }
240
241    /** The projection to use when querying the phones table */
242    static final String[] PHONES_PROJECTION = new String[] {
243        PhoneLookup.NUMBER,
244        PhoneLookup.DISPLAY_NAME
245    };
246
247    /**
248     * Class used to run asynchronous queries to re-populate
249     * the notifications we care about.
250     */
251    private class QueryHandler extends AsyncQueryHandler {
252
253        /**
254         * Used to store relevant fields for the Missed Call
255         * notifications.
256         */
257        private class NotificationInfo {
258            public String name;
259            public String number;
260            public String label;
261            public long date;
262        }
263
264        public QueryHandler(ContentResolver cr) {
265            super(cr);
266        }
267
268        /**
269         * Handles the query results.  There are really 2 steps to this,
270         * similar to what happens in RecentCallsListActivity.
271         *  1. Find the list of missed calls
272         *  2. For each call, run a query to retrieve the caller's name.
273         */
274        @Override
275        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
276            // TODO: it would be faster to use a join here, but for the purposes
277            // of this small record set, it should be ok.
278
279            // Note that CursorJoiner is not useable here because the number
280            // comparisons are not strictly equals; the comparisons happen in
281            // the SQL function PHONE_NUMBERS_EQUAL, which is not available for
282            // the CursorJoiner.
283
284            // Executing our own query is also feasible (with a join), but that
285            // will require some work (possibly destabilizing) in Contacts
286            // Provider.
287
288            // At this point, we will execute subqueries on each row just as
289            // RecentCallsListActivity.java does.
290            switch (token) {
291                case CALL_LOG_TOKEN:
292                    if (DBG) log("call log query complete.");
293
294                    // initial call to retrieve the call list.
295                    if (cursor != null) {
296                        while (cursor.moveToNext()) {
297                            // for each call in the call log list, create
298                            // the notification object and query contacts
299                            NotificationInfo n = getNotificationInfo (cursor);
300
301                            if (DBG) log("query contacts for number: " + n.number);
302
303                            mQueryHandler.startQuery(CONTACT_TOKEN, n,
304                                    Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, n.number),
305                                    PHONES_PROJECTION, null, null, PhoneLookup.NUMBER);
306                        }
307
308                        if (DBG) log("closing call log cursor.");
309                        cursor.close();
310                    }
311                    break;
312                case CONTACT_TOKEN:
313                    if (DBG) log("contact query complete.");
314
315                    // subqueries to get the caller name.
316                    if ((cursor != null) && (cookie != null)){
317                        NotificationInfo n = (NotificationInfo) cookie;
318
319                        if (cursor.moveToFirst()) {
320                            // we have contacts data, get the name.
321                            if (DBG) log("contact :" + n.name + " found for phone: " + n.number);
322                            n.name = cursor.getString(
323                                    cursor.getColumnIndexOrThrow(PhoneLookup.DISPLAY_NAME));
324                        }
325
326                        // send the notification
327                        if (DBG) log("sending notification.");
328                        notifyMissedCall(n.name, n.number, n.label, n.date);
329
330                        if (DBG) log("closing contact cursor.");
331                        cursor.close();
332                    }
333                    break;
334                default:
335            }
336        }
337
338        /**
339         * Factory method to generate a NotificationInfo object given a
340         * cursor from the call log table.
341         */
342        private final NotificationInfo getNotificationInfo(Cursor cursor) {
343            NotificationInfo n = new NotificationInfo();
344            n.name = null;
345            n.number = cursor.getString(cursor.getColumnIndexOrThrow(Calls.NUMBER));
346            n.label = cursor.getString(cursor.getColumnIndexOrThrow(Calls.TYPE));
347            n.date = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DATE));
348
349            // make sure we update the number depending upon saved values in
350            // CallLog.addCall().  If either special values for unknown or
351            // private number are detected, we need to hand off the message
352            // to the missed call notification.
353            if ( (n.number.equals(CallerInfo.UNKNOWN_NUMBER)) ||
354                 (n.number.equals(CallerInfo.PRIVATE_NUMBER)) ||
355                 (n.number.equals(CallerInfo.PAYPHONE_NUMBER)) ) {
356                n.number = null;
357            }
358
359            if (DBG) log("NotificationInfo constructed for number: " + n.number);
360
361            return n;
362        }
363    }
364
365    /**
366     * Configures a Notification to emit the blinky green message-waiting/
367     * missed-call signal.
368     */
369    private static void configureLedNotification(Notification note) {
370        note.flags |= Notification.FLAG_SHOW_LIGHTS;
371        note.ledARGB = 0xff00ff00;
372        note.ledOnMS = 500;
373        note.ledOffMS = 2000;
374    }
375
376    /**
377     * Displays a notification about a missed call.
378     *
379     * @param nameOrNumber either the contact name, or the phone number if no contact
380     * @param label the label of the number if nameOrNumber is a name, null if it is a number
381     */
382    void notifyMissedCall(String name, String number, String label, long date) {
383        // title resource id
384        int titleResId;
385        // the text in the notification's line 1 and 2.
386        String expandedText, callName;
387
388        // increment number of missed calls.
389        mNumberMissedCalls++;
390
391        // get the name for the ticker text
392        // i.e. "Missed call from <caller name or number>"
393        if (name != null && TextUtils.isGraphic(name)) {
394            callName = name;
395        } else if (!TextUtils.isEmpty(number)){
396            callName = number;
397        } else {
398            // use "unknown" if the caller is unidentifiable.
399            callName = mContext.getString(R.string.unknown);
400        }
401
402        // display the first line of the notification:
403        // 1 missed call: call name
404        // more than 1 missed call: <number of calls> + "missed calls"
405        if (mNumberMissedCalls == 1) {
406            titleResId = R.string.notification_missedCallTitle;
407            expandedText = callName;
408        } else {
409            titleResId = R.string.notification_missedCallsTitle;
410            expandedText = mContext.getString(R.string.notification_missedCallsMsg,
411                    mNumberMissedCalls);
412        }
413
414        // create the target call log intent
415        final Intent intent = PhoneApp.createCallLogIntent();
416
417        // make the notification
418        Notification note = new Notification(mContext, // context
419                android.R.drawable.stat_notify_missed_call, // icon
420                mContext.getString(R.string.notification_missedCallTicker, callName), // tickerText
421                date, // when
422                mContext.getText(titleResId), // expandedTitle
423                expandedText, // expandedText
424                intent // contentIntent
425                );
426        configureLedNotification(note);
427        mNotificationMgr.notify(MISSED_CALL_NOTIFICATION, note);
428    }
429
430    void cancelMissedCallNotification() {
431        // reset the number of missed calls to 0.
432        mNumberMissedCalls = 0;
433        mNotificationMgr.cancel(MISSED_CALL_NOTIFICATION);
434    }
435
436    void notifySpeakerphone() {
437        if (mSpeakerphoneIcon == null) {
438            mSpeakerphoneIcon = mStatusBar.addIcon("speakerphone",
439                    android.R.drawable.stat_sys_speakerphone, 0);
440        }
441    }
442
443    void cancelSpeakerphone() {
444        if (mSpeakerphoneIcon != null) {
445            mStatusBar.removeIcon(mSpeakerphoneIcon);
446            mSpeakerphoneIcon = null;
447        }
448    }
449
450    /**
451     * Calls either notifySpeakerphone() or cancelSpeakerphone() based on
452     * the actual current state of the speaker.
453     */
454    void updateSpeakerNotification() {
455        AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
456
457        if ((mPhone.getState() == Phone.State.OFFHOOK) && audioManager.isSpeakerphoneOn()) {
458            if (DBG) log("updateSpeakerNotification: speaker ON");
459            notifySpeakerphone();
460        } else {
461            if (DBG) log("updateSpeakerNotification: speaker OFF (or not offhook)");
462            cancelSpeakerphone();
463        }
464    }
465
466    void notifyMute() {
467        if (mMuteIcon == null) {
468            mMuteIcon = mStatusBar.addIcon("mute", android.R.drawable.stat_notify_call_mute, 0);
469        }
470    }
471
472    void cancelMute() {
473        if (mMuteIcon != null) {
474            mStatusBar.removeIcon(mMuteIcon);
475            mMuteIcon = null;
476        }
477    }
478
479    /**
480     * Calls either notifyMute() or cancelMute() based on
481     * the actual current mute state of the Phone.
482     */
483    void updateMuteNotification() {
484        if ((mPhone.getState() == Phone.State.OFFHOOK) && mPhone.getMute()) {
485            if (DBG) log("updateMuteNotification: MUTED");
486            notifyMute();
487        } else {
488            if (DBG) log("updateMuteNotification: not muted (or not offhook)");
489            cancelMute();
490        }
491    }
492
493    void updateInCallNotification() {
494        int resId;
495        if (DBG) log("updateInCallNotification()...");
496
497        if (mPhone.getState() != Phone.State.OFFHOOK) {
498            return;
499        }
500
501        final boolean hasActiveCall = !mPhone.getForegroundCall().isIdle();
502        final boolean hasHoldingCall = !mPhone.getBackgroundCall().isIdle();
503
504        // Display the appropriate "in-call" icon in the status bar,
505        // which depends on the current phone and/or bluetooth state.
506
507
508        boolean enhancedVoicePrivacy = PhoneApp.getInstance().notifier.getCdmaVoicePrivacyState();
509        if (DBG) log("updateInCallNotification: enhancedVoicePrivacy = " + enhancedVoicePrivacy);
510
511        if (!hasActiveCall && hasHoldingCall) {
512            // There's only one call, and it's on hold.
513            if (enhancedVoicePrivacy) {
514                resId = android.R.drawable.stat_sys_vp_phone_call_on_hold;
515            } else {
516                resId = android.R.drawable.stat_sys_phone_call_on_hold;
517            }
518        } else if (PhoneApp.getInstance().showBluetoothIndication()) {
519            // Bluetooth is active.
520            if (enhancedVoicePrivacy) {
521                resId = com.android.internal.R.drawable.stat_sys_vp_phone_call_bluetooth;
522            } else {
523                resId = com.android.internal.R.drawable.stat_sys_phone_call_bluetooth;
524            }
525        } else {
526            if (enhancedVoicePrivacy) {
527                resId = android.R.drawable.stat_sys_vp_phone_call;
528            } else {
529                resId = android.R.drawable.stat_sys_phone_call;
530            }
531        }
532
533        // Note we can't just bail out now if (resId == mInCallResId),
534        // since even if the status icon hasn't changed, some *other*
535        // notification-related info may be different from the last time
536        // we were here (like the caller-id info of the foreground call,
537        // if the user swapped calls...)
538
539        if (DBG) log("- Updating status bar icon: " + resId);
540        mInCallResId = resId;
541
542        // Even if both lines are in use, we only show a single item in
543        // the expanded Notifications UI.  It's labeled "Ongoing call"
544        // (or "On hold" if there's only one call, and it's on hold.)
545
546        // The icon in the expanded view is the same as in the status bar.
547        int expandedViewIcon = mInCallResId;
548
549        // Also, we don't have room to display caller-id info from two
550        // different calls.  So if there's only one call, use that, but if
551        // both lines are in use we display the caller-id info from the
552        // foreground call and totally ignore the background call.
553        Call currentCall = hasActiveCall ? mPhone.getForegroundCall()
554                : mPhone.getBackgroundCall();
555        Connection currentConn = currentCall.getEarliestConnection();
556
557        // When expanded, the "Ongoing call" notification is (visually)
558        // different from most other Notifications, so we need to use a
559        // custom view hierarchy.
560
561        Notification notification = new Notification();
562        notification.icon = mInCallResId;
563        notification.contentIntent = PendingIntent.getActivity(mContext, 0,
564                PhoneApp.createInCallIntent(), 0);
565        notification.flags |= Notification.FLAG_ONGOING_EVENT;
566
567        // Our custom view, which includes an icon (either "ongoing call" or
568        // "on hold") and 2 lines of text: (1) the label (either "ongoing
569        // call" with time counter, or "on hold), and (2) the compact name of
570        // the current Connection.
571        RemoteViews contentView = new RemoteViews(mContext.getPackageName(),
572                                                   R.layout.ongoing_call_notification);
573        contentView.setImageViewResource(R.id.icon, expandedViewIcon);
574
575        // if the connection is valid, then build what we need for the
576        // first line of notification information, and start the chronometer.
577        // Otherwise, don't bother and just stick with line 2.
578        if (currentConn != null) {
579            // Determine the "start time" of the current connection, in terms
580            // of the SystemClock.elapsedRealtime() timebase (which is what
581            // the Chronometer widget needs.)
582            //   We can't use currentConn.getConnectTime(), because (1) that's
583            // in the currentTimeMillis() time base, and (2) it's zero when
584            // the phone first goes off hook, since the getConnectTime counter
585            // doesn't start until the DIALING -> ACTIVE transition.
586            //   Instead we start with the current connection's duration,
587            // and translate that into the elapsedRealtime() timebase.
588            long callDurationMsec = currentConn.getDurationMillis();
589            long chronometerBaseTime = SystemClock.elapsedRealtime() - callDurationMsec;
590
591            // Line 1 of the expanded view (in bold text):
592            String expandedViewLine1;
593            if (hasHoldingCall && !hasActiveCall) {
594                // Only one call, and it's on hold!
595                // Note this isn't a format string!  (We want "On hold" here,
596                // not "On hold (1:23)".)  That's OK; if you call
597                // String.format() with more arguments than format specifiers,
598                // the extra arguments are ignored.
599                expandedViewLine1 = mContext.getString(R.string.notification_on_hold);
600            } else {
601                // Format string with a "%s" where the current call time should go.
602                expandedViewLine1 = mContext.getString(R.string.notification_ongoing_call_format);
603            }
604
605            if (DBG) log("- Updating expanded view: line 1 '" + expandedViewLine1 + "'");
606
607            // Text line #1 is actually a Chronometer, not a plain TextView.
608            // We format the elapsed time of the current call into a line like
609            // "Ongoing call (01:23)".
610            contentView.setChronometer(R.id.text1,
611                                       chronometerBaseTime,
612                                       expandedViewLine1,
613                                       true);
614        } else if (DBG) {
615            log("updateInCallNotification: connection is null, call status not updated.");
616        }
617
618        // display conference call string if this call is a conference
619        // call, otherwise display the connection information.
620
621        // TODO: it may not make sense for every point to make separate
622        // checks for isConferenceCall, so we need to think about
623        // possibly including this in startGetCallerInfo or some other
624        // common point.
625        String expandedViewLine2 = "";
626        if (PhoneUtils.isConferenceCall(currentCall)) {
627            // if this is a conference call, just use that as the caller name.
628            expandedViewLine2 = mContext.getString(R.string.card_title_conf_call);
629        } else {
630            // Start asynchronous call to get the compact name.
631            PhoneUtils.CallerInfoToken cit =
632                PhoneUtils.startGetCallerInfo (mContext, currentCall, this, contentView);
633            // Line 2 of the expanded view (smaller text):
634            expandedViewLine2 = PhoneUtils.getCompactNameFromCallerInfo(cit.currentInfo, mContext);
635        }
636
637        if (DBG) log("- Updating expanded view: line 2 '" + expandedViewLine2 + "'");
638        contentView.setTextViewText(R.id.text2, expandedViewLine2);
639        notification.contentView = contentView;
640
641        // TODO: We also need to *update* this notification in some cases,
642        // like when a call ends on one line but the other is still in use
643        // (ie. make sure the caller info here corresponds to the active
644        // line), and maybe even when the user swaps calls (ie. if we only
645        // show info here for the "current active call".)
646
647        if (DBG) log("Notifying IN_CALL_NOTIFICATION: " + notification);
648        mNotificationMgr.notify(IN_CALL_NOTIFICATION,
649                                notification);
650
651        // Finally, refresh the mute and speakerphone notifications (since
652        // some phone state changes can indirectly affect the mute and/or
653        // speaker state).
654        updateSpeakerNotification();
655        updateMuteNotification();
656    }
657
658    /**
659     * Implemented for CallerInfoAsyncQuery.OnQueryCompleteListener interface.
660     * refreshes the contentView when called.
661     */
662    public void onQueryComplete(int token, Object cookie, CallerInfo ci){
663        if (DBG) log("callerinfo query complete, updating ui.");
664
665        ((RemoteViews) cookie).setTextViewText(R.id.text2,
666                PhoneUtils.getCompactNameFromCallerInfo(ci, mContext));
667    }
668
669    private void cancelInCall() {
670        if (DBG) log("cancelInCall()...");
671        cancelMute();
672        cancelSpeakerphone();
673        mNotificationMgr.cancel(IN_CALL_NOTIFICATION);
674        mInCallResId = 0;
675    }
676
677    void cancelCallInProgressNotification() {
678        if (DBG) log("cancelCallInProgressNotification()...");
679        if (mInCallResId == 0) {
680            return;
681        }
682
683        if (DBG) log("cancelCallInProgressNotification: " + mInCallResId);
684        cancelInCall();
685    }
686
687    /**
688     * Updates the message waiting indicator (voicemail) notification.
689     *
690     * @param visible true if there are messages waiting
691     */
692    /* package */ void updateMwi(boolean visible) {
693        if (DBG) log("updateMwi(): " + visible);
694        if (visible) {
695            int resId = android.R.drawable.stat_notify_voicemail;
696
697            // This Notification can get a lot fancier once we have more
698            // information about the current voicemail messages.
699            // (For example, the current voicemail system can't tell
700            // us the caller-id or timestamp of a message, or tell us the
701            // message count.)
702
703            // But for now, the UI is ultra-simple: if the MWI indication
704            // is supposed to be visible, just show a single generic
705            // notification.
706
707            String notificationTitle = mContext.getString(R.string.notification_voicemail_title);
708            String vmNumber = mPhone.getVoiceMailNumber();
709            if (DBG) log("- got vm number: '" + vmNumber + "'");
710
711            // Watch out: vmNumber may be null, for two possible reasons:
712            //
713            //   (1) This phone really has no voicemail number
714            //
715            //   (2) This phone *does* have a voicemail number, but
716            //       the SIM isn't ready yet.
717            //
718            // Case (2) *does* happen in practice if you have voicemail
719            // messages when the device first boots: we get an MWI
720            // notification as soon as we register on the network, but the
721            // SIM hasn't finished loading yet.
722            //
723            // So handle case (2) by retrying the lookup after a short
724            // delay.
725
726            if ((vmNumber == null) && !mPhone.getIccRecordsLoaded()) {
727                if (DBG) log("- Null vm number: SIM records not loaded (yet)...");
728
729                // TODO: rather than retrying after an arbitrary delay, it
730                // would be cleaner to instead just wait for a
731                // SIM_RECORDS_LOADED notification.
732                // (Unfortunately right now there's no convenient way to
733                // get that notification in phone app code.  We'd first
734                // want to add a call like registerForSimRecordsLoaded()
735                // to Phone.java and GSMPhone.java, and *then* we could
736                // listen for that in the CallNotifier class.)
737
738                // Limit the number of retries (in case the SIM is broken
739                // or missing and can *never* load successfully.)
740                if (mVmNumberRetriesRemaining-- > 0) {
741                    if (DBG) log("  - Retrying in " + VM_NUMBER_RETRY_DELAY_MILLIS + " msec...");
742                    PhoneApp.getInstance().notifier.sendMwiChangedDelayed(
743                            VM_NUMBER_RETRY_DELAY_MILLIS);
744                    return;
745                } else {
746                    Log.w(LOG_TAG, "NotificationMgr.updateMwi: getVoiceMailNumber() failed after "
747                          + MAX_VM_NUMBER_RETRIES + " retries; giving up.");
748                    // ...and continue with vmNumber==null, just as if the
749                    // phone had no VM number set up in the first place.
750                }
751            }
752
753            if (mPhone.getPhoneType() == Phone.PHONE_TYPE_CDMA) {
754                int vmCount = mPhone.getVoiceMessageCount();
755                String titleFormat = mContext.getString(R.string.notification_voicemail_title_count);
756                notificationTitle = String.format(titleFormat, vmCount);
757            }
758
759            String notificationText;
760            if (TextUtils.isEmpty(vmNumber)) {
761                notificationText = mContext.getString(
762                        R.string.notification_voicemail_no_vm_number);
763            } else {
764                notificationText = String.format(
765                        mContext.getString(R.string.notification_voicemail_text_format),
766                        PhoneNumberUtils.formatNumber(vmNumber));
767            }
768
769            Intent intent = new Intent(Intent.ACTION_CALL,
770                    Uri.fromParts("voicemail", "", null));
771            PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
772
773            Notification notification = new Notification(
774                    resId,  // icon
775                    null, // tickerText
776                    System.currentTimeMillis()  // Show the time the MWI notification came in,
777                                                // since we don't know the actual time of the
778                                                // most recent voicemail message
779                    );
780            notification.setLatestEventInfo(
781                    mContext,  // context
782                    notificationTitle,  // contentTitle
783                    notificationText,  // contentText
784                    pendingIntent  // contentIntent
785                    );
786            notification.defaults |= Notification.DEFAULT_SOUND;
787            notification.flags |= Notification.FLAG_NO_CLEAR;
788            configureLedNotification(notification);
789            mNotificationMgr.notify(VOICEMAIL_NOTIFICATION, notification);
790        } else {
791            mNotificationMgr.cancel(VOICEMAIL_NOTIFICATION);
792        }
793    }
794
795    /**
796     * Updates the message call forwarding indicator notification.
797     *
798     * @param visible true if there are messages waiting
799     */
800    /* package */ void updateCfi(boolean visible) {
801        if (DBG) log("updateCfi(): " + visible);
802        if (visible) {
803            // If Unconditional Call Forwarding (forward all calls) for VOICE
804            // is enabled, just show a notification.  We'll default to expanded
805            // view for now, so the there is less confusion about the icon.  If
806            // it is deemed too weird to have CF indications as expanded views,
807            // then we'll flip the flag back.
808
809            // TODO: We may want to take a look to see if the notification can
810            // display the target to forward calls to.  This will require some
811            // effort though, since there are multiple layers of messages that
812            // will need to propagate that information.
813
814            Notification notification;
815            final boolean showExpandedNotification = true;
816            if (showExpandedNotification) {
817                Intent intent = new Intent(Intent.ACTION_MAIN);
818                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
819                intent.setClassName("com.android.phone",
820                        "com.android.phone.CallFeaturesSetting");
821
822                notification = new Notification(
823                        mContext,  // context
824                        android.R.drawable.stat_sys_phone_call_forward,  // icon
825                        null, // tickerText
826                        0,  // The "timestamp" of this notification is meaningless;
827                            // we only care about whether CFI is currently on or not.
828                        mContext.getString(R.string.labelCF), // expandedTitle
829                        mContext.getString(R.string.sum_cfu_enabled_indicator),  // expandedText
830                        intent // contentIntent
831                        );
832
833            } else {
834                notification = new Notification(
835                        android.R.drawable.stat_sys_phone_call_forward,  // icon
836                        null,  // tickerText
837                        System.currentTimeMillis()  // when
838                        );
839            }
840
841            notification.flags |= Notification.FLAG_ONGOING_EVENT;  // also implies FLAG_NO_CLEAR
842
843            mNotificationMgr.notify(
844                    CALL_FORWARD_NOTIFICATION,
845                    notification);
846        } else {
847            mNotificationMgr.cancel(CALL_FORWARD_NOTIFICATION);
848        }
849    }
850
851    /**
852     * Shows the "data disconnected due to roaming" notification, which
853     * appears when you lose data connectivity because you're roaming and
854     * you have the "data roaming" feature turned off.
855     */
856    /* package */ void showDataDisconnectedRoaming() {
857        if (DBG) log("showDataDisconnectedRoaming()...");
858
859        Intent intent = new Intent(mContext,
860                                   Settings.class);  // "Mobile network settings" screen
861
862        Notification notification = new Notification(
863                mContext,  // context
864                android.R.drawable.stat_sys_warning,  // icon
865                null, // tickerText
866                System.currentTimeMillis(),
867                mContext.getString(R.string.roaming), // expandedTitle
868                mContext.getString(R.string.roaming_reenable_message),  // expandedText
869                intent // contentIntent
870                );
871        mNotificationMgr.notify(
872                DATA_DISCONNECTED_ROAMING_NOTIFICATION,
873                notification);
874    }
875
876    /**
877     * Turns off the "data disconnected due to roaming" notification.
878     */
879    /* package */ void hideDataDisconnectedRoaming() {
880        if (DBG) log("hideDataDisconnectedRoaming()...");
881        mNotificationMgr.cancel(DATA_DISCONNECTED_ROAMING_NOTIFICATION);
882    }
883
884    /**
885     * Display the network selection "no service" notification
886     * @param operator is the numeric operator number
887     */
888    private void showNetworkSelection(String operator) {
889        if (DBG) log("showNetworkSelection(" + operator + ")...");
890
891        String titleText = mContext.getString(
892                R.string.notification_network_selection_title);
893        String expandedText = mContext.getString(
894                R.string.notification_network_selection_text, operator);
895
896        Notification notification = new Notification();
897        notification.icon = com.android.internal.R.drawable.stat_sys_warning;
898        notification.when = 0;
899        notification.flags = Notification.FLAG_ONGOING_EVENT;
900        notification.tickerText = null;
901
902        // create the target network operators settings intent
903        Intent intent = new Intent(Intent.ACTION_MAIN);
904        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
905                Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
906        // Use NetworkSetting to handle the selection intent
907        intent.setComponent(new ComponentName("com.android.phone",
908                "com.android.phone.NetworkSetting"));
909        PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0);
910
911        notification.setLatestEventInfo(mContext, titleText, expandedText, pi);
912
913        mNotificationMgr.notify(SELECTED_OPERATOR_FAIL_NOTIFICATION, notification);
914    }
915
916    /**
917     * Turn off the network selection "no service" notification
918     */
919    private void cancelNetworkSelection() {
920        if (DBG) log("cancelNetworkSelection()...");
921        mNotificationMgr.cancel(SELECTED_OPERATOR_FAIL_NOTIFICATION);
922    }
923
924    /**
925     * Update notification about no service of user selected operator
926     *
927     * @param serviceState Phone service state
928     */
929    void updateNetworkSelection(int serviceState) {
930        if (mPhone.getPhoneType() == Phone.PHONE_TYPE_GSM) {
931            // get the shared preference of network_selection.
932            // empty is auto mode, otherwise it is the operator alpha name
933            // in case there is no operator name, check the operator numeric
934            SharedPreferences sp =
935                    PreferenceManager.getDefaultSharedPreferences(mContext);
936            String networkSelection =
937                    sp.getString(PhoneBase.NETWORK_SELECTION_NAME_KEY, "");
938            if (TextUtils.isEmpty(networkSelection)) {
939                networkSelection =
940                        sp.getString(PhoneBase.NETWORK_SELECTION_KEY, "");
941            }
942
943            if (DBG) log("updateNetworkSelection()..." + "state = " +
944                    serviceState + " new network " + networkSelection);
945
946            if (serviceState == ServiceState.STATE_OUT_OF_SERVICE
947                    && !TextUtils.isEmpty(networkSelection)) {
948                if (!mSelectedUnavailableNotify) {
949                    showNetworkSelection(networkSelection);
950                    mSelectedUnavailableNotify = true;
951                }
952            } else {
953                if (mSelectedUnavailableNotify) {
954                    cancelNetworkSelection();
955                    mSelectedUnavailableNotify = false;
956                }
957            }
958        }
959    }
960
961    /* package */ void postTransientNotification(int notifyId, CharSequence msg) {
962        if (mToast != null) {
963            mToast.cancel();
964        }
965
966        mToast = Toast.makeText(mContext, msg, Toast.LENGTH_LONG);
967        mToast.show();
968    }
969
970    private void log(String msg) {
971        Log.d(LOG_TAG, msg);
972    }
973}
974