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