• 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.ContentUris;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.SharedPreferences;
30 import android.database.Cursor;
31 import android.graphics.Bitmap;
32 import android.graphics.drawable.BitmapDrawable;
33 import android.graphics.drawable.Drawable;
34 import android.media.AudioManager;
35 import android.net.Uri;
36 import android.os.PowerManager;
37 import android.os.SystemProperties;
38 import android.preference.PreferenceManager;
39 import android.provider.CallLog.Calls;
40 import android.provider.ContactsContract.Contacts;
41 import android.provider.ContactsContract.PhoneLookup;
42 import android.provider.Settings;
43 import android.telephony.PhoneNumberUtils;
44 import android.telephony.ServiceState;
45 import android.text.TextUtils;
46 import android.util.Log;
47 import android.widget.ImageView;
48 import android.widget.Toast;
49 
50 import com.android.internal.telephony.Call;
51 import com.android.internal.telephony.CallManager;
52 import com.android.internal.telephony.CallerInfo;
53 import com.android.internal.telephony.CallerInfoAsyncQuery;
54 import com.android.internal.telephony.Connection;
55 import com.android.internal.telephony.Phone;
56 import com.android.internal.telephony.PhoneBase;
57 import com.android.internal.telephony.PhoneConstants;
58 import com.android.internal.telephony.TelephonyCapabilities;
59 
60 
61 /**
62  * NotificationManager-related utility code for the Phone app.
63  *
64  * This is a singleton object which acts as the interface to the
65  * framework's NotificationManager, and is used to display status bar
66  * icons and control other status bar-related behavior.
67  *
68  * @see PhoneGlobals.notificationMgr
69  */
70 public class NotificationMgr implements CallerInfoAsyncQuery.OnQueryCompleteListener{
71     private static final String LOG_TAG = "NotificationMgr";
72     private static final boolean DBG =
73             (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
74     // Do not check in with VDBG = true, since that may write PII to the system log.
75     private static final boolean VDBG = false;
76 
77     private static final String[] CALL_LOG_PROJECTION = new String[] {
78         Calls._ID,
79         Calls.NUMBER,
80         Calls.DATE,
81         Calls.DURATION,
82         Calls.TYPE,
83     };
84 
85     // notification types
86     static final int MISSED_CALL_NOTIFICATION = 1;
87     static final int IN_CALL_NOTIFICATION = 2;
88     static final int MMI_NOTIFICATION = 3;
89     static final int NETWORK_SELECTION_NOTIFICATION = 4;
90     static final int VOICEMAIL_NOTIFICATION = 5;
91     static final int CALL_FORWARD_NOTIFICATION = 6;
92     static final int DATA_DISCONNECTED_ROAMING_NOTIFICATION = 7;
93     static final int SELECTED_OPERATOR_FAIL_NOTIFICATION = 8;
94 
95     /** The singleton NotificationMgr instance. */
96     private static NotificationMgr sInstance;
97 
98     private PhoneGlobals mApp;
99     private Phone mPhone;
100     private CallManager mCM;
101 
102     private Context mContext;
103     private NotificationManager mNotificationManager;
104     private StatusBarManager mStatusBarManager;
105     private PowerManager mPowerManager;
106     private Toast mToast;
107     private boolean mShowingSpeakerphoneIcon;
108     private boolean mShowingMuteIcon;
109 
110     public StatusBarHelper statusBarHelper;
111 
112     // used to track the missed call counter, default to 0.
113     private int mNumberMissedCalls = 0;
114 
115     // Currently-displayed resource IDs for some status bar icons (or zero
116     // if no notification is active):
117     private int mInCallResId;
118 
119     // used to track the notification of selected network unavailable
120     private boolean mSelectedUnavailableNotify = false;
121 
122     // Retry params for the getVoiceMailNumber() call; see updateMwi().
123     private static final int MAX_VM_NUMBER_RETRIES = 5;
124     private static final int VM_NUMBER_RETRY_DELAY_MILLIS = 10000;
125     private int mVmNumberRetriesRemaining = MAX_VM_NUMBER_RETRIES;
126 
127     // Query used to look up caller-id info for the "call log" notification.
128     private QueryHandler mQueryHandler = null;
129     private static final int CALL_LOG_TOKEN = -1;
130     private static final int CONTACT_TOKEN = -2;
131 
132     /**
133      * Private constructor (this is a singleton).
134      * @see init()
135      */
NotificationMgr(PhoneGlobals app)136     private NotificationMgr(PhoneGlobals app) {
137         mApp = app;
138         mContext = app;
139         mNotificationManager =
140                 (NotificationManager) app.getSystemService(Context.NOTIFICATION_SERVICE);
141         mStatusBarManager =
142                 (StatusBarManager) app.getSystemService(Context.STATUS_BAR_SERVICE);
143         mPowerManager =
144                 (PowerManager) app.getSystemService(Context.POWER_SERVICE);
145         mPhone = app.phone;  // TODO: better style to use mCM.getDefaultPhone() everywhere instead
146         mCM = app.mCM;
147         statusBarHelper = new StatusBarHelper();
148     }
149 
150     /**
151      * Initialize the singleton NotificationMgr instance.
152      *
153      * This is only done once, at startup, from PhoneApp.onCreate().
154      * From then on, the NotificationMgr instance is available via the
155      * PhoneApp's public "notificationMgr" field, which is why there's no
156      * getInstance() method here.
157      */
init(PhoneGlobals app)158     /* package */ static NotificationMgr init(PhoneGlobals app) {
159         synchronized (NotificationMgr.class) {
160             if (sInstance == null) {
161                 sInstance = new NotificationMgr(app);
162                 // Update the notifications that need to be touched at startup.
163                 sInstance.updateNotificationsAtStartup();
164             } else {
165                 Log.wtf(LOG_TAG, "init() called multiple times!  sInstance = " + sInstance);
166             }
167             return sInstance;
168         }
169     }
170 
171     /**
172      * Helper class that's a wrapper around the framework's
173      * StatusBarManager.disable() API.
174      *
175      * This class is used to control features like:
176      *
177      *   - Disabling the status bar "notification windowshade"
178      *     while the in-call UI is up
179      *
180      *   - Disabling notification alerts (audible or vibrating)
181      *     while a phone call is active
182      *
183      *   - Disabling navigation via the system bar (the "soft buttons" at
184      *     the bottom of the screen on devices with no hard buttons)
185      *
186      * We control these features through a single point of control to make
187      * sure that the various StatusBarManager.disable() calls don't
188      * interfere with each other.
189      */
190     public class StatusBarHelper {
191         // Current desired state of status bar / system bar behavior
192         private boolean mIsNotificationEnabled = true;
193         private boolean mIsExpandedViewEnabled = true;
194         private boolean mIsSystemBarNavigationEnabled = true;
195 
StatusBarHelper()196         private StatusBarHelper () {
197         }
198 
199         /**
200          * Enables or disables auditory / vibrational alerts.
201          *
202          * (We disable these any time a voice call is active, regardless
203          * of whether or not the in-call UI is visible.)
204          */
enableNotificationAlerts(boolean enable)205         public void enableNotificationAlerts(boolean enable) {
206             if (mIsNotificationEnabled != enable) {
207                 mIsNotificationEnabled = enable;
208                 updateStatusBar();
209             }
210         }
211 
212         /**
213          * Enables or disables the expanded view of the status bar
214          * (i.e. the ability to pull down the "notification windowshade").
215          *
216          * (This feature is disabled by the InCallScreen while the in-call
217          * UI is active.)
218          */
enableExpandedView(boolean enable)219         public void enableExpandedView(boolean enable) {
220             if (mIsExpandedViewEnabled != enable) {
221                 mIsExpandedViewEnabled = enable;
222                 updateStatusBar();
223             }
224         }
225 
226         /**
227          * Enables or disables the navigation via the system bar (the
228          * "soft buttons" at the bottom of the screen)
229          *
230          * (This feature is disabled while an incoming call is ringing,
231          * because it's easy to accidentally touch the system bar while
232          * pulling the phone out of your pocket.)
233          */
enableSystemBarNavigation(boolean enable)234         public void enableSystemBarNavigation(boolean enable) {
235             if (mIsSystemBarNavigationEnabled != enable) {
236                 mIsSystemBarNavigationEnabled = enable;
237                 updateStatusBar();
238             }
239         }
240 
241         /**
242          * Updates the status bar to reflect the current desired state.
243          */
updateStatusBar()244         private void updateStatusBar() {
245             int state = StatusBarManager.DISABLE_NONE;
246 
247             if (!mIsExpandedViewEnabled) {
248                 state |= StatusBarManager.DISABLE_EXPAND;
249             }
250             if (!mIsNotificationEnabled) {
251                 state |= StatusBarManager.DISABLE_NOTIFICATION_ALERTS;
252             }
253             if (!mIsSystemBarNavigationEnabled) {
254                 // Disable *all* possible navigation via the system bar.
255                 state |= StatusBarManager.DISABLE_HOME;
256                 state |= StatusBarManager.DISABLE_RECENT;
257                 state |= StatusBarManager.DISABLE_BACK;
258             }
259 
260             if (DBG) log("updateStatusBar: state = 0x" + Integer.toHexString(state));
261             mStatusBarManager.disable(state);
262         }
263     }
264 
265     /**
266      * Makes sure phone-related notifications are up to date on a
267      * freshly-booted device.
268      */
updateNotificationsAtStartup()269     private void updateNotificationsAtStartup() {
270         if (DBG) log("updateNotificationsAtStartup()...");
271 
272         // instantiate query handler
273         mQueryHandler = new QueryHandler(mContext.getContentResolver());
274 
275         // setup query spec, look for all Missed calls that are new.
276         StringBuilder where = new StringBuilder("type=");
277         where.append(Calls.MISSED_TYPE);
278         where.append(" AND new=1");
279 
280         // start the query
281         if (DBG) log("- start call log query...");
282         mQueryHandler.startQuery(CALL_LOG_TOKEN, null, Calls.CONTENT_URI,  CALL_LOG_PROJECTION,
283                 where.toString(), null, Calls.DEFAULT_SORT_ORDER);
284 
285         // Update (or cancel) the in-call notification
286         if (DBG) log("- updating in-call notification at startup...");
287         updateInCallNotification();
288 
289         // Depend on android.app.StatusBarManager to be set to
290         // disable(DISABLE_NONE) upon startup.  This will be the
291         // case even if the phone app crashes.
292     }
293 
294     /** The projection to use when querying the phones table */
295     static final String[] PHONES_PROJECTION = new String[] {
296         PhoneLookup.NUMBER,
297         PhoneLookup.DISPLAY_NAME,
298         PhoneLookup._ID
299     };
300 
301     /**
302      * Class used to run asynchronous queries to re-populate the notifications we care about.
303      * There are really 3 steps to this:
304      *  1. Find the list of missed calls
305      *  2. For each call, run a query to retrieve the caller's name.
306      *  3. For each caller, try obtaining photo.
307      */
308     private class QueryHandler extends AsyncQueryHandler
309             implements ContactsAsyncHelper.OnImageLoadCompleteListener {
310 
311         /**
312          * Used to store relevant fields for the Missed Call
313          * notifications.
314          */
315         private class NotificationInfo {
316             public String name;
317             public String number;
318             /**
319              * Type of the call. {@link android.provider.CallLog.Calls#INCOMING_TYPE}
320              * {@link android.provider.CallLog.Calls#OUTGOING_TYPE}, or
321              * {@link android.provider.CallLog.Calls#MISSED_TYPE}.
322              */
323             public String type;
324             public long date;
325         }
326 
QueryHandler(ContentResolver cr)327         public QueryHandler(ContentResolver cr) {
328             super(cr);
329         }
330 
331         /**
332          * Handles the query results.
333          */
334         @Override
onQueryComplete(int token, Object cookie, Cursor cursor)335         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
336             // TODO: it would be faster to use a join here, but for the purposes
337             // of this small record set, it should be ok.
338 
339             // Note that CursorJoiner is not useable here because the number
340             // comparisons are not strictly equals; the comparisons happen in
341             // the SQL function PHONE_NUMBERS_EQUAL, which is not available for
342             // the CursorJoiner.
343 
344             // Executing our own query is also feasible (with a join), but that
345             // will require some work (possibly destabilizing) in Contacts
346             // Provider.
347 
348             // At this point, we will execute subqueries on each row just as
349             // CallLogActivity.java does.
350             switch (token) {
351                 case CALL_LOG_TOKEN:
352                     if (DBG) log("call log query complete.");
353 
354                     // initial call to retrieve the call list.
355                     if (cursor != null) {
356                         while (cursor.moveToNext()) {
357                             // for each call in the call log list, create
358                             // the notification object and query contacts
359                             NotificationInfo n = getNotificationInfo (cursor);
360 
361                             if (DBG) log("query contacts for number: " + n.number);
362 
363                             mQueryHandler.startQuery(CONTACT_TOKEN, n,
364                                     Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, n.number),
365                                     PHONES_PROJECTION, null, null, PhoneLookup.NUMBER);
366                         }
367 
368                         if (DBG) log("closing call log cursor.");
369                         cursor.close();
370                     }
371                     break;
372                 case CONTACT_TOKEN:
373                     if (DBG) log("contact query complete.");
374 
375                     // subqueries to get the caller name.
376                     if ((cursor != null) && (cookie != null)){
377                         NotificationInfo n = (NotificationInfo) cookie;
378 
379                         Uri personUri = null;
380                         if (cursor.moveToFirst()) {
381                             n.name = cursor.getString(
382                                     cursor.getColumnIndexOrThrow(PhoneLookup.DISPLAY_NAME));
383                             long person_id = cursor.getLong(
384                                     cursor.getColumnIndexOrThrow(PhoneLookup._ID));
385                             if (DBG) {
386                                 log("contact :" + n.name + " found for phone: " + n.number
387                                         + ". id : " + person_id);
388                             }
389                             personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, person_id);
390                         }
391 
392                         if (personUri != null) {
393                             if (DBG) {
394                                 log("Start obtaining picture for the missed call. Uri: "
395                                         + personUri);
396                             }
397                             // Now try to obtain a photo for this person.
398                             // ContactsAsyncHelper will do that and call onImageLoadComplete()
399                             // after that.
400                             ContactsAsyncHelper.startObtainPhotoAsync(
401                                     0, mContext, personUri, this, n);
402                         } else {
403                             if (DBG) {
404                                 log("Failed to find Uri for obtaining photo."
405                                         + " Just send notification without it.");
406                             }
407                             // We couldn't find person Uri, so we're sure we cannot obtain a photo.
408                             // Call notifyMissedCall() right now.
409                             notifyMissedCall(n.name, n.number, n.type, null, null, n.date);
410                         }
411 
412                         if (DBG) log("closing contact cursor.");
413                         cursor.close();
414                     }
415                     break;
416                 default:
417             }
418         }
419 
420         @Override
onImageLoadComplete( int token, Drawable photo, Bitmap photoIcon, Object cookie)421         public void onImageLoadComplete(
422                 int token, Drawable photo, Bitmap photoIcon, Object cookie) {
423             if (DBG) log("Finished loading image: " + photo);
424             NotificationInfo n = (NotificationInfo) cookie;
425             notifyMissedCall(n.name, n.number, n.type, photo, photoIcon, n.date);
426         }
427 
428         /**
429          * Factory method to generate a NotificationInfo object given a
430          * cursor from the call log table.
431          */
getNotificationInfo(Cursor cursor)432         private final NotificationInfo getNotificationInfo(Cursor cursor) {
433             NotificationInfo n = new NotificationInfo();
434             n.name = null;
435             n.number = cursor.getString(cursor.getColumnIndexOrThrow(Calls.NUMBER));
436             n.type = cursor.getString(cursor.getColumnIndexOrThrow(Calls.TYPE));
437             n.date = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DATE));
438 
439             // make sure we update the number depending upon saved values in
440             // CallLog.addCall().  If either special values for unknown or
441             // private number are detected, we need to hand off the message
442             // to the missed call notification.
443             if ( (n.number.equals(CallerInfo.UNKNOWN_NUMBER)) ||
444                  (n.number.equals(CallerInfo.PRIVATE_NUMBER)) ||
445                  (n.number.equals(CallerInfo.PAYPHONE_NUMBER)) ) {
446                 n.number = null;
447             }
448 
449             if (DBG) log("NotificationInfo constructed for number: " + n.number);
450 
451             return n;
452         }
453     }
454 
455     /**
456      * Configures a Notification to emit the blinky green message-waiting/
457      * missed-call signal.
458      */
configureLedNotification(Notification note)459     private static void configureLedNotification(Notification note) {
460         note.flags |= Notification.FLAG_SHOW_LIGHTS;
461         note.defaults |= Notification.DEFAULT_LIGHTS;
462     }
463 
464     /**
465      * Displays a notification about a missed call.
466      *
467      * @param name the contact name.
468      * @param number the phone number. Note that this may be a non-callable String like "Unknown",
469      * or "Private Number", which possibly come from methods like
470      * {@link PhoneUtils#modifyForSpecialCnapCases(Context, CallerInfo, String, int)}.
471      * @param type the type of the call. {@link android.provider.CallLog.Calls#INCOMING_TYPE}
472      * {@link android.provider.CallLog.Calls#OUTGOING_TYPE}, or
473      * {@link android.provider.CallLog.Calls#MISSED_TYPE}
474      * @param photo picture which may be used for the notification (when photoIcon is null).
475      * This also can be null when the picture itself isn't available. If photoIcon is available
476      * it should be prioritized (because this may be too huge for notification).
477      * See also {@link ContactsAsyncHelper}.
478      * @param photoIcon picture which should be used for the notification. Can be null. This is
479      * the most suitable for {@link android.app.Notification.Builder#setLargeIcon(Bitmap)}, this
480      * should be used when non-null.
481      * @param date the time when the missed call happened
482      */
notifyMissedCall( String name, String number, String type, Drawable photo, Bitmap photoIcon, long date)483     /* package */ void notifyMissedCall(
484             String name, String number, String type, Drawable photo, Bitmap photoIcon, long date) {
485 
486         // When the user clicks this notification, we go to the call log.
487         final Intent callLogIntent = PhoneGlobals.createCallLogIntent();
488 
489         // Never display the missed call notification on non-voice-capable
490         // devices, even if the device does somehow manage to get an
491         // incoming call.
492         if (!PhoneGlobals.sVoiceCapable) {
493             if (DBG) log("notifyMissedCall: non-voice-capable device, not posting notification");
494             return;
495         }
496 
497         if (VDBG) {
498             log("notifyMissedCall(). name: " + name + ", number: " + number
499                 + ", label: " + type + ", photo: " + photo + ", photoIcon: " + photoIcon
500                 + ", date: " + date);
501         }
502 
503         // title resource id
504         int titleResId;
505         // the text in the notification's line 1 and 2.
506         String expandedText, callName;
507 
508         // increment number of missed calls.
509         mNumberMissedCalls++;
510 
511         // get the name for the ticker text
512         // i.e. "Missed call from <caller name or number>"
513         if (name != null && TextUtils.isGraphic(name)) {
514             callName = name;
515         } else if (!TextUtils.isEmpty(number)){
516             callName = number;
517         } else {
518             // use "unknown" if the caller is unidentifiable.
519             callName = mContext.getString(R.string.unknown);
520         }
521 
522         // display the first line of the notification:
523         // 1 missed call: call name
524         // more than 1 missed call: <number of calls> + "missed calls"
525         if (mNumberMissedCalls == 1) {
526             titleResId = R.string.notification_missedCallTitle;
527             expandedText = callName;
528         } else {
529             titleResId = R.string.notification_missedCallsTitle;
530             expandedText = mContext.getString(R.string.notification_missedCallsMsg,
531                     mNumberMissedCalls);
532         }
533 
534         Notification.Builder builder = new Notification.Builder(mContext);
535         builder.setSmallIcon(android.R.drawable.stat_notify_missed_call)
536                 .setTicker(mContext.getString(R.string.notification_missedCallTicker, callName))
537                 .setWhen(date)
538                 .setContentTitle(mContext.getText(titleResId))
539                 .setContentText(expandedText)
540                 .setContentIntent(PendingIntent.getActivity(mContext, 0, callLogIntent, 0))
541                 .setAutoCancel(true)
542                 .setDeleteIntent(createClearMissedCallsIntent());
543 
544         // Simple workaround for issue 6476275; refrain having actions when the given number seems
545         // not a real one but a non-number which was embedded by methods outside (like
546         // PhoneUtils#modifyForSpecialCnapCases()).
547         // TODO: consider removing equals() checks here, and modify callers of this method instead.
548         if (mNumberMissedCalls == 1
549                 && !TextUtils.isEmpty(number)
550                 && !TextUtils.equals(number, mContext.getString(R.string.private_num))
551                 && !TextUtils.equals(number, mContext.getString(R.string.unknown))){
552             if (DBG) log("Add actions with the number " + number);
553 
554             builder.addAction(R.drawable.stat_sys_phone_call,
555                     mContext.getString(R.string.notification_missedCall_call_back),
556                     PhoneGlobals.getCallBackPendingIntent(mContext, number));
557 
558             builder.addAction(R.drawable.ic_text_holo_dark,
559                     mContext.getString(R.string.notification_missedCall_message),
560                     PhoneGlobals.getSendSmsFromNotificationPendingIntent(mContext, number));
561 
562             if (photoIcon != null) {
563                 builder.setLargeIcon(photoIcon);
564             } else if (photo instanceof BitmapDrawable) {
565                 builder.setLargeIcon(((BitmapDrawable) photo).getBitmap());
566             }
567         } else {
568             if (DBG) {
569                 log("Suppress actions. number: " + number + ", missedCalls: " + mNumberMissedCalls);
570             }
571         }
572 
573         Notification notification = builder.getNotification();
574         configureLedNotification(notification);
575         mNotificationManager.notify(MISSED_CALL_NOTIFICATION, notification);
576     }
577 
578     /** Returns an intent to be invoked when the missed call notification is cleared. */
createClearMissedCallsIntent()579     private PendingIntent createClearMissedCallsIntent() {
580         Intent intent = new Intent(mContext, ClearMissedCallsService.class);
581         intent.setAction(ClearMissedCallsService.ACTION_CLEAR_MISSED_CALLS);
582         return PendingIntent.getService(mContext, 0, intent, 0);
583     }
584 
585     /**
586      * Cancels the "missed call" notification.
587      *
588      * @see ITelephony.cancelMissedCallsNotification()
589      */
cancelMissedCallNotification()590     void cancelMissedCallNotification() {
591         // reset the number of missed calls to 0.
592         mNumberMissedCalls = 0;
593         mNotificationManager.cancel(MISSED_CALL_NOTIFICATION);
594     }
595 
notifySpeakerphone()596     private void notifySpeakerphone() {
597         if (!mShowingSpeakerphoneIcon) {
598             mStatusBarManager.setIcon("speakerphone", android.R.drawable.stat_sys_speakerphone, 0,
599                     mContext.getString(R.string.accessibility_speakerphone_enabled));
600             mShowingSpeakerphoneIcon = true;
601         }
602     }
603 
cancelSpeakerphone()604     private void cancelSpeakerphone() {
605         if (mShowingSpeakerphoneIcon) {
606             mStatusBarManager.removeIcon("speakerphone");
607             mShowingSpeakerphoneIcon = false;
608         }
609     }
610 
611     /**
612      * Shows or hides the "speakerphone" notification in the status bar,
613      * based on the actual current state of the speaker.
614      *
615      * If you already know the current speaker state (e.g. if you just
616      * called AudioManager.setSpeakerphoneOn() yourself) then you should
617      * directly call {@link #updateSpeakerNotification(boolean)} instead.
618      *
619      * (But note that the status bar icon is *never* shown while the in-call UI
620      * is active; it only appears if you bail out to some other activity.)
621      */
updateSpeakerNotification()622     private void updateSpeakerNotification() {
623         AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
624         boolean showNotification =
625                 (mPhone.getState() == PhoneConstants.State.OFFHOOK) && audioManager.isSpeakerphoneOn();
626 
627         if (DBG) log(showNotification
628                      ? "updateSpeakerNotification: speaker ON"
629                      : "updateSpeakerNotification: speaker OFF (or not offhook)");
630 
631         updateSpeakerNotification(showNotification);
632     }
633 
634     /**
635      * Shows or hides the "speakerphone" notification in the status bar.
636      *
637      * @param showNotification if true, call notifySpeakerphone();
638      *                         if false, call cancelSpeakerphone().
639      *
640      * Use {@link updateSpeakerNotification()} to update the status bar
641      * based on the actual current state of the speaker.
642      *
643      * (But note that the status bar icon is *never* shown while the in-call UI
644      * is active; it only appears if you bail out to some other activity.)
645      */
updateSpeakerNotification(boolean showNotification)646     public void updateSpeakerNotification(boolean showNotification) {
647         if (DBG) log("updateSpeakerNotification(" + showNotification + ")...");
648 
649         // Regardless of the value of the showNotification param, suppress
650         // the status bar icon if the the InCallScreen is the foreground
651         // activity, since the in-call UI already provides an onscreen
652         // indication of the speaker state.  (This reduces clutter in the
653         // status bar.)
654         if (mApp.isShowingCallScreen()) {
655             cancelSpeakerphone();
656             return;
657         }
658 
659         if (showNotification) {
660             notifySpeakerphone();
661         } else {
662             cancelSpeakerphone();
663         }
664     }
665 
notifyMute()666     private void notifyMute() {
667         if (!mShowingMuteIcon) {
668             mStatusBarManager.setIcon("mute", android.R.drawable.stat_notify_call_mute, 0,
669                     mContext.getString(R.string.accessibility_call_muted));
670             mShowingMuteIcon = true;
671         }
672     }
673 
cancelMute()674     private void cancelMute() {
675         if (mShowingMuteIcon) {
676             mStatusBarManager.removeIcon("mute");
677             mShowingMuteIcon = false;
678         }
679     }
680 
681     /**
682      * Shows or hides the "mute" notification in the status bar,
683      * based on the current mute state of the Phone.
684      *
685      * (But note that the status bar icon is *never* shown while the in-call UI
686      * is active; it only appears if you bail out to some other activity.)
687      */
updateMuteNotification()688     void updateMuteNotification() {
689         // Suppress the status bar icon if the the InCallScreen is the
690         // foreground activity, since the in-call UI already provides an
691         // onscreen indication of the mute state.  (This reduces clutter
692         // in the status bar.)
693         if (mApp.isShowingCallScreen()) {
694             cancelMute();
695             return;
696         }
697 
698         if ((mCM.getState() == PhoneConstants.State.OFFHOOK) && PhoneUtils.getMute()) {
699             if (DBG) log("updateMuteNotification: MUTED");
700             notifyMute();
701         } else {
702             if (DBG) log("updateMuteNotification: not muted (or not offhook)");
703             cancelMute();
704         }
705     }
706 
707     /**
708      * Updates the phone app's status bar notification based on the
709      * current telephony state, or cancels the notification if the phone
710      * is totally idle.
711      *
712      * This method will never actually launch the incoming-call UI.
713      * (Use updateNotificationAndLaunchIncomingCallUi() for that.)
714      */
updateInCallNotification()715     public void updateInCallNotification() {
716         // allowFullScreenIntent=false means *don't* allow the incoming
717         // call UI to be launched.
718         updateInCallNotification(false);
719     }
720 
721     /**
722      * Updates the phone app's status bar notification *and* launches the
723      * incoming call UI in response to a new incoming call.
724      *
725      * This is just like updateInCallNotification(), with one exception:
726      * If an incoming call is ringing (or call-waiting), the notification
727      * will also include a "fullScreenIntent" that will cause the
728      * InCallScreen to be launched immediately, unless the current
729      * foreground activity is marked as "immersive".
730      *
731      * (This is the mechanism that actually brings up the incoming call UI
732      * when we receive a "new ringing connection" event from the telephony
733      * layer.)
734      *
735      * Watch out: this method should ONLY be called directly from the code
736      * path in CallNotifier that handles the "new ringing connection"
737      * event from the telephony layer.  All other places that update the
738      * in-call notification (like for phone state changes) should call
739      * updateInCallNotification() instead.  (This ensures that we don't
740      * end up launching the InCallScreen multiple times for a single
741      * incoming call, which could cause slow responsiveness and/or visible
742      * glitches.)
743      *
744      * Also note that this method is safe to call even if the phone isn't
745      * actually ringing (or, more likely, if an incoming call *was*
746      * ringing briefly but then disconnected).  In that case, we'll simply
747      * update or cancel the in-call notification based on the current
748      * phone state.
749      *
750      * @see #updateInCallNotification(boolean)
751      */
updateNotificationAndLaunchIncomingCallUi()752     public void updateNotificationAndLaunchIncomingCallUi() {
753         // Set allowFullScreenIntent=true to indicate that we *should*
754         // launch the incoming call UI if necessary.
755         updateInCallNotification(true);
756     }
757 
758     /**
759      * Helper method for updateInCallNotification() and
760      * updateNotificationAndLaunchIncomingCallUi(): Update the phone app's
761      * status bar notification based on the current telephony state, or
762      * cancels the notification if the phone is totally idle.
763      *
764      * @param allowFullScreenIntent If true, *and* an incoming call is
765      *   ringing, the notification will include a "fullScreenIntent"
766      *   pointing at the InCallScreen (which will cause the InCallScreen
767      *   to be launched.)
768      *   Watch out: This should be set to true *only* when directly
769      *   handling the "new ringing connection" event from the telephony
770      *   layer (see updateNotificationAndLaunchIncomingCallUi().)
771      */
updateInCallNotification(boolean allowFullScreenIntent)772     private void updateInCallNotification(boolean allowFullScreenIntent) {
773         int resId;
774         if (DBG) log("updateInCallNotification(allowFullScreenIntent = "
775                      + allowFullScreenIntent + ")...");
776 
777         // Never display the "ongoing call" notification on
778         // non-voice-capable devices, even if the phone is actually
779         // offhook (like during a non-interactive OTASP call.)
780         if (!PhoneGlobals.sVoiceCapable) {
781             if (DBG) log("- non-voice-capable device; suppressing notification.");
782             return;
783         }
784 
785         // If the phone is idle, completely clean up all call-related
786         // notifications.
787         if (mCM.getState() == PhoneConstants.State.IDLE) {
788             cancelInCall();
789             cancelMute();
790             cancelSpeakerphone();
791             return;
792         }
793 
794         final boolean hasRingingCall = mCM.hasActiveRingingCall();
795         final boolean hasActiveCall = mCM.hasActiveFgCall();
796         final boolean hasHoldingCall = mCM.hasActiveBgCall();
797         if (DBG) {
798             log("  - hasRingingCall = " + hasRingingCall);
799             log("  - hasActiveCall = " + hasActiveCall);
800             log("  - hasHoldingCall = " + hasHoldingCall);
801         }
802 
803         // Suppress the in-call notification if the InCallScreen is the
804         // foreground activity, since it's already obvious that you're on a
805         // call.  (The status bar icon is needed only if you navigate *away*
806         // from the in-call UI.)
807         boolean suppressNotification = mApp.isShowingCallScreen();
808         // if (DBG) log("- suppressNotification: initial value: " + suppressNotification);
809 
810         // ...except for a couple of cases where we *never* suppress the
811         // notification:
812         //
813         //   - If there's an incoming ringing call: always show the
814         //     notification, since the in-call notification is what actually
815         //     launches the incoming call UI in the first place (see
816         //     notification.fullScreenIntent below.)  This makes sure that we'll
817         //     correctly handle the case where a new incoming call comes in but
818         //     the InCallScreen is already in the foreground.
819         if (hasRingingCall) suppressNotification = false;
820 
821         //   - If "voice privacy" mode is active: always show the notification,
822         //     since that's the only "voice privacy" indication we have.
823         boolean enhancedVoicePrivacy = mApp.notifier.getVoicePrivacyState();
824         // if (DBG) log("updateInCallNotification: enhancedVoicePrivacy = " + enhancedVoicePrivacy);
825         if (enhancedVoicePrivacy) suppressNotification = false;
826 
827         if (suppressNotification) {
828             if (DBG) log("- suppressNotification = true; reducing clutter in status bar...");
829             cancelInCall();
830             // Suppress the mute and speaker status bar icons too
831             // (also to reduce clutter in the status bar.)
832             cancelSpeakerphone();
833             cancelMute();
834             return;
835         }
836 
837         // Display the appropriate icon in the status bar,
838         // based on the current phone and/or bluetooth state.
839 
840         if (hasRingingCall) {
841             // There's an incoming ringing call.
842             resId = R.drawable.stat_sys_phone_call;
843         } else if (!hasActiveCall && hasHoldingCall) {
844             // There's only one call, and it's on hold.
845             if (enhancedVoicePrivacy) {
846                 resId = R.drawable.stat_sys_vp_phone_call_on_hold;
847             } else {
848                 resId = R.drawable.stat_sys_phone_call_on_hold;
849             }
850         } else {
851             if (enhancedVoicePrivacy) {
852                 resId = R.drawable.stat_sys_vp_phone_call;
853             } else {
854                 resId = R.drawable.stat_sys_phone_call;
855             }
856         }
857 
858         // Note we can't just bail out now if (resId == mInCallResId),
859         // since even if the status icon hasn't changed, some *other*
860         // notification-related info may be different from the last time
861         // we were here (like the caller-id info of the foreground call,
862         // if the user swapped calls...)
863 
864         if (DBG) log("- Updating status bar icon: resId = " + resId);
865         mInCallResId = resId;
866 
867         // Even if both lines are in use, we only show a single item in
868         // the expanded Notifications UI.  It's labeled "Ongoing call"
869         // (or "On hold" if there's only one call, and it's on hold.)
870         // Also, we don't have room to display caller-id info from two
871         // different calls.  So if both lines are in use, display info
872         // from the foreground call.  And if there's a ringing call,
873         // display that regardless of the state of the other calls.
874 
875         Call currentCall;
876         if (hasRingingCall) {
877             currentCall = mCM.getFirstActiveRingingCall();
878         } else if (hasActiveCall) {
879             currentCall = mCM.getActiveFgCall();
880         } else {
881             currentCall = mCM.getFirstActiveBgCall();
882         }
883         Connection currentConn = currentCall.getEarliestConnection();
884 
885         final Notification.Builder builder = new Notification.Builder(mContext);
886         builder.setSmallIcon(mInCallResId).setOngoing(true);
887 
888         // PendingIntent that can be used to launch the InCallScreen.  The
889         // system fires off this intent if the user pulls down the windowshade
890         // and clicks the notification's expanded view.  It's also used to
891         // launch the InCallScreen immediately when when there's an incoming
892         // call (see the "fullScreenIntent" field below).
893         PendingIntent inCallPendingIntent =
894                 PendingIntent.getActivity(mContext, 0,
895                                           PhoneGlobals.createInCallIntent(), 0);
896         builder.setContentIntent(inCallPendingIntent);
897 
898         // Update icon on the left of the notification.
899         // - If it is directly available from CallerInfo, we'll just use that.
900         // - If it is not, use the same icon as in the status bar.
901         CallerInfo callerInfo = null;
902         if (currentConn != null) {
903             Object o = currentConn.getUserData();
904             if (o instanceof CallerInfo) {
905                 callerInfo = (CallerInfo) o;
906             } else if (o instanceof PhoneUtils.CallerInfoToken) {
907                 callerInfo = ((PhoneUtils.CallerInfoToken) o).currentInfo;
908             } else {
909                 Log.w(LOG_TAG, "CallerInfo isn't available while Call object is available.");
910             }
911         }
912         boolean largeIconWasSet = false;
913         if (callerInfo != null) {
914             // In most cases, the user will see the notification after CallerInfo is already
915             // available, so photo will be available from this block.
916             if (callerInfo.isCachedPhotoCurrent) {
917                 // .. and in that case CallerInfo's cachedPhotoIcon should also be available.
918                 // If it happens not, then try using cachedPhoto, assuming Drawable coming from
919                 // ContactProvider will be BitmapDrawable.
920                 if (callerInfo.cachedPhotoIcon != null) {
921                     builder.setLargeIcon(callerInfo.cachedPhotoIcon);
922                     largeIconWasSet = true;
923                 } else if (callerInfo.cachedPhoto instanceof BitmapDrawable) {
924                     if (DBG) log("- BitmapDrawable found for large icon");
925                     Bitmap bitmap = ((BitmapDrawable) callerInfo.cachedPhoto).getBitmap();
926                     builder.setLargeIcon(bitmap);
927                     largeIconWasSet = true;
928                 } else {
929                     if (DBG) {
930                         log("- Failed to fetch icon from CallerInfo's cached photo."
931                                 + " (cachedPhotoIcon: " + callerInfo.cachedPhotoIcon
932                                 + ", cachedPhoto: " + callerInfo.cachedPhoto + ")."
933                                 + " Ignore it.");
934                     }
935                 }
936             }
937 
938             if (!largeIconWasSet && callerInfo.photoResource > 0) {
939                 if (DBG) {
940                     log("- BitmapDrawable nor person Id not found for large icon."
941                             + " Use photoResource: " + callerInfo.photoResource);
942                 }
943                 Drawable drawable =
944                         mContext.getResources().getDrawable(callerInfo.photoResource);
945                 if (drawable instanceof BitmapDrawable) {
946                     Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
947                     builder.setLargeIcon(bitmap);
948                     largeIconWasSet = true;
949                 } else {
950                     if (DBG) {
951                         log("- PhotoResource was found but it didn't return BitmapDrawable."
952                                 + " Ignore it");
953                     }
954                 }
955             }
956         } else {
957             if (DBG) log("- CallerInfo not found. Use the same icon as in the status bar.");
958         }
959 
960         // Failed to fetch Bitmap.
961         if (!largeIconWasSet && DBG) {
962             log("- No useful Bitmap was found for the photo."
963                     + " Use the same icon as in the status bar.");
964         }
965 
966         // If the connection is valid, then build what we need for the
967         // content text of notification, and start the chronometer.
968         // Otherwise, don't bother and just stick with content title.
969         if (currentConn != null) {
970             if (DBG) log("- Updating context text and chronometer.");
971             if (hasRingingCall) {
972                 // Incoming call is ringing.
973                 builder.setContentText(mContext.getString(R.string.notification_incoming_call));
974                 builder.setUsesChronometer(false);
975             } else if (hasHoldingCall && !hasActiveCall) {
976                 // Only one call, and it's on hold.
977                 builder.setContentText(mContext.getString(R.string.notification_on_hold));
978                 builder.setUsesChronometer(false);
979             } else {
980                 // We show the elapsed time of the current call using Chronometer.
981                 builder.setUsesChronometer(true);
982 
983                 // Determine the "start time" of the current connection.
984                 //   We can't use currentConn.getConnectTime(), because (1) that's
985                 // in the currentTimeMillis() time base, and (2) it's zero when
986                 // the phone first goes off hook, since the getConnectTime counter
987                 // doesn't start until the DIALING -> ACTIVE transition.
988                 //   Instead we start with the current connection's duration,
989                 // and translate that into the elapsedRealtime() timebase.
990                 long callDurationMsec = currentConn.getDurationMillis();
991                 builder.setWhen(System.currentTimeMillis() - callDurationMsec);
992 
993                 int contextTextId = R.string.notification_ongoing_call;
994 
995                 Call call = mCM.getActiveFgCall();
996                 if (TelephonyCapabilities.canDistinguishDialingAndConnected(
997                         call.getPhone().getPhoneType()) && call.isDialingOrAlerting()) {
998                   contextTextId = R.string.notification_dialing;
999                 }
1000 
1001                 builder.setContentText(mContext.getString(contextTextId));
1002             }
1003         } else if (DBG) {
1004             Log.w(LOG_TAG, "updateInCallNotification: null connection, can't set exp view line 1.");
1005         }
1006 
1007         // display conference call string if this call is a conference
1008         // call, otherwise display the connection information.
1009 
1010         // Line 2 of the expanded view (smaller text).  This is usually a
1011         // contact name or phone number.
1012         String expandedViewLine2 = "";
1013         // TODO: it may not make sense for every point to make separate
1014         // checks for isConferenceCall, so we need to think about
1015         // possibly including this in startGetCallerInfo or some other
1016         // common point.
1017         if (PhoneUtils.isConferenceCall(currentCall)) {
1018             // if this is a conference call, just use that as the caller name.
1019             expandedViewLine2 = mContext.getString(R.string.card_title_conf_call);
1020         } else {
1021             // If necessary, start asynchronous query to do the caller-id lookup.
1022             PhoneUtils.CallerInfoToken cit =
1023                 PhoneUtils.startGetCallerInfo(mContext, currentCall, this, this);
1024             expandedViewLine2 = PhoneUtils.getCompactNameFromCallerInfo(cit.currentInfo, mContext);
1025             // Note: For an incoming call, the very first time we get here we
1026             // won't have a contact name yet, since we only just started the
1027             // caller-id query.  So expandedViewLine2 will start off as a raw
1028             // phone number, but we'll update it very quickly when the query
1029             // completes (see onQueryComplete() below.)
1030         }
1031 
1032         if (DBG) log("- Updating expanded view: line 2 '" + /*expandedViewLine2*/ "xxxxxxx" + "'");
1033         builder.setContentTitle(expandedViewLine2);
1034 
1035         // TODO: We also need to *update* this notification in some cases,
1036         // like when a call ends on one line but the other is still in use
1037         // (ie. make sure the caller info here corresponds to the active
1038         // line), and maybe even when the user swaps calls (ie. if we only
1039         // show info here for the "current active call".)
1040 
1041         // Activate a couple of special Notification features if an
1042         // incoming call is ringing:
1043         if (hasRingingCall) {
1044             if (DBG) log("- Using hi-pri notification for ringing call!");
1045 
1046             // This is a high-priority event that should be shown even if the
1047             // status bar is hidden or if an immersive activity is running.
1048             builder.setPriority(Notification.PRIORITY_HIGH);
1049 
1050             // If an immersive activity is running, we have room for a single
1051             // line of text in the small notification popup window.
1052             // We use expandedViewLine2 for this (i.e. the name or number of
1053             // the incoming caller), since that's more relevant than
1054             // expandedViewLine1 (which is something generic like "Incoming
1055             // call".)
1056             builder.setTicker(expandedViewLine2);
1057 
1058             if (allowFullScreenIntent) {
1059                 // Ok, we actually want to launch the incoming call
1060                 // UI at this point (in addition to simply posting a notification
1061                 // to the status bar).  Setting fullScreenIntent will cause
1062                 // the InCallScreen to be launched immediately *unless* the
1063                 // current foreground activity is marked as "immersive".
1064                 if (DBG) log("- Setting fullScreenIntent: " + inCallPendingIntent);
1065                 builder.setFullScreenIntent(inCallPendingIntent, true);
1066 
1067                 // Ugly hack alert:
1068                 //
1069                 // The NotificationManager has the (undocumented) behavior
1070                 // that it will *ignore* the fullScreenIntent field if you
1071                 // post a new Notification that matches the ID of one that's
1072                 // already active.  Unfortunately this is exactly what happens
1073                 // when you get an incoming call-waiting call:  the
1074                 // "ongoing call" notification is already visible, so the
1075                 // InCallScreen won't get launched in this case!
1076                 // (The result: if you bail out of the in-call UI while on a
1077                 // call and then get a call-waiting call, the incoming call UI
1078                 // won't come up automatically.)
1079                 //
1080                 // The workaround is to just notice this exact case (this is a
1081                 // call-waiting call *and* the InCallScreen is not in the
1082                 // foreground) and manually cancel the in-call notification
1083                 // before (re)posting it.
1084                 //
1085                 // TODO: there should be a cleaner way of avoiding this
1086                 // problem (see discussion in bug 3184149.)
1087                 Call ringingCall = mCM.getFirstActiveRingingCall();
1088                 if ((ringingCall.getState() == Call.State.WAITING) && !mApp.isShowingCallScreen()) {
1089                     Log.i(LOG_TAG, "updateInCallNotification: call-waiting! force relaunch...");
1090                     // Cancel the IN_CALL_NOTIFICATION immediately before
1091                     // (re)posting it; this seems to force the
1092                     // NotificationManager to launch the fullScreenIntent.
1093                     mNotificationManager.cancel(IN_CALL_NOTIFICATION);
1094                 }
1095             }
1096         } else { // not ringing call
1097             // Make the notification prioritized over the other normal notifications.
1098             builder.setPriority(Notification.PRIORITY_HIGH);
1099 
1100             // TODO: use "if (DBG)" for this comment.
1101             log("Will show \"hang-up\" action in the ongoing active call Notification");
1102             // TODO: use better asset.
1103             builder.addAction(R.drawable.stat_sys_phone_call_end,
1104                     mContext.getText(R.string.notification_action_end_call),
1105                     PhoneGlobals.createHangUpOngoingCallPendingIntent(mContext));
1106         }
1107 
1108         Notification notification = builder.getNotification();
1109         if (DBG) log("Notifying IN_CALL_NOTIFICATION: " + notification);
1110         mNotificationManager.notify(IN_CALL_NOTIFICATION, notification);
1111 
1112         // Finally, refresh the mute and speakerphone notifications (since
1113         // some phone state changes can indirectly affect the mute and/or
1114         // speaker state).
1115         updateSpeakerNotification();
1116         updateMuteNotification();
1117     }
1118 
1119     /**
1120      * Implemented for CallerInfoAsyncQuery.OnQueryCompleteListener interface.
1121      * refreshes the contentView when called.
1122      */
1123     @Override
onQueryComplete(int token, Object cookie, CallerInfo ci)1124     public void onQueryComplete(int token, Object cookie, CallerInfo ci){
1125         if (DBG) log("CallerInfo query complete (for NotificationMgr), "
1126                      + "updating in-call notification..");
1127         if (DBG) log("- cookie: " + cookie);
1128         if (DBG) log("- ci: " + ci);
1129 
1130         if (cookie == this) {
1131             // Ok, this is the caller-id query we fired off in
1132             // updateInCallNotification(), presumably when an incoming call
1133             // first appeared.  If the caller-id info matched any contacts,
1134             // compactName should now be a real person name rather than a raw
1135             // phone number:
1136             if (DBG) log("- compactName is now: "
1137                          + PhoneUtils.getCompactNameFromCallerInfo(ci, mContext));
1138 
1139             // Now that our CallerInfo object has been fully filled-in,
1140             // refresh the in-call notification.
1141             if (DBG) log("- updating notification after query complete...");
1142             updateInCallNotification();
1143         } else {
1144             Log.w(LOG_TAG, "onQueryComplete: caller-id query from unknown source! "
1145                   + "cookie = " + cookie);
1146         }
1147     }
1148 
1149     /**
1150      * Take down the in-call notification.
1151      * @see updateInCallNotification()
1152      */
cancelInCall()1153     private void cancelInCall() {
1154         if (DBG) log("cancelInCall()...");
1155         mNotificationManager.cancel(IN_CALL_NOTIFICATION);
1156         mInCallResId = 0;
1157     }
1158 
1159     /**
1160      * Completely take down the in-call notification *and* the mute/speaker
1161      * notifications as well, to indicate that the phone is now idle.
1162      */
cancelCallInProgressNotifications()1163     /* package */ void cancelCallInProgressNotifications() {
1164         if (DBG) log("cancelCallInProgressNotifications()...");
1165         if (mInCallResId == 0) {
1166             return;
1167         }
1168 
1169         if (DBG) log("cancelCallInProgressNotifications: " + mInCallResId);
1170         cancelInCall();
1171         cancelMute();
1172         cancelSpeakerphone();
1173     }
1174 
1175     /**
1176      * Updates the message waiting indicator (voicemail) notification.
1177      *
1178      * @param visible true if there are messages waiting
1179      */
updateMwi(boolean visible)1180     /* package */ void updateMwi(boolean visible) {
1181         if (DBG) log("updateMwi(): " + visible);
1182 
1183         if (visible) {
1184             int resId = android.R.drawable.stat_notify_voicemail;
1185 
1186             // This Notification can get a lot fancier once we have more
1187             // information about the current voicemail messages.
1188             // (For example, the current voicemail system can't tell
1189             // us the caller-id or timestamp of a message, or tell us the
1190             // message count.)
1191 
1192             // But for now, the UI is ultra-simple: if the MWI indication
1193             // is supposed to be visible, just show a single generic
1194             // notification.
1195 
1196             String notificationTitle = mContext.getString(R.string.notification_voicemail_title);
1197             String vmNumber = mPhone.getVoiceMailNumber();
1198             if (DBG) log("- got vm number: '" + vmNumber + "'");
1199 
1200             // Watch out: vmNumber may be null, for two possible reasons:
1201             //
1202             //   (1) This phone really has no voicemail number
1203             //
1204             //   (2) This phone *does* have a voicemail number, but
1205             //       the SIM isn't ready yet.
1206             //
1207             // Case (2) *does* happen in practice if you have voicemail
1208             // messages when the device first boots: we get an MWI
1209             // notification as soon as we register on the network, but the
1210             // SIM hasn't finished loading yet.
1211             //
1212             // So handle case (2) by retrying the lookup after a short
1213             // delay.
1214 
1215             if ((vmNumber == null) && !mPhone.getIccRecordsLoaded()) {
1216                 if (DBG) log("- Null vm number: SIM records not loaded (yet)...");
1217 
1218                 // TODO: rather than retrying after an arbitrary delay, it
1219                 // would be cleaner to instead just wait for a
1220                 // SIM_RECORDS_LOADED notification.
1221                 // (Unfortunately right now there's no convenient way to
1222                 // get that notification in phone app code.  We'd first
1223                 // want to add a call like registerForSimRecordsLoaded()
1224                 // to Phone.java and GSMPhone.java, and *then* we could
1225                 // listen for that in the CallNotifier class.)
1226 
1227                 // Limit the number of retries (in case the SIM is broken
1228                 // or missing and can *never* load successfully.)
1229                 if (mVmNumberRetriesRemaining-- > 0) {
1230                     if (DBG) log("  - Retrying in " + VM_NUMBER_RETRY_DELAY_MILLIS + " msec...");
1231                     mApp.notifier.sendMwiChangedDelayed(VM_NUMBER_RETRY_DELAY_MILLIS);
1232                     return;
1233                 } else {
1234                     Log.w(LOG_TAG, "NotificationMgr.updateMwi: getVoiceMailNumber() failed after "
1235                           + MAX_VM_NUMBER_RETRIES + " retries; giving up.");
1236                     // ...and continue with vmNumber==null, just as if the
1237                     // phone had no VM number set up in the first place.
1238                 }
1239             }
1240 
1241             if (TelephonyCapabilities.supportsVoiceMessageCount(mPhone)) {
1242                 int vmCount = mPhone.getVoiceMessageCount();
1243                 String titleFormat = mContext.getString(R.string.notification_voicemail_title_count);
1244                 notificationTitle = String.format(titleFormat, vmCount);
1245             }
1246 
1247             String notificationText;
1248             if (TextUtils.isEmpty(vmNumber)) {
1249                 notificationText = mContext.getString(
1250                         R.string.notification_voicemail_no_vm_number);
1251             } else {
1252                 notificationText = String.format(
1253                         mContext.getString(R.string.notification_voicemail_text_format),
1254                         PhoneNumberUtils.formatNumber(vmNumber));
1255             }
1256 
1257             Intent intent = new Intent(Intent.ACTION_CALL,
1258                     Uri.fromParts(Constants.SCHEME_VOICEMAIL, "", null));
1259             PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
1260 
1261             SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
1262             Uri ringtoneUri;
1263             String uriString = prefs.getString(
1264                     CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_RINGTONE_KEY, null);
1265             if (!TextUtils.isEmpty(uriString)) {
1266                 ringtoneUri = Uri.parse(uriString);
1267             } else {
1268                 ringtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI;
1269             }
1270 
1271             Notification.Builder builder = new Notification.Builder(mContext);
1272             builder.setSmallIcon(resId)
1273                     .setWhen(System.currentTimeMillis())
1274                     .setContentTitle(notificationTitle)
1275                     .setContentText(notificationText)
1276                     .setContentIntent(pendingIntent)
1277                     .setSound(ringtoneUri);
1278             Notification notification = builder.getNotification();
1279 
1280             CallFeaturesSetting.migrateVoicemailVibrationSettingsIfNeeded(prefs);
1281             final boolean vibrate = prefs.getBoolean(
1282                     CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_VIBRATE_KEY, false);
1283             if (vibrate) {
1284                 notification.defaults |= Notification.DEFAULT_VIBRATE;
1285             }
1286             notification.flags |= Notification.FLAG_NO_CLEAR;
1287             configureLedNotification(notification);
1288             mNotificationManager.notify(VOICEMAIL_NOTIFICATION, notification);
1289         } else {
1290             mNotificationManager.cancel(VOICEMAIL_NOTIFICATION);
1291         }
1292     }
1293 
1294     /**
1295      * Updates the message call forwarding indicator notification.
1296      *
1297      * @param visible true if there are messages waiting
1298      */
updateCfi(boolean visible)1299     /* package */ void updateCfi(boolean visible) {
1300         if (DBG) log("updateCfi(): " + visible);
1301         if (visible) {
1302             // If Unconditional Call Forwarding (forward all calls) for VOICE
1303             // is enabled, just show a notification.  We'll default to expanded
1304             // view for now, so the there is less confusion about the icon.  If
1305             // it is deemed too weird to have CF indications as expanded views,
1306             // then we'll flip the flag back.
1307 
1308             // TODO: We may want to take a look to see if the notification can
1309             // display the target to forward calls to.  This will require some
1310             // effort though, since there are multiple layers of messages that
1311             // will need to propagate that information.
1312 
1313             Notification notification;
1314             final boolean showExpandedNotification = true;
1315             if (showExpandedNotification) {
1316                 Intent intent = new Intent(Intent.ACTION_MAIN);
1317                 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
1318                 intent.setClassName("com.android.phone",
1319                         "com.android.phone.CallFeaturesSetting");
1320 
1321                 notification = new Notification(
1322                         R.drawable.stat_sys_phone_call_forward,  // icon
1323                         null, // tickerText
1324                         0); // The "timestamp" of this notification is meaningless;
1325                             // we only care about whether CFI is currently on or not.
1326                 notification.setLatestEventInfo(
1327                         mContext, // context
1328                         mContext.getString(R.string.labelCF), // expandedTitle
1329                         mContext.getString(R.string.sum_cfu_enabled_indicator), // expandedText
1330                         PendingIntent.getActivity(mContext, 0, intent, 0)); // contentIntent
1331             } else {
1332                 notification = new Notification(
1333                         R.drawable.stat_sys_phone_call_forward,  // icon
1334                         null,  // tickerText
1335                         System.currentTimeMillis()  // when
1336                         );
1337             }
1338 
1339             notification.flags |= Notification.FLAG_ONGOING_EVENT;  // also implies FLAG_NO_CLEAR
1340 
1341             mNotificationManager.notify(
1342                     CALL_FORWARD_NOTIFICATION,
1343                     notification);
1344         } else {
1345             mNotificationManager.cancel(CALL_FORWARD_NOTIFICATION);
1346         }
1347     }
1348 
1349     /**
1350      * Shows the "data disconnected due to roaming" notification, which
1351      * appears when you lose data connectivity because you're roaming and
1352      * you have the "data roaming" feature turned off.
1353      */
showDataDisconnectedRoaming()1354     /* package */ void showDataDisconnectedRoaming() {
1355         if (DBG) log("showDataDisconnectedRoaming()...");
1356 
1357         // "Mobile network settings" screen / dialog
1358         Intent intent = new Intent(mContext,
1359                 com.android.phone.MobileNetworkSettings.class);
1360 
1361         Notification notification = new Notification(
1362                 android.R.drawable.stat_sys_warning, // icon
1363                 null, // tickerText
1364                 System.currentTimeMillis());
1365         notification.setLatestEventInfo(
1366                 mContext, // Context
1367                 mContext.getString(R.string.roaming), // expandedTitle
1368                 mContext.getString(R.string.roaming_reenable_message), // expandedText
1369                 PendingIntent.getActivity(mContext, 0, intent, 0)); // contentIntent
1370 
1371         mNotificationManager.notify(
1372                 DATA_DISCONNECTED_ROAMING_NOTIFICATION,
1373                 notification);
1374     }
1375 
1376     /**
1377      * Turns off the "data disconnected due to roaming" notification.
1378      */
hideDataDisconnectedRoaming()1379     /* package */ void hideDataDisconnectedRoaming() {
1380         if (DBG) log("hideDataDisconnectedRoaming()...");
1381         mNotificationManager.cancel(DATA_DISCONNECTED_ROAMING_NOTIFICATION);
1382     }
1383 
1384     /**
1385      * Display the network selection "no service" notification
1386      * @param operator is the numeric operator number
1387      */
showNetworkSelection(String operator)1388     private void showNetworkSelection(String operator) {
1389         if (DBG) log("showNetworkSelection(" + operator + ")...");
1390 
1391         String titleText = mContext.getString(
1392                 R.string.notification_network_selection_title);
1393         String expandedText = mContext.getString(
1394                 R.string.notification_network_selection_text, operator);
1395 
1396         Notification notification = new Notification();
1397         notification.icon = android.R.drawable.stat_sys_warning;
1398         notification.when = 0;
1399         notification.flags = Notification.FLAG_ONGOING_EVENT;
1400         notification.tickerText = null;
1401 
1402         // create the target network operators settings intent
1403         Intent intent = new Intent(Intent.ACTION_MAIN);
1404         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
1405                 Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
1406         // Use NetworkSetting to handle the selection intent
1407         intent.setComponent(new ComponentName("com.android.phone",
1408                 "com.android.phone.NetworkSetting"));
1409         PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0);
1410 
1411         notification.setLatestEventInfo(mContext, titleText, expandedText, pi);
1412 
1413         mNotificationManager.notify(SELECTED_OPERATOR_FAIL_NOTIFICATION, notification);
1414     }
1415 
1416     /**
1417      * Turn off the network selection "no service" notification
1418      */
cancelNetworkSelection()1419     private void cancelNetworkSelection() {
1420         if (DBG) log("cancelNetworkSelection()...");
1421         mNotificationManager.cancel(SELECTED_OPERATOR_FAIL_NOTIFICATION);
1422     }
1423 
1424     /**
1425      * Update notification about no service of user selected operator
1426      *
1427      * @param serviceState Phone service state
1428      */
updateNetworkSelection(int serviceState)1429     void updateNetworkSelection(int serviceState) {
1430         if (TelephonyCapabilities.supportsNetworkSelection(mPhone)) {
1431             // get the shared preference of network_selection.
1432             // empty is auto mode, otherwise it is the operator alpha name
1433             // in case there is no operator name, check the operator numeric
1434             SharedPreferences sp =
1435                     PreferenceManager.getDefaultSharedPreferences(mContext);
1436             String networkSelection =
1437                     sp.getString(PhoneBase.NETWORK_SELECTION_NAME_KEY, "");
1438             if (TextUtils.isEmpty(networkSelection)) {
1439                 networkSelection =
1440                         sp.getString(PhoneBase.NETWORK_SELECTION_KEY, "");
1441             }
1442 
1443             if (DBG) log("updateNetworkSelection()..." + "state = " +
1444                     serviceState + " new network " + networkSelection);
1445 
1446             if (serviceState == ServiceState.STATE_OUT_OF_SERVICE
1447                     && !TextUtils.isEmpty(networkSelection)) {
1448                 if (!mSelectedUnavailableNotify) {
1449                     showNetworkSelection(networkSelection);
1450                     mSelectedUnavailableNotify = true;
1451                 }
1452             } else {
1453                 if (mSelectedUnavailableNotify) {
1454                     cancelNetworkSelection();
1455                     mSelectedUnavailableNotify = false;
1456                 }
1457             }
1458         }
1459     }
1460 
postTransientNotification(int notifyId, CharSequence msg)1461     /* package */ void postTransientNotification(int notifyId, CharSequence msg) {
1462         if (mToast != null) {
1463             mToast.cancel();
1464         }
1465 
1466         mToast = Toast.makeText(mContext, msg, Toast.LENGTH_LONG);
1467         mToast.show();
1468     }
1469 
log(String msg)1470     private void log(String msg) {
1471         Log.d(LOG_TAG, msg);
1472     }
1473 }
1474