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