• 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.BidiFormatter;
46 import android.text.TextDirectionHeuristics;
47 import android.text.TextUtils;
48 import android.util.Log;
49 import android.widget.Toast;
50 
51 import com.android.internal.telephony.Call;
52 import com.android.internal.telephony.CallManager;
53 import com.android.internal.telephony.CallerInfo;
54 import com.android.internal.telephony.CallerInfoAsyncQuery;
55 import com.android.internal.telephony.Connection;
56 import com.android.internal.telephony.Phone;
57 import com.android.internal.telephony.PhoneBase;
58 import com.android.internal.telephony.PhoneConstants;
59 import com.android.internal.telephony.TelephonyCapabilities;
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 {
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.NUMBER_PRESENTATION,
81         Calls.DATE,
82         Calls.DURATION,
83         Calls.TYPE,
84     };
85 
86     // notification types
87     static final int MISSED_CALL_NOTIFICATION = 1;
88     static final int IN_CALL_NOTIFICATION = 2;
89     static final int MMI_NOTIFICATION = 3;
90     static final int NETWORK_SELECTION_NOTIFICATION = 4;
91     static final int VOICEMAIL_NOTIFICATION = 5;
92     static final int CALL_FORWARD_NOTIFICATION = 6;
93     static final int DATA_DISCONNECTED_ROAMING_NOTIFICATION = 7;
94     static final int SELECTED_OPERATOR_FAIL_NOTIFICATION = 8;
95 
96     /** The singleton NotificationMgr instance. */
97     private static NotificationMgr sInstance;
98 
99     private PhoneGlobals mApp;
100     private Phone mPhone;
101     private CallManager mCM;
102 
103     private Context mContext;
104     private NotificationManager mNotificationManager;
105     private StatusBarManager mStatusBarManager;
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     // used to track the notification of selected network unavailable
116     private boolean mSelectedUnavailableNotify = false;
117 
118     // Retry params for the getVoiceMailNumber() call; see updateMwi().
119     private static final int MAX_VM_NUMBER_RETRIES = 5;
120     private static final int VM_NUMBER_RETRY_DELAY_MILLIS = 10000;
121     private int mVmNumberRetriesRemaining = MAX_VM_NUMBER_RETRIES;
122 
123     // Query used to look up caller-id info for the "call log" notification.
124     private QueryHandler mQueryHandler = null;
125     private static final int CALL_LOG_TOKEN = -1;
126     private static final int CONTACT_TOKEN = -2;
127 
128     /**
129      * Private constructor (this is a singleton).
130      * @see init()
131      */
NotificationMgr(PhoneGlobals app)132     private NotificationMgr(PhoneGlobals app) {
133         mApp = app;
134         mContext = app;
135         mNotificationManager =
136                 (NotificationManager) app.getSystemService(Context.NOTIFICATION_SERVICE);
137         mStatusBarManager =
138                 (StatusBarManager) app.getSystemService(Context.STATUS_BAR_SERVICE);
139         mPhone = app.phone;  // TODO: better style to use mCM.getDefaultPhone() everywhere instead
140         mCM = app.mCM;
141         statusBarHelper = new StatusBarHelper();
142     }
143 
144     /**
145      * Initialize the singleton NotificationMgr instance.
146      *
147      * This is only done once, at startup, from PhoneApp.onCreate().
148      * From then on, the NotificationMgr instance is available via the
149      * PhoneApp's public "notificationMgr" field, which is why there's no
150      * getInstance() method here.
151      */
init(PhoneGlobals app)152     /* package */ static NotificationMgr init(PhoneGlobals app) {
153         synchronized (NotificationMgr.class) {
154             if (sInstance == null) {
155                 sInstance = new NotificationMgr(app);
156                 // Update the notifications that need to be touched at startup.
157                 sInstance.updateNotificationsAtStartup();
158             } else {
159                 Log.wtf(LOG_TAG, "init() called multiple times!  sInstance = " + sInstance);
160             }
161             return sInstance;
162         }
163     }
164 
165     /**
166      * Helper class that's a wrapper around the framework's
167      * StatusBarManager.disable() API.
168      *
169      * This class is used to control features like:
170      *
171      *   - Disabling the status bar "notification windowshade"
172      *     while the in-call UI is up
173      *
174      *   - Disabling notification alerts (audible or vibrating)
175      *     while a phone call is active
176      *
177      *   - Disabling navigation via the system bar (the "soft buttons" at
178      *     the bottom of the screen on devices with no hard buttons)
179      *
180      * We control these features through a single point of control to make
181      * sure that the various StatusBarManager.disable() calls don't
182      * interfere with each other.
183      */
184     public class StatusBarHelper {
185         // Current desired state of status bar / system bar behavior
186         private boolean mIsNotificationEnabled = true;
187         private boolean mIsExpandedViewEnabled = true;
188         private boolean mIsSystemBarNavigationEnabled = true;
189 
StatusBarHelper()190         private StatusBarHelper () {
191         }
192 
193         /**
194          * Enables or disables auditory / vibrational alerts.
195          *
196          * (We disable these any time a voice call is active, regardless
197          * of whether or not the in-call UI is visible.)
198          */
enableNotificationAlerts(boolean enable)199         public void enableNotificationAlerts(boolean enable) {
200             if (mIsNotificationEnabled != enable) {
201                 mIsNotificationEnabled = enable;
202                 updateStatusBar();
203             }
204         }
205 
206         /**
207          * Enables or disables the expanded view of the status bar
208          * (i.e. the ability to pull down the "notification windowshade").
209          *
210          * (This feature is disabled by the InCallScreen while the in-call
211          * UI is active.)
212          */
enableExpandedView(boolean enable)213         public void enableExpandedView(boolean enable) {
214             if (mIsExpandedViewEnabled != enable) {
215                 mIsExpandedViewEnabled = enable;
216                 updateStatusBar();
217             }
218         }
219 
220         /**
221          * Enables or disables the navigation via the system bar (the
222          * "soft buttons" at the bottom of the screen)
223          *
224          * (This feature is disabled while an incoming call is ringing,
225          * because it's easy to accidentally touch the system bar while
226          * pulling the phone out of your pocket.)
227          */
enableSystemBarNavigation(boolean enable)228         public void enableSystemBarNavigation(boolean enable) {
229             if (mIsSystemBarNavigationEnabled != enable) {
230                 mIsSystemBarNavigationEnabled = enable;
231                 updateStatusBar();
232             }
233         }
234 
235         /**
236          * Updates the status bar to reflect the current desired state.
237          */
updateStatusBar()238         private void updateStatusBar() {
239             int state = StatusBarManager.DISABLE_NONE;
240 
241             if (!mIsExpandedViewEnabled) {
242                 state |= StatusBarManager.DISABLE_EXPAND;
243             }
244             if (!mIsNotificationEnabled) {
245                 state |= StatusBarManager.DISABLE_NOTIFICATION_ALERTS;
246             }
247             if (!mIsSystemBarNavigationEnabled) {
248                 // Disable *all* possible navigation via the system bar.
249                 state |= StatusBarManager.DISABLE_HOME;
250                 state |= StatusBarManager.DISABLE_RECENT;
251                 state |= StatusBarManager.DISABLE_BACK;
252                 state |= StatusBarManager.DISABLE_SEARCH;
253             }
254 
255             if (DBG) log("updateStatusBar: state = 0x" + Integer.toHexString(state));
256             mStatusBarManager.disable(state);
257         }
258     }
259 
260     /**
261      * Makes sure phone-related notifications are up to date on a
262      * freshly-booted device.
263      */
updateNotificationsAtStartup()264     private void updateNotificationsAtStartup() {
265         if (DBG) log("updateNotificationsAtStartup()...");
266 
267         // instantiate query handler
268         mQueryHandler = new QueryHandler(mContext.getContentResolver());
269 
270         // setup query spec, look for all Missed calls that are new.
271         StringBuilder where = new StringBuilder("type=");
272         where.append(Calls.MISSED_TYPE);
273         where.append(" AND new=1");
274 
275         // start the query
276         if (DBG) log("- start call log query...");
277         mQueryHandler.startQuery(CALL_LOG_TOKEN, null, Calls.CONTENT_URI,  CALL_LOG_PROJECTION,
278                 where.toString(), null, Calls.DEFAULT_SORT_ORDER);
279 
280         // Depend on android.app.StatusBarManager to be set to
281         // disable(DISABLE_NONE) upon startup.  This will be the
282         // case even if the phone app crashes.
283     }
284 
285     /** The projection to use when querying the phones table */
286     static final String[] PHONES_PROJECTION = new String[] {
287         PhoneLookup.NUMBER,
288         PhoneLookup.DISPLAY_NAME,
289         PhoneLookup._ID
290     };
291 
292     /**
293      * Class used to run asynchronous queries to re-populate the notifications we care about.
294      * There are really 3 steps to this:
295      *  1. Find the list of missed calls
296      *  2. For each call, run a query to retrieve the caller's name.
297      *  3. For each caller, try obtaining photo.
298      */
299     private class QueryHandler extends AsyncQueryHandler
300             implements ContactsAsyncHelper.OnImageLoadCompleteListener {
301 
302         /**
303          * Used to store relevant fields for the Missed Call
304          * notifications.
305          */
306         private class NotificationInfo {
307             public String name;
308             public String number;
309             public int presentation;
310             /**
311              * Type of the call. {@link android.provider.CallLog.Calls#INCOMING_TYPE}
312              * {@link android.provider.CallLog.Calls#OUTGOING_TYPE}, or
313              * {@link android.provider.CallLog.Calls#MISSED_TYPE}.
314              */
315             public String type;
316             public long date;
317         }
318 
QueryHandler(ContentResolver cr)319         public QueryHandler(ContentResolver cr) {
320             super(cr);
321         }
322 
323         /**
324          * Handles the query results.
325          */
326         @Override
onQueryComplete(int token, Object cookie, Cursor cursor)327         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
328             // TODO: it would be faster to use a join here, but for the purposes
329             // of this small record set, it should be ok.
330 
331             // Note that CursorJoiner is not useable here because the number
332             // comparisons are not strictly equals; the comparisons happen in
333             // the SQL function PHONE_NUMBERS_EQUAL, which is not available for
334             // the CursorJoiner.
335 
336             // Executing our own query is also feasible (with a join), but that
337             // will require some work (possibly destabilizing) in Contacts
338             // Provider.
339 
340             // At this point, we will execute subqueries on each row just as
341             // CallLogActivity.java does.
342             switch (token) {
343                 case CALL_LOG_TOKEN:
344                     if (DBG) log("call log query complete.");
345 
346                     // initial call to retrieve the call list.
347                     if (cursor != null) {
348                         while (cursor.moveToNext()) {
349                             // for each call in the call log list, create
350                             // the notification object and query contacts
351                             NotificationInfo n = getNotificationInfo (cursor);
352 
353                             if (DBG) log("query contacts for number: " + n.number);
354 
355                             mQueryHandler.startQuery(CONTACT_TOKEN, n,
356                                     Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, n.number),
357                                     PHONES_PROJECTION, null, null, PhoneLookup.NUMBER);
358                         }
359 
360                         if (DBG) log("closing call log cursor.");
361                         cursor.close();
362                     }
363                     break;
364                 case CONTACT_TOKEN:
365                     if (DBG) log("contact query complete.");
366 
367                     // subqueries to get the caller name.
368                     if ((cursor != null) && (cookie != null)){
369                         NotificationInfo n = (NotificationInfo) cookie;
370 
371                         Uri personUri = null;
372                         if (cursor.moveToFirst()) {
373                             n.name = cursor.getString(
374                                     cursor.getColumnIndexOrThrow(PhoneLookup.DISPLAY_NAME));
375                             long person_id = cursor.getLong(
376                                     cursor.getColumnIndexOrThrow(PhoneLookup._ID));
377                             if (DBG) {
378                                 log("contact :" + n.name + " found for phone: " + n.number
379                                         + ". id : " + person_id);
380                             }
381                             personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, person_id);
382                         }
383 
384                         if (personUri != null) {
385                             if (DBG) {
386                                 log("Start obtaining picture for the missed call. Uri: "
387                                         + personUri);
388                             }
389                             // Now try to obtain a photo for this person.
390                             // ContactsAsyncHelper will do that and call onImageLoadComplete()
391                             // after that.
392                             ContactsAsyncHelper.startObtainPhotoAsync(
393                                     0, mContext, personUri, this, n);
394                         } else {
395                             if (DBG) {
396                                 log("Failed to find Uri for obtaining photo."
397                                         + " Just send notification without it.");
398                             }
399                             // We couldn't find person Uri, so we're sure we cannot obtain a photo.
400                             // Call notifyMissedCall() right now.
401                             notifyMissedCall(n.name, n.number, n.presentation, n.type, null, null,
402                                     n.date);
403                         }
404 
405                         if (DBG) log("closing contact cursor.");
406                         cursor.close();
407                     }
408                     break;
409                 default:
410             }
411         }
412 
413         @Override
onImageLoadComplete( int token, Drawable photo, Bitmap photoIcon, Object cookie)414         public void onImageLoadComplete(
415                 int token, Drawable photo, Bitmap photoIcon, Object cookie) {
416             if (DBG) log("Finished loading image: " + photo);
417             NotificationInfo n = (NotificationInfo) cookie;
418             notifyMissedCall(n.name, n.number, n.presentation, n.type, photo, photoIcon, n.date);
419         }
420 
421         /**
422          * Factory method to generate a NotificationInfo object given a
423          * cursor from the call log table.
424          */
getNotificationInfo(Cursor cursor)425         private final NotificationInfo getNotificationInfo(Cursor cursor) {
426             NotificationInfo n = new NotificationInfo();
427             n.name = null;
428             n.number = cursor.getString(cursor.getColumnIndexOrThrow(Calls.NUMBER));
429             n.presentation = cursor.getInt(cursor.getColumnIndexOrThrow(Calls.NUMBER_PRESENTATION));
430             n.type = cursor.getString(cursor.getColumnIndexOrThrow(Calls.TYPE));
431             n.date = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DATE));
432 
433             // make sure we update the number depending upon saved values in
434             // CallLog.addCall().  If either special values for unknown or
435             // private number are detected, we need to hand off the message
436             // to the missed call notification.
437             if (n.presentation != Calls.PRESENTATION_ALLOWED) {
438                 n.number = null;
439             }
440 
441             if (DBG) log("NotificationInfo constructed for number: " + n.number);
442 
443             return n;
444         }
445     }
446 
447     /**
448      * Configures a Notification to emit the blinky green message-waiting/
449      * missed-call signal.
450      */
configureLedNotification(Notification note)451     private static void configureLedNotification(Notification note) {
452         note.flags |= Notification.FLAG_SHOW_LIGHTS;
453         note.defaults |= Notification.DEFAULT_LIGHTS;
454     }
455 
456     /**
457      * Displays a notification about a missed call.
458      *
459      * @param name the contact name.
460      * @param number the phone number. Note that this may be a non-callable String like "Unknown",
461      * or "Private Number", which possibly come from methods like
462      * {@link PhoneUtils#modifyForSpecialCnapCases(Context, CallerInfo, String, int)}.
463      * @param type the type of the call. {@link android.provider.CallLog.Calls#INCOMING_TYPE}
464      * {@link android.provider.CallLog.Calls#OUTGOING_TYPE}, or
465      * {@link android.provider.CallLog.Calls#MISSED_TYPE}
466      * @param photo picture which may be used for the notification (when photoIcon is null).
467      * This also can be null when the picture itself isn't available. If photoIcon is available
468      * it should be prioritized (because this may be too huge for notification).
469      * See also {@link ContactsAsyncHelper}.
470      * @param photoIcon picture which should be used for the notification. Can be null. This is
471      * the most suitable for {@link android.app.Notification.Builder#setLargeIcon(Bitmap)}, this
472      * should be used when non-null.
473      * @param date the time when the missed call happened
474      */
notifyMissedCall(String name, String number, int presentation, String type, Drawable photo, Bitmap photoIcon, long date)475     /* package */ void notifyMissedCall(String name, String number, int presentation, String type,
476             Drawable photo, Bitmap photoIcon, long date) {
477 
478         // When the user clicks this notification, we go to the call log.
479         final PendingIntent pendingCallLogIntent = PhoneGlobals.createPendingCallLogIntent(
480                 mContext);
481 
482         // Never display the missed call notification on non-voice-capable
483         // devices, even if the device does somehow manage to get an
484         // incoming call.
485         if (!PhoneGlobals.sVoiceCapable) {
486             if (DBG) log("notifyMissedCall: non-voice-capable device, not posting notification");
487             return;
488         }
489 
490         if (VDBG) {
491             log("notifyMissedCall(). name: " + name + ", number: " + number
492                 + ", label: " + type + ", photo: " + photo + ", photoIcon: " + photoIcon
493                 + ", date: " + date);
494         }
495 
496         // title resource id
497         int titleResId;
498         // the text in the notification's line 1 and 2.
499         String expandedText, callName;
500 
501         // increment number of missed calls.
502         mNumberMissedCalls++;
503 
504         // get the name for the ticker text
505         // i.e. "Missed call from <caller name or number>"
506         if (name != null && TextUtils.isGraphic(name)) {
507             callName = name;
508         } else if (!TextUtils.isEmpty(number)){
509             final BidiFormatter bidiFormatter = BidiFormatter.getInstance();
510             // A number should always be displayed LTR using {@link BidiFormatter}
511             // regardless of the content of the rest of the notification.
512             callName = bidiFormatter.unicodeWrap(number, TextDirectionHeuristics.LTR);
513         } else {
514             // use "unknown" if the caller is unidentifiable.
515             callName = mContext.getString(R.string.unknown);
516         }
517 
518         // display the first line of the notification:
519         // 1 missed call: call name
520         // more than 1 missed call: <number of calls> + "missed calls"
521         if (mNumberMissedCalls == 1) {
522             titleResId = R.string.notification_missedCallTitle;
523             expandedText = callName;
524         } else {
525             titleResId = R.string.notification_missedCallsTitle;
526             expandedText = mContext.getString(R.string.notification_missedCallsMsg,
527                     mNumberMissedCalls);
528         }
529 
530         Notification.Builder builder = new Notification.Builder(mContext);
531         builder.setSmallIcon(android.R.drawable.stat_notify_missed_call)
532                 .setTicker(mContext.getString(R.string.notification_missedCallTicker, callName))
533                 .setWhen(date)
534                 .setContentTitle(mContext.getText(titleResId))
535                 .setContentText(expandedText)
536                 .setContentIntent(pendingCallLogIntent)
537                 .setAutoCancel(true)
538                 .setDeleteIntent(createClearMissedCallsIntent());
539 
540         // Simple workaround for issue 6476275; refrain having actions when the given number seems
541         // not a real one but a non-number which was embedded by methods outside (like
542         // PhoneUtils#modifyForSpecialCnapCases()).
543         // TODO: consider removing equals() checks here, and modify callers of this method instead.
544         if (mNumberMissedCalls == 1
545                 && !TextUtils.isEmpty(number)
546                 && (presentation == PhoneConstants.PRESENTATION_ALLOWED ||
547                         presentation == PhoneConstants.PRESENTATION_PAYPHONE)) {
548             if (DBG) log("Add actions with the number " + number);
549 
550             builder.addAction(R.drawable.stat_sys_phone_call,
551                     mContext.getString(R.string.notification_missedCall_call_back),
552                     PhoneGlobals.getCallBackPendingIntent(mContext, number));
553 
554             builder.addAction(R.drawable.ic_text_holo_dark,
555                     mContext.getString(R.string.notification_missedCall_message),
556                     PhoneGlobals.getSendSmsFromNotificationPendingIntent(mContext, number));
557 
558             if (photoIcon != null) {
559                 builder.setLargeIcon(photoIcon);
560             } else if (photo instanceof BitmapDrawable) {
561                 builder.setLargeIcon(((BitmapDrawable) photo).getBitmap());
562             }
563         } else {
564             if (DBG) {
565                 log("Suppress actions. number: " + number + ", missedCalls: " + mNumberMissedCalls);
566             }
567         }
568 
569         Notification notification = builder.getNotification();
570         configureLedNotification(notification);
571         mNotificationManager.notify(MISSED_CALL_NOTIFICATION, notification);
572     }
573 
574     /** Returns an intent to be invoked when the missed call notification is cleared. */
createClearMissedCallsIntent()575     private PendingIntent createClearMissedCallsIntent() {
576         Intent intent = new Intent(mContext, ClearMissedCallsService.class);
577         intent.setAction(ClearMissedCallsService.ACTION_CLEAR_MISSED_CALLS);
578         return PendingIntent.getService(mContext, 0, intent, 0);
579     }
580 
581     /**
582      * Cancels the "missed call" notification.
583      *
584      * @see ITelephony.cancelMissedCallsNotification()
585      */
cancelMissedCallNotification()586     void cancelMissedCallNotification() {
587         // reset the number of missed calls to 0.
588         mNumberMissedCalls = 0;
589         mNotificationManager.cancel(MISSED_CALL_NOTIFICATION);
590     }
591 
notifySpeakerphone()592     private void notifySpeakerphone() {
593         if (!mShowingSpeakerphoneIcon) {
594             mStatusBarManager.setIcon("speakerphone", android.R.drawable.stat_sys_speakerphone, 0,
595                     mContext.getString(R.string.accessibility_speakerphone_enabled));
596             mShowingSpeakerphoneIcon = true;
597         }
598     }
599 
cancelSpeakerphone()600     private void cancelSpeakerphone() {
601         if (mShowingSpeakerphoneIcon) {
602             mStatusBarManager.removeIcon("speakerphone");
603             mShowingSpeakerphoneIcon = false;
604         }
605     }
606 
607     /**
608      * Shows or hides the "speakerphone" notification in the status bar,
609      * based on the actual current state of the speaker.
610      *
611      * If you already know the current speaker state (e.g. if you just
612      * called AudioManager.setSpeakerphoneOn() yourself) then you should
613      * directly call {@link #updateSpeakerNotification(boolean)} instead.
614      *
615      * (But note that the status bar icon is *never* shown while the in-call UI
616      * is active; it only appears if you bail out to some other activity.)
617      */
updateSpeakerNotification()618     private void updateSpeakerNotification() {
619         AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
620         boolean showNotification =
621                 (mPhone.getState() == PhoneConstants.State.OFFHOOK) && audioManager.isSpeakerphoneOn();
622 
623         if (DBG) log(showNotification
624                      ? "updateSpeakerNotification: speaker ON"
625                      : "updateSpeakerNotification: speaker OFF (or not offhook)");
626 
627         updateSpeakerNotification(showNotification);
628     }
629 
630     /**
631      * Shows or hides the "speakerphone" notification in the status bar.
632      *
633      * @param showNotification if true, call notifySpeakerphone();
634      *                         if false, call cancelSpeakerphone().
635      *
636      * Use {@link updateSpeakerNotification()} to update the status bar
637      * based on the actual current state of the speaker.
638      *
639      * (But note that the status bar icon is *never* shown while the in-call UI
640      * is active; it only appears if you bail out to some other activity.)
641      */
updateSpeakerNotification(boolean showNotification)642     public void updateSpeakerNotification(boolean showNotification) {
643         if (DBG) log("updateSpeakerNotification(" + showNotification + ")...");
644 
645         // Regardless of the value of the showNotification param, suppress
646         // the status bar icon if the the InCallScreen is the foreground
647         // activity, since the in-call UI already provides an onscreen
648         // indication of the speaker state.  (This reduces clutter in the
649         // status bar.)
650 
651         if (showNotification) {
652             notifySpeakerphone();
653         } else {
654             cancelSpeakerphone();
655         }
656     }
657 
notifyMute()658     private void notifyMute() {
659         if (!mShowingMuteIcon) {
660             mStatusBarManager.setIcon("mute", android.R.drawable.stat_notify_call_mute, 0,
661                     mContext.getString(R.string.accessibility_call_muted));
662             mShowingMuteIcon = true;
663         }
664     }
665 
cancelMute()666     private void cancelMute() {
667         if (mShowingMuteIcon) {
668             mStatusBarManager.removeIcon("mute");
669             mShowingMuteIcon = false;
670         }
671     }
672 
673     /**
674      * Shows or hides the "mute" notification in the status bar,
675      * based on the current mute state of the Phone.
676      *
677      * (But note that the status bar icon is *never* shown while the in-call UI
678      * is active; it only appears if you bail out to some other activity.)
679      */
updateMuteNotification()680     void updateMuteNotification() {
681         // Suppress the status bar icon if the the InCallScreen is the
682         // foreground activity, since the in-call UI already provides an
683         // onscreen indication of the mute state.  (This reduces clutter
684         // in the status bar.)
685 
686         if ((mCM.getState() == PhoneConstants.State.OFFHOOK) && PhoneUtils.getMute()) {
687             if (DBG) log("updateMuteNotification: MUTED");
688             notifyMute();
689         } else {
690             if (DBG) log("updateMuteNotification: not muted (or not offhook)");
691             cancelMute();
692         }
693     }
694 
695     /**
696      * Completely take down the in-call notification *and* the mute/speaker
697      * notifications as well, to indicate that the phone is now idle.
698      */
cancelCallInProgressNotifications()699     /* package */ void cancelCallInProgressNotifications() {
700         if (DBG) log("cancelCallInProgressNotifications");
701         cancelMute();
702         cancelSpeakerphone();
703     }
704 
705     /**
706      * Updates the message waiting indicator (voicemail) notification.
707      *
708      * @param visible true if there are messages waiting
709      */
updateMwi(boolean visible)710     /* package */ void updateMwi(boolean visible) {
711         if (DBG) log("updateMwi(): " + visible);
712 
713         if (visible) {
714             int resId = android.R.drawable.stat_notify_voicemail;
715 
716             // This Notification can get a lot fancier once we have more
717             // information about the current voicemail messages.
718             // (For example, the current voicemail system can't tell
719             // us the caller-id or timestamp of a message, or tell us the
720             // message count.)
721 
722             // But for now, the UI is ultra-simple: if the MWI indication
723             // is supposed to be visible, just show a single generic
724             // notification.
725 
726             String notificationTitle = mContext.getString(R.string.notification_voicemail_title);
727             String vmNumber = mPhone.getVoiceMailNumber();
728             if (DBG) log("- got vm number: '" + vmNumber + "'");
729 
730             // Watch out: vmNumber may be null, for two possible reasons:
731             //
732             //   (1) This phone really has no voicemail number
733             //
734             //   (2) This phone *does* have a voicemail number, but
735             //       the SIM isn't ready yet.
736             //
737             // Case (2) *does* happen in practice if you have voicemail
738             // messages when the device first boots: we get an MWI
739             // notification as soon as we register on the network, but the
740             // SIM hasn't finished loading yet.
741             //
742             // So handle case (2) by retrying the lookup after a short
743             // delay.
744 
745             if ((vmNumber == null) && !mPhone.getIccRecordsLoaded()) {
746                 if (DBG) log("- Null vm number: SIM records not loaded (yet)...");
747 
748                 // TODO: rather than retrying after an arbitrary delay, it
749                 // would be cleaner to instead just wait for a
750                 // SIM_RECORDS_LOADED notification.
751                 // (Unfortunately right now there's no convenient way to
752                 // get that notification in phone app code.  We'd first
753                 // want to add a call like registerForSimRecordsLoaded()
754                 // to Phone.java and GSMPhone.java, and *then* we could
755                 // listen for that in the CallNotifier class.)
756 
757                 // Limit the number of retries (in case the SIM is broken
758                 // or missing and can *never* load successfully.)
759                 if (mVmNumberRetriesRemaining-- > 0) {
760                     if (DBG) log("  - Retrying in " + VM_NUMBER_RETRY_DELAY_MILLIS + " msec...");
761                     mApp.notifier.sendMwiChangedDelayed(VM_NUMBER_RETRY_DELAY_MILLIS);
762                     return;
763                 } else {
764                     Log.w(LOG_TAG, "NotificationMgr.updateMwi: getVoiceMailNumber() failed after "
765                           + MAX_VM_NUMBER_RETRIES + " retries; giving up.");
766                     // ...and continue with vmNumber==null, just as if the
767                     // phone had no VM number set up in the first place.
768                 }
769             }
770 
771             if (TelephonyCapabilities.supportsVoiceMessageCount(mPhone)) {
772                 int vmCount = mPhone.getVoiceMessageCount();
773                 String titleFormat = mContext.getString(R.string.notification_voicemail_title_count);
774                 notificationTitle = String.format(titleFormat, vmCount);
775             }
776 
777             String notificationText;
778             if (TextUtils.isEmpty(vmNumber)) {
779                 notificationText = mContext.getString(
780                         R.string.notification_voicemail_no_vm_number);
781             } else {
782                 notificationText = String.format(
783                         mContext.getString(R.string.notification_voicemail_text_format),
784                         PhoneNumberUtils.formatNumber(vmNumber));
785             }
786 
787             Intent intent = new Intent(Intent.ACTION_CALL,
788                     Uri.fromParts(Constants.SCHEME_VOICEMAIL, "", null));
789             PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
790 
791             SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
792             Uri ringtoneUri;
793             String uriString = prefs.getString(
794                     CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_RINGTONE_KEY, null);
795             if (!TextUtils.isEmpty(uriString)) {
796                 ringtoneUri = Uri.parse(uriString);
797             } else {
798                 ringtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI;
799             }
800 
801             Notification.Builder builder = new Notification.Builder(mContext);
802             builder.setSmallIcon(resId)
803                     .setWhen(System.currentTimeMillis())
804                     .setContentTitle(notificationTitle)
805                     .setContentText(notificationText)
806                     .setContentIntent(pendingIntent)
807                     .setSound(ringtoneUri);
808             Notification notification = builder.getNotification();
809 
810             CallFeaturesSetting.migrateVoicemailVibrationSettingsIfNeeded(prefs);
811             final boolean vibrate = prefs.getBoolean(
812                     CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_VIBRATE_KEY, false);
813             if (vibrate) {
814                 notification.defaults |= Notification.DEFAULT_VIBRATE;
815             }
816             notification.flags |= Notification.FLAG_NO_CLEAR;
817             configureLedNotification(notification);
818             mNotificationManager.notify(VOICEMAIL_NOTIFICATION, notification);
819         } else {
820             mNotificationManager.cancel(VOICEMAIL_NOTIFICATION);
821         }
822     }
823 
824     /**
825      * Updates the message call forwarding indicator notification.
826      *
827      * @param visible true if there are messages waiting
828      */
updateCfi(boolean visible)829     /* package */ void updateCfi(boolean visible) {
830         if (DBG) log("updateCfi(): " + visible);
831         if (visible) {
832             // If Unconditional Call Forwarding (forward all calls) for VOICE
833             // is enabled, just show a notification.  We'll default to expanded
834             // view for now, so the there is less confusion about the icon.  If
835             // it is deemed too weird to have CF indications as expanded views,
836             // then we'll flip the flag back.
837 
838             // TODO: We may want to take a look to see if the notification can
839             // display the target to forward calls to.  This will require some
840             // effort though, since there are multiple layers of messages that
841             // will need to propagate that information.
842 
843             Notification notification;
844             final boolean showExpandedNotification = true;
845             if (showExpandedNotification) {
846                 Intent intent = new Intent(Intent.ACTION_MAIN);
847                 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
848                 intent.setClassName("com.android.phone",
849                         "com.android.phone.CallFeaturesSetting");
850 
851                 notification = new Notification(
852                         R.drawable.stat_sys_phone_call_forward,  // icon
853                         null, // tickerText
854                         0); // The "timestamp" of this notification is meaningless;
855                             // we only care about whether CFI is currently on or not.
856                 notification.setLatestEventInfo(
857                         mContext, // context
858                         mContext.getString(R.string.labelCF), // expandedTitle
859                         mContext.getString(R.string.sum_cfu_enabled_indicator), // expandedText
860                         PendingIntent.getActivity(mContext, 0, intent, 0)); // contentIntent
861             } else {
862                 notification = new Notification(
863                         R.drawable.stat_sys_phone_call_forward,  // icon
864                         null,  // tickerText
865                         System.currentTimeMillis()  // when
866                         );
867             }
868 
869             notification.flags |= Notification.FLAG_ONGOING_EVENT;  // also implies FLAG_NO_CLEAR
870 
871             mNotificationManager.notify(
872                     CALL_FORWARD_NOTIFICATION,
873                     notification);
874         } else {
875             mNotificationManager.cancel(CALL_FORWARD_NOTIFICATION);
876         }
877     }
878 
879     /**
880      * Shows the "data disconnected due to roaming" notification, which
881      * appears when you lose data connectivity because you're roaming and
882      * you have the "data roaming" feature turned off.
883      */
showDataDisconnectedRoaming()884     /* package */ void showDataDisconnectedRoaming() {
885         if (DBG) log("showDataDisconnectedRoaming()...");
886 
887         // "Mobile network settings" screen / dialog
888         Intent intent = new Intent(mContext, com.android.phone.MobileNetworkSettings.class);
889 
890         final CharSequence contentText = mContext.getText(R.string.roaming_reenable_message);
891 
892         final Notification.Builder builder = new Notification.Builder(mContext);
893         builder.setSmallIcon(android.R.drawable.stat_sys_warning);
894         builder.setContentTitle(mContext.getText(R.string.roaming));
895         builder.setContentText(contentText);
896         builder.setContentIntent(PendingIntent.getActivity(mContext, 0, intent, 0));
897 
898         final Notification notif = new Notification.BigTextStyle(builder).bigText(contentText)
899                 .build();
900 
901         mNotificationManager.notify(DATA_DISCONNECTED_ROAMING_NOTIFICATION, notif);
902     }
903 
904     /**
905      * Turns off the "data disconnected due to roaming" notification.
906      */
hideDataDisconnectedRoaming()907     /* package */ void hideDataDisconnectedRoaming() {
908         if (DBG) log("hideDataDisconnectedRoaming()...");
909         mNotificationManager.cancel(DATA_DISCONNECTED_ROAMING_NOTIFICATION);
910     }
911 
912     /**
913      * Display the network selection "no service" notification
914      * @param operator is the numeric operator number
915      */
showNetworkSelection(String operator)916     private void showNetworkSelection(String operator) {
917         if (DBG) log("showNetworkSelection(" + operator + ")...");
918 
919         String titleText = mContext.getString(
920                 R.string.notification_network_selection_title);
921         String expandedText = mContext.getString(
922                 R.string.notification_network_selection_text, operator);
923 
924         Notification notification = new Notification();
925         notification.icon = android.R.drawable.stat_sys_warning;
926         notification.when = 0;
927         notification.flags = Notification.FLAG_ONGOING_EVENT;
928         notification.tickerText = null;
929 
930         // create the target network operators settings intent
931         Intent intent = new Intent(Intent.ACTION_MAIN);
932         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
933                 Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
934         // Use NetworkSetting to handle the selection intent
935         intent.setComponent(new ComponentName("com.android.phone",
936                 "com.android.phone.NetworkSetting"));
937         PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0);
938 
939         notification.setLatestEventInfo(mContext, titleText, expandedText, pi);
940 
941         mNotificationManager.notify(SELECTED_OPERATOR_FAIL_NOTIFICATION, notification);
942     }
943 
944     /**
945      * Turn off the network selection "no service" notification
946      */
cancelNetworkSelection()947     private void cancelNetworkSelection() {
948         if (DBG) log("cancelNetworkSelection()...");
949         mNotificationManager.cancel(SELECTED_OPERATOR_FAIL_NOTIFICATION);
950     }
951 
952     /**
953      * Update notification about no service of user selected operator
954      *
955      * @param serviceState Phone service state
956      */
updateNetworkSelection(int serviceState)957     void updateNetworkSelection(int serviceState) {
958         if (TelephonyCapabilities.supportsNetworkSelection(mPhone)) {
959             // get the shared preference of network_selection.
960             // empty is auto mode, otherwise it is the operator alpha name
961             // in case there is no operator name, check the operator numeric
962             SharedPreferences sp =
963                     PreferenceManager.getDefaultSharedPreferences(mContext);
964             String networkSelection =
965                     sp.getString(PhoneBase.NETWORK_SELECTION_NAME_KEY, "");
966             if (TextUtils.isEmpty(networkSelection)) {
967                 networkSelection =
968                         sp.getString(PhoneBase.NETWORK_SELECTION_KEY, "");
969             }
970 
971             if (DBG) log("updateNetworkSelection()..." + "state = " +
972                     serviceState + " new network " + networkSelection);
973 
974             if (serviceState == ServiceState.STATE_OUT_OF_SERVICE
975                     && !TextUtils.isEmpty(networkSelection)) {
976                 if (!mSelectedUnavailableNotify) {
977                     showNetworkSelection(networkSelection);
978                     mSelectedUnavailableNotify = true;
979                 }
980             } else {
981                 if (mSelectedUnavailableNotify) {
982                     cancelNetworkSelection();
983                     mSelectedUnavailableNotify = false;
984                 }
985             }
986         }
987     }
988 
postTransientNotification(int notifyId, CharSequence msg)989     /* package */ void postTransientNotification(int notifyId, CharSequence msg) {
990         if (mToast != null) {
991             mToast.cancel();
992         }
993 
994         mToast = Toast.makeText(mContext, msg, Toast.LENGTH_LONG);
995         mToast.show();
996     }
997 
log(String msg)998     private void log(String msg) {
999         Log.d(LOG_TAG, msg);
1000     }
1001 }
1002