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