Home | History | Annotate | Download | only in phone
      1 /*
      2  * Copyright (C) 2006 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.phone;
     18 
     19 import android.app.Notification;
     20 import android.app.NotificationManager;
     21 import android.app.PendingIntent;
     22 import android.app.StatusBarManager;
     23 import android.content.AsyncQueryHandler;
     24 import android.content.ComponentName;
     25 import android.content.ContentResolver;
     26 import android.content.ContentUris;
     27 import android.content.Context;
     28 import android.content.Intent;
     29 import android.content.SharedPreferences;
     30 import android.database.Cursor;
     31 import android.graphics.Bitmap;
     32 import android.graphics.drawable.BitmapDrawable;
     33 import android.graphics.drawable.Drawable;
     34 import android.media.AudioManager;
     35 import android.net.Uri;
     36 import android.os.PowerManager;
     37 import android.os.SystemProperties;
     38 import android.preference.PreferenceManager;
     39 import android.provider.CallLog.Calls;
     40 import android.provider.ContactsContract.Contacts;
     41 import android.provider.ContactsContract.PhoneLookup;
     42 import android.provider.Settings;
     43 import android.telephony.PhoneNumberUtils;
     44 import android.telephony.ServiceState;
     45 import android.text.BidiFormatter;
     46 import android.text.TextDirectionHeuristics;
     47 import android.text.TextUtils;
     48 import android.util.Log;
     49 import android.widget.Toast;
     50 
     51 import com.android.internal.telephony.Call;
     52 import com.android.internal.telephony.CallManager;
     53 import com.android.internal.telephony.CallerInfo;
     54 import com.android.internal.telephony.CallerInfoAsyncQuery;
     55 import com.android.internal.telephony.Connection;
     56 import com.android.internal.telephony.Phone;
     57 import com.android.internal.telephony.PhoneBase;
     58 import com.android.internal.telephony.PhoneConstants;
     59 import com.android.internal.telephony.TelephonyCapabilities;
     60 
     61 /**
     62  * NotificationManager-related utility code for the Phone app.
     63  *
     64  * This is a singleton object which acts as the interface to the
     65  * framework's NotificationManager, and is used to display status bar
     66  * icons and control other status bar-related behavior.
     67  *
     68  * @see PhoneGlobals.notificationMgr
     69  */
     70 public class NotificationMgr {
     71     private static final String LOG_TAG = "NotificationMgr";
     72     private static final boolean DBG =
     73             (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
     74     // Do not check in with VDBG = true, since that may write PII to the system log.
     75     private static final boolean VDBG = false;
     76 
     77     private static final String[] CALL_LOG_PROJECTION = new String[] {
     78         Calls._ID,
     79         Calls.NUMBER,
     80         Calls.NUMBER_PRESENTATION,
     81         Calls.DATE,
     82         Calls.DURATION,
     83         Calls.TYPE,
     84     };
     85 
     86     // notification types
     87     static final int MISSED_CALL_NOTIFICATION = 1;
     88     static final int IN_CALL_NOTIFICATION = 2;
     89     static final int MMI_NOTIFICATION = 3;
     90     static final int NETWORK_SELECTION_NOTIFICATION = 4;
     91     static final int VOICEMAIL_NOTIFICATION = 5;
     92     static final int CALL_FORWARD_NOTIFICATION = 6;
     93     static final int DATA_DISCONNECTED_ROAMING_NOTIFICATION = 7;
     94     static final int SELECTED_OPERATOR_FAIL_NOTIFICATION = 8;
     95 
     96     /** The singleton NotificationMgr instance. */
     97     private static NotificationMgr sInstance;
     98 
     99     private PhoneGlobals mApp;
    100     private Phone mPhone;
    101     private CallManager mCM;
    102 
    103     private Context mContext;
    104     private NotificationManager mNotificationManager;
    105     private StatusBarManager mStatusBarManager;
    106     private Toast mToast;
    107     private boolean mShowingSpeakerphoneIcon;
    108     private boolean mShowingMuteIcon;
    109 
    110     public StatusBarHelper statusBarHelper;
    111 
    112     // used to track the missed call counter, default to 0.
    113     private int mNumberMissedCalls = 0;
    114 
    115     // used to track the notification of selected network unavailable
    116     private boolean mSelectedUnavailableNotify = false;
    117 
    118     // Retry params for the getVoiceMailNumber() call; see updateMwi().
    119     private static final int MAX_VM_NUMBER_RETRIES = 5;
    120     private static final int VM_NUMBER_RETRY_DELAY_MILLIS = 10000;
    121     private int mVmNumberRetriesRemaining = MAX_VM_NUMBER_RETRIES;
    122 
    123     // Query used to look up caller-id info for the "call log" notification.
    124     private QueryHandler mQueryHandler = null;
    125     private static final int CALL_LOG_TOKEN = -1;
    126     private static final int CONTACT_TOKEN = -2;
    127 
    128     /**
    129      * Private constructor (this is a singleton).
    130      * @see init()
    131      */
    132     private NotificationMgr(PhoneGlobals app) {
    133         mApp = app;
    134         mContext = app;
    135         mNotificationManager =
    136                 (NotificationManager) app.getSystemService(Context.NOTIFICATION_SERVICE);
    137         mStatusBarManager =
    138                 (StatusBarManager) app.getSystemService(Context.STATUS_BAR_SERVICE);
    139         mPhone = app.phone;  // TODO: better style to use mCM.getDefaultPhone() everywhere instead
    140         mCM = app.mCM;
    141         statusBarHelper = new StatusBarHelper();
    142     }
    143 
    144     /**
    145      * Initialize the singleton NotificationMgr instance.
    146      *
    147      * This is only done once, at startup, from PhoneApp.onCreate().
    148      * From then on, the NotificationMgr instance is available via the
    149      * PhoneApp's public "notificationMgr" field, which is why there's no
    150      * getInstance() method here.
    151      */
    152     /* package */ static NotificationMgr init(PhoneGlobals app) {
    153         synchronized (NotificationMgr.class) {
    154             if (sInstance == null) {
    155                 sInstance = new NotificationMgr(app);
    156                 // Update the notifications that need to be touched at startup.
    157                 sInstance.updateNotificationsAtStartup();
    158             } else {
    159                 Log.wtf(LOG_TAG, "init() called multiple times!  sInstance = " + sInstance);
    160             }
    161             return sInstance;
    162         }
    163     }
    164 
    165     /**
    166      * Helper class that's a wrapper around the framework's
    167      * StatusBarManager.disable() API.
    168      *
    169      * This class is used to control features like:
    170      *
    171      *   - Disabling the status bar "notification windowshade"
    172      *     while the in-call UI is up
    173      *
    174      *   - Disabling notification alerts (audible or vibrating)
    175      *     while a phone call is active
    176      *
    177      *   - Disabling navigation via the system bar (the "soft buttons" at
    178      *     the bottom of the screen on devices with no hard buttons)
    179      *
    180      * We control these features through a single point of control to make
    181      * sure that the various StatusBarManager.disable() calls don't
    182      * interfere with each other.
    183      */
    184     public class StatusBarHelper {
    185         // Current desired state of status bar / system bar behavior
    186         private boolean mIsNotificationEnabled = true;
    187         private boolean mIsExpandedViewEnabled = true;
    188         private boolean mIsSystemBarNavigationEnabled = true;
    189 
    190         private StatusBarHelper () {
    191         }
    192 
    193         /**
    194          * Enables or disables auditory / vibrational alerts.
    195          *
    196          * (We disable these any time a voice call is active, regardless
    197          * of whether or not the in-call UI is visible.)
    198          */
    199         public void enableNotificationAlerts(boolean enable) {
    200             if (mIsNotificationEnabled != enable) {
    201                 mIsNotificationEnabled = enable;
    202                 updateStatusBar();
    203             }
    204         }
    205 
    206         /**
    207          * Enables or disables the expanded view of the status bar
    208          * (i.e. the ability to pull down the "notification windowshade").
    209          *
    210          * (This feature is disabled by the InCallScreen while the in-call
    211          * UI is active.)
    212          */
    213         public void enableExpandedView(boolean enable) {
    214             if (mIsExpandedViewEnabled != enable) {
    215                 mIsExpandedViewEnabled = enable;
    216                 updateStatusBar();
    217             }
    218         }
    219 
    220         /**
    221          * Enables or disables the navigation via the system bar (the
    222          * "soft buttons" at the bottom of the screen)
    223          *
    224          * (This feature is disabled while an incoming call is ringing,
    225          * because it's easy to accidentally touch the system bar while
    226          * pulling the phone out of your pocket.)
    227          */
    228         public void enableSystemBarNavigation(boolean enable) {
    229             if (mIsSystemBarNavigationEnabled != enable) {
    230                 mIsSystemBarNavigationEnabled = enable;
    231                 updateStatusBar();
    232             }
    233         }
    234 
    235         /**
    236          * Updates the status bar to reflect the current desired state.
    237          */
    238         private void updateStatusBar() {
    239             int state = StatusBarManager.DISABLE_NONE;
    240 
    241             if (!mIsExpandedViewEnabled) {
    242                 state |= StatusBarManager.DISABLE_EXPAND;
    243             }
    244             if (!mIsNotificationEnabled) {
    245                 state |= StatusBarManager.DISABLE_NOTIFICATION_ALERTS;
    246             }
    247             if (!mIsSystemBarNavigationEnabled) {
    248                 // Disable *all* possible navigation via the system bar.
    249                 state |= StatusBarManager.DISABLE_HOME;
    250                 state |= StatusBarManager.DISABLE_RECENT;
    251                 state |= StatusBarManager.DISABLE_BACK;
    252                 state |= StatusBarManager.DISABLE_SEARCH;
    253             }
    254 
    255             if (DBG) log("updateStatusBar: state = 0x" + Integer.toHexString(state));
    256             mStatusBarManager.disable(state);
    257         }
    258     }
    259 
    260     /**
    261      * Makes sure phone-related notifications are up to date on a
    262      * freshly-booted device.
    263      */
    264     private void updateNotificationsAtStartup() {
    265         if (DBG) log("updateNotificationsAtStartup()...");
    266 
    267         // instantiate query handler
    268         mQueryHandler = new QueryHandler(mContext.getContentResolver());
    269 
    270         // setup query spec, look for all Missed calls that are new.
    271         StringBuilder where = new StringBuilder("type=");
    272         where.append(Calls.MISSED_TYPE);
    273         where.append(" AND new=1");
    274 
    275         // start the query
    276         if (DBG) log("- start call log query...");
    277         mQueryHandler.startQuery(CALL_LOG_TOKEN, null, Calls.CONTENT_URI,  CALL_LOG_PROJECTION,
    278                 where.toString(), null, Calls.DEFAULT_SORT_ORDER);
    279 
    280         // Depend on android.app.StatusBarManager to be set to
    281         // disable(DISABLE_NONE) upon startup.  This will be the
    282         // case even if the phone app crashes.
    283     }
    284 
    285     /** The projection to use when querying the phones table */
    286     static final String[] PHONES_PROJECTION = new String[] {
    287         PhoneLookup.NUMBER,
    288         PhoneLookup.DISPLAY_NAME,
    289         PhoneLookup._ID
    290     };
    291 
    292     /**
    293      * Class used to run asynchronous queries to re-populate the notifications we care about.
    294      * There are really 3 steps to this:
    295      *  1. Find the list of missed calls
    296      *  2. For each call, run a query to retrieve the caller's name.
    297      *  3. For each caller, try obtaining photo.
    298      */
    299     private class QueryHandler extends AsyncQueryHandler
    300             implements ContactsAsyncHelper.OnImageLoadCompleteListener {
    301 
    302         /**
    303          * Used to store relevant fields for the Missed Call
    304          * notifications.
    305          */
    306         private class NotificationInfo {
    307             public String name;
    308             public String number;
    309             public int presentation;
    310             /**
    311              * Type of the call. {@link android.provider.CallLog.Calls#INCOMING_TYPE}
    312              * {@link android.provider.CallLog.Calls#OUTGOING_TYPE}, or
    313              * {@link android.provider.CallLog.Calls#MISSED_TYPE}.
    314              */
    315             public String type;
    316             public long date;
    317         }
    318 
    319         public QueryHandler(ContentResolver cr) {
    320             super(cr);
    321         }
    322 
    323         /**
    324          * Handles the query results.
    325          */
    326         @Override
    327         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
    328             // TODO: it would be faster to use a join here, but for the purposes
    329             // of this small record set, it should be ok.
    330 
    331             // Note that CursorJoiner is not useable here because the number
    332             // comparisons are not strictly equals; the comparisons happen in
    333             // the SQL function PHONE_NUMBERS_EQUAL, which is not available for
    334             // the CursorJoiner.
    335 
    336             // Executing our own query is also feasible (with a join), but that
    337             // will require some work (possibly destabilizing) in Contacts
    338             // Provider.
    339 
    340             // At this point, we will execute subqueries on each row just as
    341             // CallLogActivity.java does.
    342             switch (token) {
    343                 case CALL_LOG_TOKEN:
    344                     if (DBG) log("call log query complete.");
    345 
    346                     // initial call to retrieve the call list.
    347                     if (cursor != null) {
    348                         while (cursor.moveToNext()) {
    349                             // for each call in the call log list, create
    350                             // the notification object and query contacts
    351                             NotificationInfo n = getNotificationInfo (cursor);
    352 
    353                             if (DBG) log("query contacts for number: " + n.number);
    354 
    355                             mQueryHandler.startQuery(CONTACT_TOKEN, n,
    356                                     Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, n.number),
    357                                     PHONES_PROJECTION, null, null, PhoneLookup.NUMBER);
    358                         }
    359 
    360                         if (DBG) log("closing call log cursor.");
    361                         cursor.close();
    362                     }
    363                     break;
    364                 case CONTACT_TOKEN:
    365                     if (DBG) log("contact query complete.");
    366 
    367                     // subqueries to get the caller name.
    368                     if ((cursor != null) && (cookie != null)){
    369                         NotificationInfo n = (NotificationInfo) cookie;
    370 
    371                         Uri personUri = null;
    372                         if (cursor.moveToFirst()) {
    373                             n.name = cursor.getString(
    374                                     cursor.getColumnIndexOrThrow(PhoneLookup.DISPLAY_NAME));
    375                             long person_id = cursor.getLong(
    376                                     cursor.getColumnIndexOrThrow(PhoneLookup._ID));
    377                             if (DBG) {
    378                                 log("contact :" + n.name + " found for phone: " + n.number
    379                                         + ". id : " + person_id);
    380                             }
    381                             personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, person_id);
    382                         }
    383 
    384                         if (personUri != null) {
    385                             if (DBG) {
    386                                 log("Start obtaining picture for the missed call. Uri: "
    387                                         + personUri);
    388                             }
    389                             // Now try to obtain a photo for this person.
    390                             // ContactsAsyncHelper will do that and call onImageLoadComplete()
    391                             // after that.
    392                             ContactsAsyncHelper.startObtainPhotoAsync(
    393                                     0, mContext, personUri, this, n);
    394                         } else {
    395                             if (DBG) {
    396                                 log("Failed to find Uri for obtaining photo."
    397                                         + " Just send notification without it.");
    398                             }
    399                             // We couldn't find person Uri, so we're sure we cannot obtain a photo.
    400                             // Call notifyMissedCall() right now.
    401                             notifyMissedCall(n.name, n.number, n.presentation, n.type, null, null,
    402                                     n.date);
    403                         }
    404 
    405                         if (DBG) log("closing contact cursor.");
    406                         cursor.close();
    407                     }
    408                     break;
    409                 default:
    410             }
    411         }
    412 
    413         @Override
    414         public void onImageLoadComplete(
    415                 int token, Drawable photo, Bitmap photoIcon, Object cookie) {
    416             if (DBG) log("Finished loading image: " + photo);
    417             NotificationInfo n = (NotificationInfo) cookie;
    418             notifyMissedCall(n.name, n.number, n.presentation, n.type, photo, photoIcon, n.date);
    419         }
    420 
    421         /**
    422          * Factory method to generate a NotificationInfo object given a
    423          * cursor from the call log table.
    424          */
    425         private final NotificationInfo getNotificationInfo(Cursor cursor) {
    426             NotificationInfo n = new NotificationInfo();
    427             n.name = null;
    428             n.number = cursor.getString(cursor.getColumnIndexOrThrow(Calls.NUMBER));
    429             n.presentation = cursor.getInt(cursor.getColumnIndexOrThrow(Calls.NUMBER_PRESENTATION));
    430             n.type = cursor.getString(cursor.getColumnIndexOrThrow(Calls.TYPE));
    431             n.date = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DATE));
    432 
    433             // make sure we update the number depending upon saved values in
    434             // CallLog.addCall().  If either special values for unknown or
    435             // private number are detected, we need to hand off the message
    436             // to the missed call notification.
    437             if (n.presentation != Calls.PRESENTATION_ALLOWED) {
    438                 n.number = null;
    439             }
    440 
    441             if (DBG) log("NotificationInfo constructed for number: " + n.number);
    442 
    443             return n;
    444         }
    445     }
    446 
    447     /**
    448      * Configures a Notification to emit the blinky green message-waiting/
    449      * missed-call signal.
    450      */
    451     private static void configureLedNotification(Notification note) {
    452         note.flags |= Notification.FLAG_SHOW_LIGHTS;
    453         note.defaults |= Notification.DEFAULT_LIGHTS;
    454     }
    455 
    456     /**
    457      * Displays a notification about a missed call.
    458      *
    459      * @param name the contact name.
    460      * @param number the phone number. Note that this may be a non-callable String like "Unknown",
    461      * or "Private Number", which possibly come from methods like
    462      * {@link PhoneUtils#modifyForSpecialCnapCases(Context, CallerInfo, String, int)}.
    463      * @param type the type of the call. {@link android.provider.CallLog.Calls#INCOMING_TYPE}
    464      * {@link android.provider.CallLog.Calls#OUTGOING_TYPE}, or
    465      * {@link android.provider.CallLog.Calls#MISSED_TYPE}
    466      * @param photo picture which may be used for the notification (when photoIcon is null).
    467      * This also can be null when the picture itself isn't available. If photoIcon is available
    468      * it should be prioritized (because this may be too huge for notification).
    469      * See also {@link ContactsAsyncHelper}.
    470      * @param photoIcon picture which should be used for the notification. Can be null. This is
    471      * the most suitable for {@link android.app.Notification.Builder#setLargeIcon(Bitmap)}, this
    472      * should be used when non-null.
    473      * @param date the time when the missed call happened
    474      */
    475     /* package */ void notifyMissedCall(String name, String number, int presentation, String type,
    476             Drawable photo, Bitmap photoIcon, long date) {
    477 
    478         // When the user clicks this notification, we go to the call log.
    479         final PendingIntent pendingCallLogIntent = PhoneGlobals.createPendingCallLogIntent(
    480                 mContext);
    481 
    482         // Never display the missed call notification on non-voice-capable
    483         // devices, even if the device does somehow manage to get an
    484         // incoming call.
    485         if (!PhoneGlobals.sVoiceCapable) {
    486             if (DBG) log("notifyMissedCall: non-voice-capable device, not posting notification");
    487             return;
    488         }
    489 
    490         if (VDBG) {
    491             log("notifyMissedCall(). name: " + name + ", number: " + number
    492                 + ", label: " + type + ", photo: " + photo + ", photoIcon: " + photoIcon
    493                 + ", date: " + date);
    494         }
    495 
    496         // title resource id
    497         int titleResId;
    498         // the text in the notification's line 1 and 2.
    499         String expandedText, callName;
    500 
    501         // increment number of missed calls.
    502         mNumberMissedCalls++;
    503 
    504         // get the name for the ticker text
    505         // i.e. "Missed call from <caller name or number>"
    506         if (name != null && TextUtils.isGraphic(name)) {
    507             callName = name;
    508         } else if (!TextUtils.isEmpty(number)){
    509             final BidiFormatter bidiFormatter = BidiFormatter.getInstance();
    510             // A number should always be displayed LTR using {@link BidiFormatter}
    511             // regardless of the content of the rest of the notification.
    512             callName = bidiFormatter.unicodeWrap(number, TextDirectionHeuristics.LTR);
    513         } else {
    514             // use "unknown" if the caller is unidentifiable.
    515             callName = mContext.getString(R.string.unknown);
    516         }
    517 
    518         // display the first line of the notification:
    519         // 1 missed call: call name
    520         // more than 1 missed call: <number of calls> + "missed calls"
    521         if (mNumberMissedCalls == 1) {
    522             titleResId = R.string.notification_missedCallTitle;
    523             expandedText = callName;
    524         } else {
    525             titleResId = R.string.notification_missedCallsTitle;
    526             expandedText = mContext.getString(R.string.notification_missedCallsMsg,
    527                     mNumberMissedCalls);
    528         }
    529 
    530         Notification.Builder builder = new Notification.Builder(mContext);
    531         builder.setSmallIcon(android.R.drawable.stat_notify_missed_call)
    532                 .setTicker(mContext.getString(R.string.notification_missedCallTicker, callName))
    533                 .setWhen(date)
    534                 .setContentTitle(mContext.getText(titleResId))
    535                 .setContentText(expandedText)
    536                 .setContentIntent(pendingCallLogIntent)
    537                 .setAutoCancel(true)
    538                 .setDeleteIntent(createClearMissedCallsIntent());
    539 
    540         // Simple workaround for issue 6476275; refrain having actions when the given number seems
    541         // not a real one but a non-number which was embedded by methods outside (like
    542         // PhoneUtils#modifyForSpecialCnapCases()).
    543         // TODO: consider removing equals() checks here, and modify callers of this method instead.
    544         if (mNumberMissedCalls == 1
    545                 && !TextUtils.isEmpty(number)
    546                 && (presentation == PhoneConstants.PRESENTATION_ALLOWED ||
    547                         presentation == PhoneConstants.PRESENTATION_PAYPHONE)) {
    548             if (DBG) log("Add actions with the number " + number);
    549 
    550             builder.addAction(R.drawable.stat_sys_phone_call,
    551                     mContext.getString(R.string.notification_missedCall_call_back),
    552                     PhoneGlobals.getCallBackPendingIntent(mContext, number));
    553 
    554             builder.addAction(R.drawable.ic_text_holo_dark,
    555                     mContext.getString(R.string.notification_missedCall_message),
    556                     PhoneGlobals.getSendSmsFromNotificationPendingIntent(mContext, number));
    557 
    558             if (photoIcon != null) {
    559                 builder.setLargeIcon(photoIcon);
    560             } else if (photo instanceof BitmapDrawable) {
    561                 builder.setLargeIcon(((BitmapDrawable) photo).getBitmap());
    562             }
    563         } else {
    564             if (DBG) {
    565                 log("Suppress actions. number: " + number + ", missedCalls: " + mNumberMissedCalls);
    566             }
    567         }
    568 
    569         Notification notification = builder.getNotification();
    570         configureLedNotification(notification);
    571         mNotificationManager.notify(MISSED_CALL_NOTIFICATION, notification);
    572     }
    573 
    574     /** Returns an intent to be invoked when the missed call notification is cleared. */
    575     private PendingIntent createClearMissedCallsIntent() {
    576         Intent intent = new Intent(mContext, ClearMissedCallsService.class);
    577         intent.setAction(ClearMissedCallsService.ACTION_CLEAR_MISSED_CALLS);
    578         return PendingIntent.getService(mContext, 0, intent, 0);
    579     }
    580 
    581     /**
    582      * Cancels the "missed call" notification.
    583      *
    584      * @see ITelephony.cancelMissedCallsNotification()
    585      */
    586     void cancelMissedCallNotification() {
    587         // reset the number of missed calls to 0.
    588         mNumberMissedCalls = 0;
    589         mNotificationManager.cancel(MISSED_CALL_NOTIFICATION);
    590     }
    591 
    592     private void notifySpeakerphone() {
    593         if (!mShowingSpeakerphoneIcon) {
    594             mStatusBarManager.setIcon("speakerphone", android.R.drawable.stat_sys_speakerphone, 0,
    595                     mContext.getString(R.string.accessibility_speakerphone_enabled));
    596             mShowingSpeakerphoneIcon = true;
    597         }
    598     }
    599 
    600     private void cancelSpeakerphone() {
    601         if (mShowingSpeakerphoneIcon) {
    602             mStatusBarManager.removeIcon("speakerphone");
    603             mShowingSpeakerphoneIcon = false;
    604         }
    605     }
    606 
    607     /**
    608      * Shows or hides the "speakerphone" notification in the status bar,
    609      * based on the actual current state of the speaker.
    610      *
    611      * If you already know the current speaker state (e.g. if you just
    612      * called AudioManager.setSpeakerphoneOn() yourself) then you should
    613      * directly call {@link #updateSpeakerNotification(boolean)} instead.
    614      *
    615      * (But note that the status bar icon is *never* shown while the in-call UI
    616      * is active; it only appears if you bail out to some other activity.)
    617      */
    618     private void updateSpeakerNotification() {
    619         AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
    620         boolean showNotification =
    621                 (mPhone.getState() == PhoneConstants.State.OFFHOOK) && audioManager.isSpeakerphoneOn();
    622 
    623         if (DBG) log(showNotification
    624                      ? "updateSpeakerNotification: speaker ON"
    625                      : "updateSpeakerNotification: speaker OFF (or not offhook)");
    626 
    627         updateSpeakerNotification(showNotification);
    628     }
    629 
    630     /**
    631      * Shows or hides the "speakerphone" notification in the status bar.
    632      *
    633      * @param showNotification if true, call notifySpeakerphone();
    634      *                         if false, call cancelSpeakerphone().
    635      *
    636      * Use {@link updateSpeakerNotification()} to update the status bar
    637      * based on the actual current state of the speaker.
    638      *
    639      * (But note that the status bar icon is *never* shown while the in-call UI
    640      * is active; it only appears if you bail out to some other activity.)
    641      */
    642     public void updateSpeakerNotification(boolean showNotification) {
    643         if (DBG) log("updateSpeakerNotification(" + showNotification + ")...");
    644 
    645         // Regardless of the value of the showNotification param, suppress
    646         // the status bar icon if the the InCallScreen is the foreground
    647         // activity, since the in-call UI already provides an onscreen
    648         // indication of the speaker state.  (This reduces clutter in the
    649         // status bar.)
    650 
    651         if (showNotification) {
    652             notifySpeakerphone();
    653         } else {
    654             cancelSpeakerphone();
    655         }
    656     }
    657 
    658     private void notifyMute() {
    659         if (!mShowingMuteIcon) {
    660             mStatusBarManager.setIcon("mute", android.R.drawable.stat_notify_call_mute, 0,
    661                     mContext.getString(R.string.accessibility_call_muted));
    662             mShowingMuteIcon = true;
    663         }
    664     }
    665 
    666     private void cancelMute() {
    667         if (mShowingMuteIcon) {
    668             mStatusBarManager.removeIcon("mute");
    669             mShowingMuteIcon = false;
    670         }
    671     }
    672 
    673     /**
    674      * Shows or hides the "mute" notification in the status bar,
    675      * based on the current mute state of the Phone.
    676      *
    677      * (But note that the status bar icon is *never* shown while the in-call UI
    678      * is active; it only appears if you bail out to some other activity.)
    679      */
    680     void updateMuteNotification() {
    681         // Suppress the status bar icon if the the InCallScreen is the
    682         // foreground activity, since the in-call UI already provides an
    683         // onscreen indication of the mute state.  (This reduces clutter
    684         // in the status bar.)
    685 
    686         if ((mCM.getState() == PhoneConstants.State.OFFHOOK) && PhoneUtils.getMute()) {
    687             if (DBG) log("updateMuteNotification: MUTED");
    688             notifyMute();
    689         } else {
    690             if (DBG) log("updateMuteNotification: not muted (or not offhook)");
    691             cancelMute();
    692         }
    693     }
    694 
    695     /**
    696      * Completely take down the in-call notification *and* the mute/speaker
    697      * notifications as well, to indicate that the phone is now idle.
    698      */
    699     /* package */ void cancelCallInProgressNotifications() {
    700         if (DBG) log("cancelCallInProgressNotifications");
    701         cancelMute();
    702         cancelSpeakerphone();
    703     }
    704 
    705     /**
    706      * Updates the message waiting indicator (voicemail) notification.
    707      *
    708      * @param visible true if there are messages waiting
    709      */
    710     /* package */ void updateMwi(boolean visible) {
    711         if (DBG) log("updateMwi(): " + visible);
    712 
    713         if (visible) {
    714             int resId = android.R.drawable.stat_notify_voicemail;
    715 
    716             // This Notification can get a lot fancier once we have more
    717             // information about the current voicemail messages.
    718             // (For example, the current voicemail system can't tell
    719             // us the caller-id or timestamp of a message, or tell us the
    720             // message count.)
    721 
    722             // But for now, the UI is ultra-simple: if the MWI indication
    723             // is supposed to be visible, just show a single generic
    724             // notification.
    725 
    726             String notificationTitle = mContext.getString(R.string.notification_voicemail_title);
    727             String vmNumber = mPhone.getVoiceMailNumber();
    728             if (DBG) log("- got vm number: '" + vmNumber + "'");
    729 
    730             // Watch out: vmNumber may be null, for two possible reasons:
    731             //
    732             //   (1) This phone really has no voicemail number
    733             //
    734             //   (2) This phone *does* have a voicemail number, but
    735             //       the SIM isn't ready yet.
    736             //
    737             // Case (2) *does* happen in practice if you have voicemail
    738             // messages when the device first boots: we get an MWI
    739             // notification as soon as we register on the network, but the
    740             // SIM hasn't finished loading yet.
    741             //
    742             // So handle case (2) by retrying the lookup after a short
    743             // delay.
    744 
    745             if ((vmNumber == null) && !mPhone.getIccRecordsLoaded()) {
    746                 if (DBG) log("- Null vm number: SIM records not loaded (yet)...");
    747 
    748                 // TODO: rather than retrying after an arbitrary delay, it
    749                 // would be cleaner to instead just wait for a
    750                 // SIM_RECORDS_LOADED notification.
    751                 // (Unfortunately right now there's no convenient way to
    752                 // get that notification in phone app code.  We'd first
    753                 // want to add a call like registerForSimRecordsLoaded()
    754                 // to Phone.java and GSMPhone.java, and *then* we could
    755                 // listen for that in the CallNotifier class.)
    756 
    757                 // Limit the number of retries (in case the SIM is broken
    758                 // or missing and can *never* load successfully.)
    759                 if (mVmNumberRetriesRemaining-- > 0) {
    760                     if (DBG) log("  - Retrying in " + VM_NUMBER_RETRY_DELAY_MILLIS + " msec...");
    761                     mApp.notifier.sendMwiChangedDelayed(VM_NUMBER_RETRY_DELAY_MILLIS);
    762                     return;
    763                 } else {
    764                     Log.w(LOG_TAG, "NotificationMgr.updateMwi: getVoiceMailNumber() failed after "
    765                           + MAX_VM_NUMBER_RETRIES + " retries; giving up.");
    766                     // ...and continue with vmNumber==null, just as if the
    767                     // phone had no VM number set up in the first place.
    768                 }
    769             }
    770 
    771             if (TelephonyCapabilities.supportsVoiceMessageCount(mPhone)) {
    772                 int vmCount = mPhone.getVoiceMessageCount();
    773                 String titleFormat = mContext.getString(R.string.notification_voicemail_title_count);
    774                 notificationTitle = String.format(titleFormat, vmCount);
    775             }
    776 
    777             String notificationText;
    778             if (TextUtils.isEmpty(vmNumber)) {
    779                 notificationText = mContext.getString(
    780                         R.string.notification_voicemail_no_vm_number);
    781             } else {
    782                 notificationText = String.format(
    783                         mContext.getString(R.string.notification_voicemail_text_format),
    784                         PhoneNumberUtils.formatNumber(vmNumber));
    785             }
    786 
    787             Intent intent = new Intent(Intent.ACTION_CALL,
    788                     Uri.fromParts(Constants.SCHEME_VOICEMAIL, "", null));
    789             PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
    790 
    791             SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
    792             Uri ringtoneUri;
    793             String uriString = prefs.getString(
    794                     CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_RINGTONE_KEY, null);
    795             if (!TextUtils.isEmpty(uriString)) {
    796                 ringtoneUri = Uri.parse(uriString);
    797             } else {
    798                 ringtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI;
    799             }
    800 
    801             Notification.Builder builder = new Notification.Builder(mContext);
    802             builder.setSmallIcon(resId)
    803                     .setWhen(System.currentTimeMillis())
    804                     .setContentTitle(notificationTitle)
    805                     .setContentText(notificationText)
    806                     .setContentIntent(pendingIntent)
    807                     .setSound(ringtoneUri);
    808             Notification notification = builder.getNotification();
    809 
    810             CallFeaturesSetting.migrateVoicemailVibrationSettingsIfNeeded(prefs);
    811             final boolean vibrate = prefs.getBoolean(
    812                     CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_VIBRATE_KEY, false);
    813             if (vibrate) {
    814                 notification.defaults |= Notification.DEFAULT_VIBRATE;
    815             }
    816             notification.flags |= Notification.FLAG_NO_CLEAR;
    817             configureLedNotification(notification);
    818             mNotificationManager.notify(VOICEMAIL_NOTIFICATION, notification);
    819         } else {
    820             mNotificationManager.cancel(VOICEMAIL_NOTIFICATION);
    821         }
    822     }
    823 
    824     /**
    825      * Updates the message call forwarding indicator notification.
    826      *
    827      * @param visible true if there are messages waiting
    828      */
    829     /* package */ void updateCfi(boolean visible) {
    830         if (DBG) log("updateCfi(): " + visible);
    831         if (visible) {
    832             // If Unconditional Call Forwarding (forward all calls) for VOICE
    833             // is enabled, just show a notification.  We'll default to expanded
    834             // view for now, so the there is less confusion about the icon.  If
    835             // it is deemed too weird to have CF indications as expanded views,
    836             // then we'll flip the flag back.
    837 
    838             // TODO: We may want to take a look to see if the notification can
    839             // display the target to forward calls to.  This will require some
    840             // effort though, since there are multiple layers of messages that
    841             // will need to propagate that information.
    842 
    843             Notification notification;
    844             final boolean showExpandedNotification = true;
    845             if (showExpandedNotification) {
    846                 Intent intent = new Intent(Intent.ACTION_MAIN);
    847                 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    848                 intent.setClassName("com.android.phone",
    849                         "com.android.phone.CallFeaturesSetting");
    850 
    851                 notification = new Notification(
    852                         R.drawable.stat_sys_phone_call_forward,  // icon
    853                         null, // tickerText
    854                         0); // The "timestamp" of this notification is meaningless;
    855                             // we only care about whether CFI is currently on or not.
    856                 notification.setLatestEventInfo(
    857                         mContext, // context
    858                         mContext.getString(R.string.labelCF), // expandedTitle
    859                         mContext.getString(R.string.sum_cfu_enabled_indicator), // expandedText
    860                         PendingIntent.getActivity(mContext, 0, intent, 0)); // contentIntent
    861             } else {
    862                 notification = new Notification(
    863                         R.drawable.stat_sys_phone_call_forward,  // icon
    864                         null,  // tickerText
    865                         System.currentTimeMillis()  // when
    866                         );
    867             }
    868 
    869             notification.flags |= Notification.FLAG_ONGOING_EVENT;  // also implies FLAG_NO_CLEAR
    870 
    871             mNotificationManager.notify(
    872                     CALL_FORWARD_NOTIFICATION,
    873                     notification);
    874         } else {
    875             mNotificationManager.cancel(CALL_FORWARD_NOTIFICATION);
    876         }
    877     }
    878 
    879     /**
    880      * Shows the "data disconnected due to roaming" notification, which
    881      * appears when you lose data connectivity because you're roaming and
    882      * you have the "data roaming" feature turned off.
    883      */
    884     /* package */ void showDataDisconnectedRoaming() {
    885         if (DBG) log("showDataDisconnectedRoaming()...");
    886 
    887         // "Mobile network settings" screen / dialog
    888         Intent intent = new Intent(mContext, com.android.phone.MobileNetworkSettings.class);
    889 
    890         final CharSequence contentText = mContext.getText(R.string.roaming_reenable_message);
    891 
    892         final Notification.Builder builder = new Notification.Builder(mContext);
    893         builder.setSmallIcon(android.R.drawable.stat_sys_warning);
    894         builder.setContentTitle(mContext.getText(R.string.roaming));
    895         builder.setContentText(contentText);
    896         builder.setContentIntent(PendingIntent.getActivity(mContext, 0, intent, 0));
    897 
    898         final Notification notif = new Notification.BigTextStyle(builder).bigText(contentText)
    899                 .build();
    900 
    901         mNotificationManager.notify(DATA_DISCONNECTED_ROAMING_NOTIFICATION, notif);
    902     }
    903 
    904     /**
    905      * Turns off the "data disconnected due to roaming" notification.
    906      */
    907     /* package */ void hideDataDisconnectedRoaming() {
    908         if (DBG) log("hideDataDisconnectedRoaming()...");
    909         mNotificationManager.cancel(DATA_DISCONNECTED_ROAMING_NOTIFICATION);
    910     }
    911 
    912     /**
    913      * Display the network selection "no service" notification
    914      * @param operator is the numeric operator number
    915      */
    916     private void showNetworkSelection(String operator) {
    917         if (DBG) log("showNetworkSelection(" + operator + ")...");
    918 
    919         String titleText = mContext.getString(
    920                 R.string.notification_network_selection_title);
    921         String expandedText = mContext.getString(
    922                 R.string.notification_network_selection_text, operator);
    923 
    924         Notification notification = new Notification();
    925         notification.icon = android.R.drawable.stat_sys_warning;
    926         notification.when = 0;
    927         notification.flags = Notification.FLAG_ONGOING_EVENT;
    928         notification.tickerText = null;
    929 
    930         // create the target network operators settings intent
    931         Intent intent = new Intent(Intent.ACTION_MAIN);
    932         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
    933                 Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
    934         // Use NetworkSetting to handle the selection intent
    935         intent.setComponent(new ComponentName("com.android.phone",
    936                 "com.android.phone.NetworkSetting"));
    937         PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0);
    938 
    939         notification.setLatestEventInfo(mContext, titleText, expandedText, pi);
    940 
    941         mNotificationManager.notify(SELECTED_OPERATOR_FAIL_NOTIFICATION, notification);
    942     }
    943 
    944     /**
    945      * Turn off the network selection "no service" notification
    946      */
    947     private void cancelNetworkSelection() {
    948         if (DBG) log("cancelNetworkSelection()...");
    949         mNotificationManager.cancel(SELECTED_OPERATOR_FAIL_NOTIFICATION);
    950     }
    951 
    952     /**
    953      * Update notification about no service of user selected operator
    954      *
    955      * @param serviceState Phone service state
    956      */
    957     void updateNetworkSelection(int serviceState) {
    958         if (TelephonyCapabilities.supportsNetworkSelection(mPhone)) {
    959             // get the shared preference of network_selection.
    960             // empty is auto mode, otherwise it is the operator alpha name
    961             // in case there is no operator name, check the operator numeric
    962             SharedPreferences sp =
    963                     PreferenceManager.getDefaultSharedPreferences(mContext);
    964             String networkSelection =
    965                     sp.getString(PhoneBase.NETWORK_SELECTION_NAME_KEY, "");
    966             if (TextUtils.isEmpty(networkSelection)) {
    967                 networkSelection =
    968                         sp.getString(PhoneBase.NETWORK_SELECTION_KEY, "");
    969             }
    970 
    971             if (DBG) log("updateNetworkSelection()..." + "state = " +
    972                     serviceState + " new network " + networkSelection);
    973 
    974             if (serviceState == ServiceState.STATE_OUT_OF_SERVICE
    975                     && !TextUtils.isEmpty(networkSelection)) {
    976                 if (!mSelectedUnavailableNotify) {
    977                     showNetworkSelection(networkSelection);
    978                     mSelectedUnavailableNotify = true;
    979                 }
    980             } else {
    981                 if (mSelectedUnavailableNotify) {
    982                     cancelNetworkSelection();
    983                     mSelectedUnavailableNotify = false;
    984                 }
    985             }
    986         }
    987     }
    988 
    989     /* package */ void postTransientNotification(int notifyId, CharSequence msg) {
    990         if (mToast != null) {
    991             mToast.cancel();
    992         }
    993 
    994         mToast = Toast.makeText(mContext, msg, Toast.LENGTH_LONG);
    995         mToast.show();
    996     }
    997 
    998     private void log(String msg) {
    999         Log.d(LOG_TAG, msg);
   1000     }
   1001 }
   1002