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