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