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