Home | History | Annotate | Download | only in phone
      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      */
    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      */
    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 
    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          */
    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          */
    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          */
    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          */
    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      */
    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 
    327         public QueryHandler(ContentResolver cr) {
    328             super(cr);
    329         }
    330 
    331         /**
    332          * Handles the query results.
    333          */
    334         @Override
    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
    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          */
    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      */
    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      */
    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. */
    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      */
    590     void cancelMissedCallNotification() {
    591         // reset the number of missed calls to 0.
    592         mNumberMissedCalls = 0;
    593         mNotificationManager.cancel(MISSED_CALL_NOTIFICATION);
    594     }
    595 
    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 
    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      */
    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      */
    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 
    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 
    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      */
    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      */
    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      */
    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      */
    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
   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      */
   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      */
   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      */
   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             String vibrateWhen = prefs.getString(
   1281                     CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_VIBRATE_WHEN_KEY, "never");
   1282             boolean vibrateAlways = vibrateWhen.equals("always");
   1283             boolean vibrateSilent = vibrateWhen.equals("silent");
   1284             AudioManager audioManager =
   1285                     (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
   1286             boolean nowSilent = audioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE;
   1287             if (vibrateAlways || (vibrateSilent && nowSilent)) {
   1288                 notification.defaults |= Notification.DEFAULT_VIBRATE;
   1289             }
   1290 
   1291             notification.flags |= Notification.FLAG_NO_CLEAR;
   1292             configureLedNotification(notification);
   1293             mNotificationManager.notify(VOICEMAIL_NOTIFICATION, notification);
   1294         } else {
   1295             mNotificationManager.cancel(VOICEMAIL_NOTIFICATION);
   1296         }
   1297     }
   1298 
   1299     /**
   1300      * Updates the message call forwarding indicator notification.
   1301      *
   1302      * @param visible true if there are messages waiting
   1303      */
   1304     /* package */ void updateCfi(boolean visible) {
   1305         if (DBG) log("updateCfi(): " + visible);
   1306         if (visible) {
   1307             // If Unconditional Call Forwarding (forward all calls) for VOICE
   1308             // is enabled, just show a notification.  We'll default to expanded
   1309             // view for now, so the there is less confusion about the icon.  If
   1310             // it is deemed too weird to have CF indications as expanded views,
   1311             // then we'll flip the flag back.
   1312 
   1313             // TODO: We may want to take a look to see if the notification can
   1314             // display the target to forward calls to.  This will require some
   1315             // effort though, since there are multiple layers of messages that
   1316             // will need to propagate that information.
   1317 
   1318             Notification notification;
   1319             final boolean showExpandedNotification = true;
   1320             if (showExpandedNotification) {
   1321                 Intent intent = new Intent(Intent.ACTION_MAIN);
   1322                 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
   1323                 intent.setClassName("com.android.phone",
   1324                         "com.android.phone.CallFeaturesSetting");
   1325 
   1326                 notification = new Notification(
   1327                         R.drawable.stat_sys_phone_call_forward,  // icon
   1328                         null, // tickerText
   1329                         0); // The "timestamp" of this notification is meaningless;
   1330                             // we only care about whether CFI is currently on or not.
   1331                 notification.setLatestEventInfo(
   1332                         mContext, // context
   1333                         mContext.getString(R.string.labelCF), // expandedTitle
   1334                         mContext.getString(R.string.sum_cfu_enabled_indicator), // expandedText
   1335                         PendingIntent.getActivity(mContext, 0, intent, 0)); // contentIntent
   1336             } else {
   1337                 notification = new Notification(
   1338                         R.drawable.stat_sys_phone_call_forward,  // icon
   1339                         null,  // tickerText
   1340                         System.currentTimeMillis()  // when
   1341                         );
   1342             }
   1343 
   1344             notification.flags |= Notification.FLAG_ONGOING_EVENT;  // also implies FLAG_NO_CLEAR
   1345 
   1346             mNotificationManager.notify(
   1347                     CALL_FORWARD_NOTIFICATION,
   1348                     notification);
   1349         } else {
   1350             mNotificationManager.cancel(CALL_FORWARD_NOTIFICATION);
   1351         }
   1352     }
   1353 
   1354     /**
   1355      * Shows the "data disconnected due to roaming" notification, which
   1356      * appears when you lose data connectivity because you're roaming and
   1357      * you have the "data roaming" feature turned off.
   1358      */
   1359     /* package */ void showDataDisconnectedRoaming() {
   1360         if (DBG) log("showDataDisconnectedRoaming()...");
   1361 
   1362         // "Mobile network settings" screen / dialog
   1363         Intent intent = new Intent(mContext,
   1364                 com.android.phone.MobileNetworkSettings.class);
   1365 
   1366         Notification notification = new Notification(
   1367                 android.R.drawable.stat_sys_warning, // icon
   1368                 null, // tickerText
   1369                 System.currentTimeMillis());
   1370         notification.setLatestEventInfo(
   1371                 mContext, // Context
   1372                 mContext.getString(R.string.roaming), // expandedTitle
   1373                 mContext.getString(R.string.roaming_reenable_message), // expandedText
   1374                 PendingIntent.getActivity(mContext, 0, intent, 0)); // contentIntent
   1375 
   1376         mNotificationManager.notify(
   1377                 DATA_DISCONNECTED_ROAMING_NOTIFICATION,
   1378                 notification);
   1379     }
   1380 
   1381     /**
   1382      * Turns off the "data disconnected due to roaming" notification.
   1383      */
   1384     /* package */ void hideDataDisconnectedRoaming() {
   1385         if (DBG) log("hideDataDisconnectedRoaming()...");
   1386         mNotificationManager.cancel(DATA_DISCONNECTED_ROAMING_NOTIFICATION);
   1387     }
   1388 
   1389     /**
   1390      * Display the network selection "no service" notification
   1391      * @param operator is the numeric operator number
   1392      */
   1393     private void showNetworkSelection(String operator) {
   1394         if (DBG) log("showNetworkSelection(" + operator + ")...");
   1395 
   1396         String titleText = mContext.getString(
   1397                 R.string.notification_network_selection_title);
   1398         String expandedText = mContext.getString(
   1399                 R.string.notification_network_selection_text, operator);
   1400 
   1401         Notification notification = new Notification();
   1402         notification.icon = android.R.drawable.stat_sys_warning;
   1403         notification.when = 0;
   1404         notification.flags = Notification.FLAG_ONGOING_EVENT;
   1405         notification.tickerText = null;
   1406 
   1407         // create the target network operators settings intent
   1408         Intent intent = new Intent(Intent.ACTION_MAIN);
   1409         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
   1410                 Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
   1411         // Use NetworkSetting to handle the selection intent
   1412         intent.setComponent(new ComponentName("com.android.phone",
   1413                 "com.android.phone.NetworkSetting"));
   1414         PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0);
   1415 
   1416         notification.setLatestEventInfo(mContext, titleText, expandedText, pi);
   1417 
   1418         mNotificationManager.notify(SELECTED_OPERATOR_FAIL_NOTIFICATION, notification);
   1419     }
   1420 
   1421     /**
   1422      * Turn off the network selection "no service" notification
   1423      */
   1424     private void cancelNetworkSelection() {
   1425         if (DBG) log("cancelNetworkSelection()...");
   1426         mNotificationManager.cancel(SELECTED_OPERATOR_FAIL_NOTIFICATION);
   1427     }
   1428 
   1429     /**
   1430      * Update notification about no service of user selected operator
   1431      *
   1432      * @param serviceState Phone service state
   1433      */
   1434     void updateNetworkSelection(int serviceState) {
   1435         if (TelephonyCapabilities.supportsNetworkSelection(mPhone)) {
   1436             // get the shared preference of network_selection.
   1437             // empty is auto mode, otherwise it is the operator alpha name
   1438             // in case there is no operator name, check the operator numeric
   1439             SharedPreferences sp =
   1440                     PreferenceManager.getDefaultSharedPreferences(mContext);
   1441             String networkSelection =
   1442                     sp.getString(PhoneBase.NETWORK_SELECTION_NAME_KEY, "");
   1443             if (TextUtils.isEmpty(networkSelection)) {
   1444                 networkSelection =
   1445                         sp.getString(PhoneBase.NETWORK_SELECTION_KEY, "");
   1446             }
   1447 
   1448             if (DBG) log("updateNetworkSelection()..." + "state = " +
   1449                     serviceState + " new network " + networkSelection);
   1450 
   1451             if (serviceState == ServiceState.STATE_OUT_OF_SERVICE
   1452                     && !TextUtils.isEmpty(networkSelection)) {
   1453                 if (!mSelectedUnavailableNotify) {
   1454                     showNetworkSelection(networkSelection);
   1455                     mSelectedUnavailableNotify = true;
   1456                 }
   1457             } else {
   1458                 if (mSelectedUnavailableNotify) {
   1459                     cancelNetworkSelection();
   1460                     mSelectedUnavailableNotify = false;
   1461                 }
   1462             }
   1463         }
   1464     }
   1465 
   1466     /* package */ void postTransientNotification(int notifyId, CharSequence msg) {
   1467         if (mToast != null) {
   1468             mToast.cancel();
   1469         }
   1470 
   1471         mToast = Toast.makeText(mContext, msg, Toast.LENGTH_LONG);
   1472         mToast.show();
   1473     }
   1474 
   1475     private void log(String msg) {
   1476         Log.d(LOG_TAG, msg);
   1477     }
   1478 }
   1479