NotificationMgr.java revision a50e10e2efadac960987eaadc0938c6f92d3ee90
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.SystemClock;
33import android.os.SystemProperties;
34import android.preference.PreferenceManager;
35import android.provider.CallLog.Calls;
36import android.provider.ContactsContract.PhoneLookup;
37import android.provider.Settings;
38import android.telephony.PhoneNumberUtils;
39import android.telephony.ServiceState;
40import android.text.TextUtils;
41import android.util.Log;
42import android.widget.RemoteViews;
43import android.widget.Toast;
44
45import com.android.internal.telephony.Call;
46import com.android.internal.telephony.CallerInfo;
47import com.android.internal.telephony.CallerInfoAsyncQuery;
48import com.android.internal.telephony.Connection;
49import com.android.internal.telephony.Phone;
50import com.android.internal.telephony.PhoneBase;
51import com.android.internal.telephony.CallManager;
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 =
60            (PhoneApp.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
61
62    private static final String[] CALL_LOG_PROJECTION = new String[] {
63        Calls._ID,
64        Calls.NUMBER,
65        Calls.DATE,
66        Calls.DURATION,
67        Calls.TYPE,
68    };
69
70    // notification types
71    static final int MISSED_CALL_NOTIFICATION = 1;
72    static final int IN_CALL_NOTIFICATION = 2;
73    static final int MMI_NOTIFICATION = 3;
74    static final int NETWORK_SELECTION_NOTIFICATION = 4;
75    static final int VOICEMAIL_NOTIFICATION = 5;
76    static final int CALL_FORWARD_NOTIFICATION = 6;
77    static final int DATA_DISCONNECTED_ROAMING_NOTIFICATION = 7;
78    static final int SELECTED_OPERATOR_FAIL_NOTIFICATION = 8;
79
80    private static NotificationMgr sMe = null;
81    private Phone mPhone;
82    private CallManager mCM;
83
84    private Context mContext;
85    private NotificationManager mNotificationMgr;
86    private StatusBarManager mStatusBar;
87    private StatusBarMgr mStatusBarMgr;
88    private Toast mToast;
89    private boolean mShowingSpeakerphoneIcon;
90    private boolean mShowingMuteIcon;
91
92    // used to track the missed call counter, default to 0.
93    private int mNumberMissedCalls = 0;
94
95    // Currently-displayed resource IDs for some status bar icons (or zero
96    // if no notification is active):
97    private int mInCallResId;
98
99    // used to track the notification of selected network unavailable
100    private boolean mSelectedUnavailableNotify = false;
101
102    // Retry params for the getVoiceMailNumber() call; see updateMwi().
103    private static final int MAX_VM_NUMBER_RETRIES = 5;
104    private static final int VM_NUMBER_RETRY_DELAY_MILLIS = 10000;
105    private int mVmNumberRetriesRemaining = MAX_VM_NUMBER_RETRIES;
106
107    // Query used to look up caller-id info for the "call log" notification.
108    private QueryHandler mQueryHandler = null;
109    private static final int CALL_LOG_TOKEN = -1;
110    private static final int CONTACT_TOKEN = -2;
111
112    NotificationMgr(Context context) {
113        mContext = context;
114        mNotificationMgr = (NotificationManager)
115            context.getSystemService(Context.NOTIFICATION_SERVICE);
116
117        mStatusBar = (StatusBarManager) context.getSystemService(Context.STATUS_BAR_SERVICE);
118
119        PhoneApp app = PhoneApp.getInstance();
120        mPhone = app.phone;
121        mCM = app.mCM;
122    }
123
124    static void init(Context context) {
125        sMe = new NotificationMgr(context);
126
127        // update the notifications that need to be touched at startup.
128        sMe.updateNotificationsAtStartup();
129    }
130
131    static NotificationMgr getDefault() {
132        return sMe;
133    }
134
135    /**
136     * Class that controls the status bar.  This class maintains a set
137     * of state and acts as an interface between the Phone process and
138     * the Status bar.  All interaction with the status bar should be
139     * though the methods contained herein.
140     */
141
142    /**
143     * Factory method
144     */
145    StatusBarMgr getStatusBarMgr() {
146        if (mStatusBarMgr == null) {
147            mStatusBarMgr = new StatusBarMgr();
148        }
149        return mStatusBarMgr;
150    }
151
152    /**
153     * StatusBarMgr implementation
154     */
155    class StatusBarMgr {
156        // current settings
157        private boolean mIsNotificationEnabled = true;
158        private boolean mIsExpandedViewEnabled = true;
159
160        private StatusBarMgr () {
161        }
162
163        /**
164         * Sets the notification state (enable / disable
165         * vibrating notifications) for the status bar,
166         * updates the status bar service if there is a change.
167         * Independent of the remaining Status Bar
168         * functionality, including icons and expanded view.
169         */
170        void enableNotificationAlerts(boolean enable) {
171            if (mIsNotificationEnabled != enable) {
172                mIsNotificationEnabled = enable;
173                updateStatusBar();
174            }
175        }
176
177        /**
178         * Sets the ability to expand the notifications for the
179         * status bar, updates the status bar service if there
180         * is a change. Independent of the remaining Status Bar
181         * functionality, including icons and notification
182         * alerts.
183         */
184        void enableExpandedView(boolean enable) {
185            if (mIsExpandedViewEnabled != enable) {
186                mIsExpandedViewEnabled = enable;
187                updateStatusBar();
188            }
189        }
190
191        /**
192         * Method to synchronize status bar state with our current
193         * state.
194         */
195        void updateStatusBar() {
196            int state = StatusBarManager.DISABLE_NONE;
197
198            if (!mIsExpandedViewEnabled) {
199                state |= StatusBarManager.DISABLE_EXPAND;
200            }
201
202            if (!mIsNotificationEnabled) {
203                state |= StatusBarManager.DISABLE_NOTIFICATION_ALERTS;
204            }
205
206            // send the message to the status bar manager.
207            if (DBG) log("updating status bar state: " + state);
208            mStatusBar.disable(state);
209        }
210    }
211
212    /**
213     * Makes sure phone-related notifications are up to date on a
214     * freshly-booted device.
215     */
216    private void updateNotificationsAtStartup() {
217        if (DBG) log("updateNotificationsAtStartup()...");
218
219        // instantiate query handler
220        mQueryHandler = new QueryHandler(mContext.getContentResolver());
221
222        // setup query spec, look for all Missed calls that are new.
223        StringBuilder where = new StringBuilder("type=");
224        where.append(Calls.MISSED_TYPE);
225        where.append(" AND new=1");
226
227        // start the query
228        if (DBG) log("- start call log query...");
229        mQueryHandler.startQuery(CALL_LOG_TOKEN, null, Calls.CONTENT_URI,  CALL_LOG_PROJECTION,
230                where.toString(), null, Calls.DEFAULT_SORT_ORDER);
231
232        // Update (or cancel) the in-call notification
233        if (DBG) log("- updating in-call notification at startup...");
234        updateInCallNotification();
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.defaults |= Notification.DEFAULT_LIGHTS;
372    }
373
374    /**
375     * Displays a notification about a missed call.
376     *
377     * @param nameOrNumber either the contact name, or the phone number if no contact
378     * @param label the label of the number if nameOrNumber is a name, null if it is a number
379     */
380    void notifyMissedCall(String name, String number, String label, long date) {
381        // title resource id
382        int titleResId;
383        // the text in the notification's line 1 and 2.
384        String expandedText, callName;
385
386        // increment number of missed calls.
387        mNumberMissedCalls++;
388
389        // get the name for the ticker text
390        // i.e. "Missed call from <caller name or number>"
391        if (name != null && TextUtils.isGraphic(name)) {
392            callName = name;
393        } else if (!TextUtils.isEmpty(number)){
394            callName = number;
395        } else {
396            // use "unknown" if the caller is unidentifiable.
397            callName = mContext.getString(R.string.unknown);
398        }
399
400        // display the first line of the notification:
401        // 1 missed call: call name
402        // more than 1 missed call: <number of calls> + "missed calls"
403        if (mNumberMissedCalls == 1) {
404            titleResId = R.string.notification_missedCallTitle;
405            expandedText = callName;
406        } else {
407            titleResId = R.string.notification_missedCallsTitle;
408            expandedText = mContext.getString(R.string.notification_missedCallsMsg,
409                    mNumberMissedCalls);
410        }
411
412        // create the target call log intent
413        final Intent intent = PhoneApp.createCallLogIntent();
414
415        // make the notification
416        Notification note = new Notification(mContext, // context
417                android.R.drawable.stat_notify_missed_call, // icon
418                mContext.getString(R.string.notification_missedCallTicker, callName), // tickerText
419                date, // when
420                mContext.getText(titleResId), // expandedTitle
421                expandedText, // expandedText
422                intent // contentIntent
423                );
424        configureLedNotification(note);
425        mNotificationMgr.notify(MISSED_CALL_NOTIFICATION, note);
426    }
427
428    void cancelMissedCallNotification() {
429        // reset the number of missed calls to 0.
430        mNumberMissedCalls = 0;
431        mNotificationMgr.cancel(MISSED_CALL_NOTIFICATION);
432    }
433
434    void notifySpeakerphone() {
435        if (!mShowingSpeakerphoneIcon) {
436            mStatusBar.setIcon("speakerphone", android.R.drawable.stat_sys_speakerphone, 0);
437            mShowingSpeakerphoneIcon = true;
438        }
439    }
440
441    void cancelSpeakerphone() {
442        if (mShowingSpeakerphoneIcon) {
443            mStatusBar.removeIcon("speakerphone");
444            mShowingSpeakerphoneIcon = false;
445        }
446    }
447
448    /**
449     * Calls either notifySpeakerphone() or cancelSpeakerphone() based on
450     * the actual current state of the speaker.
451     */
452    void updateSpeakerNotification() {
453        AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
454
455        if ((mPhone.getState() == Phone.State.OFFHOOK) && audioManager.isSpeakerphoneOn()) {
456            if (DBG) log("updateSpeakerNotification: speaker ON");
457            notifySpeakerphone();
458        } else {
459            if (DBG) log("updateSpeakerNotification: speaker OFF (or not offhook)");
460            cancelSpeakerphone();
461        }
462    }
463
464    private void notifyMute() {
465        if (mShowingMuteIcon) {
466            mStatusBar.setIcon("mute", android.R.drawable.stat_notify_call_mute, 0);
467            mShowingMuteIcon = true;
468        }
469    }
470
471    private void cancelMute() {
472        if (mShowingMuteIcon) {
473            mStatusBar.removeIcon("mute");
474            mShowingMuteIcon = false;
475        }
476    }
477
478    /**
479     * Calls either notifyMute() or cancelMute() based on
480     * the actual current mute state of the Phone.
481     */
482    void updateMuteNotification() {
483        if ((mCM.getState() == Phone.State.OFFHOOK) && PhoneUtils.getMute()) {
484            if (DBG) log("updateMuteNotification: MUTED");
485            notifyMute();
486        } else {
487            if (DBG) log("updateMuteNotification: not muted (or not offhook)");
488            cancelMute();
489        }
490    }
491
492    /**
493     * Updates the phone app's status bar notification based on the
494     * current telephony state, or cancels the notification if the phone
495     * is totally idle.
496     */
497    void updateInCallNotification() {
498        int resId;
499        if (DBG) log("updateInCallNotification()...");
500
501        if (mCM.getState() == Phone.State.IDLE) {
502            cancelInCall();
503            return;
504        }
505
506        final boolean hasRingingCall = mCM.hasActiveRingingCall();
507        final boolean hasActiveCall = mCM.hasActiveFgCall();
508        final boolean hasHoldingCall = mCM.hasActiveBgCall();
509
510        // Display the appropriate icon in the status bar,
511        // based on the current phone and/or bluetooth state.
512
513        boolean enhancedVoicePrivacy = PhoneApp.getInstance().notifier.getCdmaVoicePrivacyState();
514        if (DBG) log("updateInCallNotification: enhancedVoicePrivacy = " + enhancedVoicePrivacy);
515
516        if (hasRingingCall) {
517            // There's an incoming ringing call.
518            resId = R.drawable.stat_sys_phone_call_ringing;
519        } else if (!hasActiveCall && hasHoldingCall) {
520            // There's only one call, and it's on hold.
521            if (enhancedVoicePrivacy) {
522                resId = R.drawable.stat_sys_vp_phone_call_on_hold;
523            } else {
524                resId = R.drawable.stat_sys_phone_call_on_hold;
525            }
526        } else if (PhoneApp.getInstance().showBluetoothIndication()) {
527            // Bluetooth is active.
528            if (enhancedVoicePrivacy) {
529                resId = R.drawable.stat_sys_vp_phone_call_bluetooth;
530            } else {
531                resId = R.drawable.stat_sys_phone_call_bluetooth;
532            }
533        } else {
534            if (enhancedVoicePrivacy) {
535                resId = R.drawable.stat_sys_vp_phone_call;
536            } else {
537                resId = R.drawable.stat_sys_phone_call;
538            }
539        }
540
541        // Note we can't just bail out now if (resId == mInCallResId),
542        // since even if the status icon hasn't changed, some *other*
543        // notification-related info may be different from the last time
544        // we were here (like the caller-id info of the foreground call,
545        // if the user swapped calls...)
546
547        if (DBG) log("- Updating status bar icon: resId = " + resId);
548        mInCallResId = resId;
549
550        // The icon in the expanded view is the same as in the status bar.
551        int expandedViewIcon = mInCallResId;
552
553        // Even if both lines are in use, we only show a single item in
554        // the expanded Notifications UI.  It's labeled "Ongoing call"
555        // (or "On hold" if there's only one call, and it's on hold.)
556        // Also, we don't have room to display caller-id info from two
557        // different calls.  So if both lines are in use, display info
558        // from the foreground call.  And if there's a ringing call,
559        // display that regardless of the state of the other calls.
560
561        Call currentCall;
562        if (hasRingingCall) {
563            currentCall = mCM.getFirstActiveRingingCall();
564        } else if (hasActiveCall) {
565            currentCall = mCM.getActiveFgCall();
566        } else {
567            currentCall = mCM.getFirstActiveBgCall();
568        }
569        Connection currentConn = currentCall.getEarliestConnection();
570
571        Notification notification = new Notification();
572        notification.icon = mInCallResId;
573        notification.flags |= Notification.FLAG_ONGOING_EVENT;
574
575        // PendingIntent that can be used to launch the InCallScreen.  The
576        // system fires off this intent if the user pulls down the windowshade
577        // and clicks the notification's expanded view.  It's also used to
578        // launch the InCallScreen immediately when when there's an incoming
579        // call (see the "fullScreenIntent" field below).
580        PendingIntent inCallPendingIntent =
581                PendingIntent.getActivity(mContext, 0,
582                                          PhoneApp.createInCallIntent(), 0);
583        notification.contentIntent = inCallPendingIntent;
584
585        // When expanded, the "Ongoing call" notification is (visually)
586        // different from most other Notifications, so we need to use a
587        // custom view hierarchy.
588        // Our custom view, which includes an icon (either "ongoing call" or
589        // "on hold") and 2 lines of text: (1) the label (either "ongoing
590        // call" with time counter, or "on hold), and (2) the compact name of
591        // the current Connection.
592        RemoteViews contentView = new RemoteViews(mContext.getPackageName(),
593                                                   R.layout.ongoing_call_notification);
594        contentView.setImageViewResource(R.id.icon, expandedViewIcon);
595
596        // if the connection is valid, then build what we need for the
597        // first line of notification information, and start the chronometer.
598        // Otherwise, don't bother and just stick with line 2.
599        if (currentConn != null) {
600            // Determine the "start time" of the current connection, in terms
601            // of the SystemClock.elapsedRealtime() timebase (which is what
602            // the Chronometer widget needs.)
603            //   We can't use currentConn.getConnectTime(), because (1) that's
604            // in the currentTimeMillis() time base, and (2) it's zero when
605            // the phone first goes off hook, since the getConnectTime counter
606            // doesn't start until the DIALING -> ACTIVE transition.
607            //   Instead we start with the current connection's duration,
608            // and translate that into the elapsedRealtime() timebase.
609            long callDurationMsec = currentConn.getDurationMillis();
610            long chronometerBaseTime = SystemClock.elapsedRealtime() - callDurationMsec;
611
612            // Line 1 of the expanded view (in bold text):
613            String expandedViewLine1;
614            if (hasRingingCall) {
615                // Incoming call is ringing.
616                // Note this isn't a format string!  (We want "Incoming call"
617                // here, not "Incoming call (1:23)".)  But that's OK; if you
618                // call String.format() with more arguments than format
619                // specifiers, the extra arguments are ignored.
620                expandedViewLine1 = mContext.getString(R.string.notification_incoming_call);
621            } else if (hasHoldingCall && !hasActiveCall) {
622                // Only one call, and it's on hold.
623                // Note this isn't a format string either (see comment above.)
624                expandedViewLine1 = mContext.getString(R.string.notification_on_hold);
625            } else {
626                // Normal ongoing call.
627                // Format string with a "%s" where the current call time should go.
628                expandedViewLine1 = mContext.getString(R.string.notification_ongoing_call_format);
629            }
630
631            if (DBG) log("- Updating expanded view: line 1 '" + expandedViewLine1 + "'");
632
633            // Text line #1 is actually a Chronometer, not a plain TextView.
634            // We format the elapsed time of the current call into a line like
635            // "Ongoing call (01:23)".
636            contentView.setChronometer(R.id.text1,
637                                       chronometerBaseTime,
638                                       expandedViewLine1,
639                                       true);
640        } else if (DBG) {
641            Log.w(LOG_TAG, "updateInCallNotification: null connection, can't set exp view line 1.");
642        }
643
644        // display conference call string if this call is a conference
645        // call, otherwise display the connection information.
646
647        // Line 2 of the expanded view (smaller text).  This is usually a
648        // contact name or phone number.
649        String expandedViewLine2 = "";
650        // TODO: it may not make sense for every point to make separate
651        // checks for isConferenceCall, so we need to think about
652        // possibly including this in startGetCallerInfo or some other
653        // common point.
654        if (PhoneUtils.isConferenceCall(currentCall)) {
655            // if this is a conference call, just use that as the caller name.
656            expandedViewLine2 = mContext.getString(R.string.card_title_conf_call);
657        } else {
658            // If necessary, start asynchronous query to do the caller-id lookup.
659            PhoneUtils.CallerInfoToken cit =
660                PhoneUtils.startGetCallerInfo(mContext, currentCall, this, this);
661            expandedViewLine2 = PhoneUtils.getCompactNameFromCallerInfo(cit.currentInfo, mContext);
662            // Note: For an incoming call, the very first time we get here we
663            // won't have a contact name yet, since we only just started the
664            // caller-id query.  So expandedViewLine2 will start off as a raw
665            // phone number, but we'll update it very quickly when the query
666            // completes (see onQueryComplete() below.)
667        }
668
669        if (DBG) log("- Updating expanded view: line 2 '" + expandedViewLine2 + "'");
670        contentView.setTextViewText(R.id.text2, expandedViewLine2);
671        notification.contentView = contentView;
672
673        // TODO: We also need to *update* this notification in some cases,
674        // like when a call ends on one line but the other is still in use
675        // (ie. make sure the caller info here corresponds to the active
676        // line), and maybe even when the user swaps calls (ie. if we only
677        // show info here for the "current active call".)
678
679        // Activate a couple of special Notification features if an
680        // incoming call is ringing:
681        if (hasRingingCall) {
682            // We actually want to launch the incoming call UI at this point
683            // (rather than just posting a notification to the status bar).
684            // Setting fullScreenIntent will cause the InCallScreen to be
685            // launched immediately.
686            notification.fullScreenIntent = inCallPendingIntent;
687        }
688
689        if (DBG) log("Notifying IN_CALL_NOTIFICATION: " + notification);
690        mNotificationMgr.notify(IN_CALL_NOTIFICATION,
691                                notification);
692
693        // Finally, refresh the mute and speakerphone notifications (since
694        // some phone state changes can indirectly affect the mute and/or
695        // speaker state).
696        updateSpeakerNotification();
697        updateMuteNotification();
698    }
699
700    /**
701     * Implemented for CallerInfoAsyncQuery.OnQueryCompleteListener interface.
702     * refreshes the contentView when called.
703     */
704    public void onQueryComplete(int token, Object cookie, CallerInfo ci){
705        if (DBG) log("CallerInfo query complete (for NotificationMgr), "
706                     + "updating in-call notification..");
707        if (DBG) log("- cookie: " + cookie);
708        if (DBG) log("- ci: " + ci);
709
710        if (cookie == this) {
711            // Ok, this is the caller-id query we fired off in
712            // updateInCallNotification(), presumably when an incoming call
713            // first appeared.  If the caller-id info matched any contacts,
714            // compactName should now be a real person name rather than a raw
715            // phone number:
716            if (DBG) log("- compactName is now: "
717                         + PhoneUtils.getCompactNameFromCallerInfo(ci, mContext));
718
719            // Now that our CallerInfo object has been fully filled-in,
720            // refresh the in-call notification.
721            if (DBG) log("- updating notification after query complete...");
722            updateInCallNotification();
723        } else {
724            Log.w(LOG_TAG, "onQueryComplete: caller-id query from unknown source! "
725                  + "cookie = " + cookie);
726        }
727    }
728
729    private void cancelInCall() {
730        if (DBG) log("cancelInCall()...");
731        cancelMute();
732        cancelSpeakerphone();
733        mNotificationMgr.cancel(IN_CALL_NOTIFICATION);
734        mInCallResId = 0;
735    }
736
737    void cancelCallInProgressNotification() {
738        if (DBG) log("cancelCallInProgressNotification()...");
739        if (mInCallResId == 0) {
740            return;
741        }
742
743        if (DBG) log("cancelCallInProgressNotification: " + mInCallResId);
744        cancelInCall();
745    }
746
747    /**
748     * Updates the message waiting indicator (voicemail) notification.
749     *
750     * @param visible true if there are messages waiting
751     */
752    /* package */ void updateMwi(boolean visible) {
753        if (DBG) log("updateMwi(): " + visible);
754        if (visible) {
755            int resId = android.R.drawable.stat_notify_voicemail;
756
757            // This Notification can get a lot fancier once we have more
758            // information about the current voicemail messages.
759            // (For example, the current voicemail system can't tell
760            // us the caller-id or timestamp of a message, or tell us the
761            // message count.)
762
763            // But for now, the UI is ultra-simple: if the MWI indication
764            // is supposed to be visible, just show a single generic
765            // notification.
766
767            String notificationTitle = mContext.getString(R.string.notification_voicemail_title);
768            String vmNumber = mPhone.getVoiceMailNumber();
769            if (DBG) log("- got vm number: '" + vmNumber + "'");
770
771            // Watch out: vmNumber may be null, for two possible reasons:
772            //
773            //   (1) This phone really has no voicemail number
774            //
775            //   (2) This phone *does* have a voicemail number, but
776            //       the SIM isn't ready yet.
777            //
778            // Case (2) *does* happen in practice if you have voicemail
779            // messages when the device first boots: we get an MWI
780            // notification as soon as we register on the network, but the
781            // SIM hasn't finished loading yet.
782            //
783            // So handle case (2) by retrying the lookup after a short
784            // delay.
785
786            if ((vmNumber == null) && !mPhone.getIccRecordsLoaded()) {
787                if (DBG) log("- Null vm number: SIM records not loaded (yet)...");
788
789                // TODO: rather than retrying after an arbitrary delay, it
790                // would be cleaner to instead just wait for a
791                // SIM_RECORDS_LOADED notification.
792                // (Unfortunately right now there's no convenient way to
793                // get that notification in phone app code.  We'd first
794                // want to add a call like registerForSimRecordsLoaded()
795                // to Phone.java and GSMPhone.java, and *then* we could
796                // listen for that in the CallNotifier class.)
797
798                // Limit the number of retries (in case the SIM is broken
799                // or missing and can *never* load successfully.)
800                if (mVmNumberRetriesRemaining-- > 0) {
801                    if (DBG) log("  - Retrying in " + VM_NUMBER_RETRY_DELAY_MILLIS + " msec...");
802                    PhoneApp.getInstance().notifier.sendMwiChangedDelayed(
803                            VM_NUMBER_RETRY_DELAY_MILLIS);
804                    return;
805                } else {
806                    Log.w(LOG_TAG, "NotificationMgr.updateMwi: getVoiceMailNumber() failed after "
807                          + MAX_VM_NUMBER_RETRIES + " retries; giving up.");
808                    // ...and continue with vmNumber==null, just as if the
809                    // phone had no VM number set up in the first place.
810                }
811            }
812
813            if (TelephonyCapabilities.supportsVoiceMessageCount(mPhone)) {
814                int vmCount = mPhone.getVoiceMessageCount();
815                String titleFormat = mContext.getString(R.string.notification_voicemail_title_count);
816                notificationTitle = String.format(titleFormat, vmCount);
817            }
818
819            String notificationText;
820            if (TextUtils.isEmpty(vmNumber)) {
821                notificationText = mContext.getString(
822                        R.string.notification_voicemail_no_vm_number);
823            } else {
824                notificationText = String.format(
825                        mContext.getString(R.string.notification_voicemail_text_format),
826                        PhoneNumberUtils.formatNumber(vmNumber));
827            }
828
829            Intent intent = new Intent(Intent.ACTION_CALL,
830                    Uri.fromParts("voicemail", "", null));
831            PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
832
833            Notification notification = new Notification(
834                    resId,  // icon
835                    null, // tickerText
836                    System.currentTimeMillis()  // Show the time the MWI notification came in,
837                                                // since we don't know the actual time of the
838                                                // most recent voicemail message
839                    );
840            notification.setLatestEventInfo(
841                    mContext,  // context
842                    notificationTitle,  // contentTitle
843                    notificationText,  // contentText
844                    pendingIntent  // contentIntent
845                    );
846            notification.defaults |= Notification.DEFAULT_SOUND;
847            notification.flags |= Notification.FLAG_NO_CLEAR;
848            configureLedNotification(notification);
849            mNotificationMgr.notify(VOICEMAIL_NOTIFICATION, notification);
850        } else {
851            mNotificationMgr.cancel(VOICEMAIL_NOTIFICATION);
852        }
853    }
854
855    /**
856     * Updates the message call forwarding indicator notification.
857     *
858     * @param visible true if there are messages waiting
859     */
860    /* package */ void updateCfi(boolean visible) {
861        if (DBG) log("updateCfi(): " + visible);
862        if (visible) {
863            // If Unconditional Call Forwarding (forward all calls) for VOICE
864            // is enabled, just show a notification.  We'll default to expanded
865            // view for now, so the there is less confusion about the icon.  If
866            // it is deemed too weird to have CF indications as expanded views,
867            // then we'll flip the flag back.
868
869            // TODO: We may want to take a look to see if the notification can
870            // display the target to forward calls to.  This will require some
871            // effort though, since there are multiple layers of messages that
872            // will need to propagate that information.
873
874            Notification notification;
875            final boolean showExpandedNotification = true;
876            if (showExpandedNotification) {
877                Intent intent = new Intent(Intent.ACTION_MAIN);
878                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
879                intent.setClassName("com.android.phone",
880                        "com.android.phone.CallFeaturesSetting");
881
882                notification = new Notification(
883                        mContext,  // context
884                        R.drawable.stat_sys_phone_call_forward,  // icon
885                        null, // tickerText
886                        0,  // The "timestamp" of this notification is meaningless;
887                            // we only care about whether CFI is currently on or not.
888                        mContext.getString(R.string.labelCF), // expandedTitle
889                        mContext.getString(R.string.sum_cfu_enabled_indicator),  // expandedText
890                        intent // contentIntent
891                        );
892
893            } else {
894                notification = new Notification(
895                        R.drawable.stat_sys_phone_call_forward,  // icon
896                        null,  // tickerText
897                        System.currentTimeMillis()  // when
898                        );
899            }
900
901            notification.flags |= Notification.FLAG_ONGOING_EVENT;  // also implies FLAG_NO_CLEAR
902
903            mNotificationMgr.notify(
904                    CALL_FORWARD_NOTIFICATION,
905                    notification);
906        } else {
907            mNotificationMgr.cancel(CALL_FORWARD_NOTIFICATION);
908        }
909    }
910
911    /**
912     * Shows the "data disconnected due to roaming" notification, which
913     * appears when you lose data connectivity because you're roaming and
914     * you have the "data roaming" feature turned off.
915     */
916    /* package */ void showDataDisconnectedRoaming() {
917        if (DBG) log("showDataDisconnectedRoaming()...");
918
919        Intent intent = new Intent(mContext,
920                                   Settings.class);  // "Mobile network settings" screen
921
922        Notification notification = new Notification(
923                mContext,  // context
924                android.R.drawable.stat_sys_warning,  // icon
925                null, // tickerText
926                System.currentTimeMillis(),
927                mContext.getString(R.string.roaming), // expandedTitle
928                mContext.getString(R.string.roaming_reenable_message),  // expandedText
929                intent // contentIntent
930                );
931        mNotificationMgr.notify(
932                DATA_DISCONNECTED_ROAMING_NOTIFICATION,
933                notification);
934    }
935
936    /**
937     * Turns off the "data disconnected due to roaming" notification.
938     */
939    /* package */ void hideDataDisconnectedRoaming() {
940        if (DBG) log("hideDataDisconnectedRoaming()...");
941        mNotificationMgr.cancel(DATA_DISCONNECTED_ROAMING_NOTIFICATION);
942    }
943
944    /**
945     * Display the network selection "no service" notification
946     * @param operator is the numeric operator number
947     */
948    private void showNetworkSelection(String operator) {
949        if (DBG) log("showNetworkSelection(" + operator + ")...");
950
951        String titleText = mContext.getString(
952                R.string.notification_network_selection_title);
953        String expandedText = mContext.getString(
954                R.string.notification_network_selection_text, operator);
955
956        Notification notification = new Notification();
957        notification.icon = android.R.drawable.stat_sys_warning;
958        notification.when = 0;
959        notification.flags = Notification.FLAG_ONGOING_EVENT;
960        notification.tickerText = null;
961
962        // create the target network operators settings intent
963        Intent intent = new Intent(Intent.ACTION_MAIN);
964        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
965                Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
966        // Use NetworkSetting to handle the selection intent
967        intent.setComponent(new ComponentName("com.android.phone",
968                "com.android.phone.NetworkSetting"));
969        PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0);
970
971        notification.setLatestEventInfo(mContext, titleText, expandedText, pi);
972
973        mNotificationMgr.notify(SELECTED_OPERATOR_FAIL_NOTIFICATION, notification);
974    }
975
976    /**
977     * Turn off the network selection "no service" notification
978     */
979    private void cancelNetworkSelection() {
980        if (DBG) log("cancelNetworkSelection()...");
981        mNotificationMgr.cancel(SELECTED_OPERATOR_FAIL_NOTIFICATION);
982    }
983
984    /**
985     * Update notification about no service of user selected operator
986     *
987     * @param serviceState Phone service state
988     */
989    void updateNetworkSelection(int serviceState) {
990        if (TelephonyCapabilities.supportsNetworkSelection(mPhone)) {
991            // get the shared preference of network_selection.
992            // empty is auto mode, otherwise it is the operator alpha name
993            // in case there is no operator name, check the operator numeric
994            SharedPreferences sp =
995                    PreferenceManager.getDefaultSharedPreferences(mContext);
996            String networkSelection =
997                    sp.getString(PhoneBase.NETWORK_SELECTION_NAME_KEY, "");
998            if (TextUtils.isEmpty(networkSelection)) {
999                networkSelection =
1000                        sp.getString(PhoneBase.NETWORK_SELECTION_KEY, "");
1001            }
1002
1003            if (DBG) log("updateNetworkSelection()..." + "state = " +
1004                    serviceState + " new network " + networkSelection);
1005
1006            if (serviceState == ServiceState.STATE_OUT_OF_SERVICE
1007                    && !TextUtils.isEmpty(networkSelection)) {
1008                if (!mSelectedUnavailableNotify) {
1009                    showNetworkSelection(networkSelection);
1010                    mSelectedUnavailableNotify = true;
1011                }
1012            } else {
1013                if (mSelectedUnavailableNotify) {
1014                    cancelNetworkSelection();
1015                    mSelectedUnavailableNotify = false;
1016                }
1017            }
1018        }
1019    }
1020
1021    /* package */ void postTransientNotification(int notifyId, CharSequence msg) {
1022        if (mToast != null) {
1023            mToast.cancel();
1024        }
1025
1026        mToast = Toast.makeText(mContext, msg, Toast.LENGTH_LONG);
1027        mToast.show();
1028    }
1029
1030    private void log(String msg) {
1031        Log.d(LOG_TAG, msg);
1032    }
1033}
1034