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