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