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