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.ComponentName;
     24 import android.content.Context;
     25 import android.content.Intent;
     26 import android.content.SharedPreferences;
     27 import android.content.pm.UserInfo;
     28 import android.net.Uri;
     29 import android.os.SystemProperties;
     30 import android.os.UserHandle;
     31 import android.os.UserManager;
     32 import android.preference.PreferenceManager;
     33 import android.provider.ContactsContract.PhoneLookup;
     34 import android.provider.Settings;
     35 import android.telecom.PhoneAccount;
     36 import android.telephony.PhoneNumberUtils;
     37 import android.telephony.ServiceState;
     38 import android.text.TextUtils;
     39 import android.util.Log;
     40 import android.widget.Toast;
     41 
     42 import com.android.internal.telephony.Phone;
     43 import com.android.internal.telephony.PhoneBase;
     44 import com.android.internal.telephony.TelephonyCapabilities;
     45 
     46 import java.util.List;
     47 
     48 /**
     49  * NotificationManager-related utility code for the Phone app.
     50  *
     51  * This is a singleton object which acts as the interface to the
     52  * framework's NotificationManager, and is used to display status bar
     53  * icons and control other status bar-related behavior.
     54  *
     55  * @see PhoneGlobals.notificationMgr
     56  */
     57 public class NotificationMgr {
     58     private static final String LOG_TAG = "NotificationMgr";
     59     private static final boolean DBG =
     60             (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
     61     // Do not check in with VDBG = true, since that may write PII to the system log.
     62     private static final boolean VDBG = false;
     63 
     64     // notification types
     65     static final int MMI_NOTIFICATION = 1;
     66     static final int NETWORK_SELECTION_NOTIFICATION = 2;
     67     static final int VOICEMAIL_NOTIFICATION = 3;
     68     static final int CALL_FORWARD_NOTIFICATION = 4;
     69     static final int DATA_DISCONNECTED_ROAMING_NOTIFICATION = 5;
     70     static final int SELECTED_OPERATOR_FAIL_NOTIFICATION = 6;
     71 
     72     /** The singleton NotificationMgr instance. */
     73     private static NotificationMgr sInstance;
     74 
     75     private PhoneGlobals mApp;
     76     private Phone mPhone;
     77 
     78     private Context mContext;
     79     private NotificationManager mNotificationManager;
     80     private StatusBarManager mStatusBarManager;
     81     private UserManager mUserManager;
     82     private Toast mToast;
     83 
     84     public StatusBarHelper statusBarHelper;
     85 
     86     // used to track the notification of selected network unavailable
     87     private boolean mSelectedUnavailableNotify = false;
     88 
     89     // Retry params for the getVoiceMailNumber() call; see updateMwi().
     90     private static final int MAX_VM_NUMBER_RETRIES = 5;
     91     private static final int VM_NUMBER_RETRY_DELAY_MILLIS = 10000;
     92     private int mVmNumberRetriesRemaining = MAX_VM_NUMBER_RETRIES;
     93 
     94     /**
     95      * Private constructor (this is a singleton).
     96      * @see #init(PhoneGlobals)
     97      */
     98     private NotificationMgr(PhoneGlobals app) {
     99         mApp = app;
    100         mContext = app;
    101         mNotificationManager =
    102                 (NotificationManager) app.getSystemService(Context.NOTIFICATION_SERVICE);
    103         mStatusBarManager =
    104                 (StatusBarManager) app.getSystemService(Context.STATUS_BAR_SERVICE);
    105         mUserManager = (UserManager) app.getSystemService(Context.USER_SERVICE);
    106         mPhone = app.phone;  // TODO: better style to use mCM.getDefaultPhone() everywhere instead
    107         statusBarHelper = new StatusBarHelper();
    108     }
    109 
    110     /**
    111      * Initialize the singleton NotificationMgr instance.
    112      *
    113      * This is only done once, at startup, from PhoneApp.onCreate().
    114      * From then on, the NotificationMgr instance is available via the
    115      * PhoneApp's public "notificationMgr" field, which is why there's no
    116      * getInstance() method here.
    117      */
    118     /* package */ static NotificationMgr init(PhoneGlobals app) {
    119         synchronized (NotificationMgr.class) {
    120             if (sInstance == null) {
    121                 sInstance = new NotificationMgr(app);
    122             } else {
    123                 Log.wtf(LOG_TAG, "init() called multiple times!  sInstance = " + sInstance);
    124             }
    125             return sInstance;
    126         }
    127     }
    128 
    129     /**
    130      * Helper class that's a wrapper around the framework's
    131      * StatusBarManager.disable() API.
    132      *
    133      * This class is used to control features like:
    134      *
    135      *   - Disabling the status bar "notification windowshade"
    136      *     while the in-call UI is up
    137      *
    138      *   - Disabling notification alerts (audible or vibrating)
    139      *     while a phone call is active
    140      *
    141      *   - Disabling navigation via the system bar (the "soft buttons" at
    142      *     the bottom of the screen on devices with no hard buttons)
    143      *
    144      * We control these features through a single point of control to make
    145      * sure that the various StatusBarManager.disable() calls don't
    146      * interfere with each other.
    147      */
    148     public class StatusBarHelper {
    149         // Current desired state of status bar / system bar behavior
    150         private boolean mIsNotificationEnabled = true;
    151         private boolean mIsExpandedViewEnabled = true;
    152         private boolean mIsSystemBarNavigationEnabled = true;
    153 
    154         private StatusBarHelper () {
    155         }
    156 
    157         /**
    158          * Enables or disables auditory / vibrational alerts.
    159          *
    160          * (We disable these any time a voice call is active, regardless
    161          * of whether or not the in-call UI is visible.)
    162          */
    163         public void enableNotificationAlerts(boolean enable) {
    164             if (mIsNotificationEnabled != enable) {
    165                 mIsNotificationEnabled = enable;
    166                 updateStatusBar();
    167             }
    168         }
    169 
    170         /**
    171          * Enables or disables the expanded view of the status bar
    172          * (i.e. the ability to pull down the "notification windowshade").
    173          *
    174          * (This feature is disabled by the InCallScreen while the in-call
    175          * UI is active.)
    176          */
    177         public void enableExpandedView(boolean enable) {
    178             if (mIsExpandedViewEnabled != enable) {
    179                 mIsExpandedViewEnabled = enable;
    180                 updateStatusBar();
    181             }
    182         }
    183 
    184         /**
    185          * Enables or disables the navigation via the system bar (the
    186          * "soft buttons" at the bottom of the screen)
    187          *
    188          * (This feature is disabled while an incoming call is ringing,
    189          * because it's easy to accidentally touch the system bar while
    190          * pulling the phone out of your pocket.)
    191          */
    192         public void enableSystemBarNavigation(boolean enable) {
    193             if (mIsSystemBarNavigationEnabled != enable) {
    194                 mIsSystemBarNavigationEnabled = enable;
    195                 updateStatusBar();
    196             }
    197         }
    198 
    199         /**
    200          * Updates the status bar to reflect the current desired state.
    201          */
    202         private void updateStatusBar() {
    203             int state = StatusBarManager.DISABLE_NONE;
    204 
    205             if (!mIsExpandedViewEnabled) {
    206                 state |= StatusBarManager.DISABLE_EXPAND;
    207             }
    208             if (!mIsNotificationEnabled) {
    209                 state |= StatusBarManager.DISABLE_NOTIFICATION_ALERTS;
    210             }
    211             if (!mIsSystemBarNavigationEnabled) {
    212                 // Disable *all* possible navigation via the system bar.
    213                 state |= StatusBarManager.DISABLE_HOME;
    214                 state |= StatusBarManager.DISABLE_RECENT;
    215                 state |= StatusBarManager.DISABLE_BACK;
    216                 state |= StatusBarManager.DISABLE_SEARCH;
    217             }
    218 
    219             if (DBG) log("updateStatusBar: state = 0x" + Integer.toHexString(state));
    220             mStatusBarManager.disable(state);
    221         }
    222     }
    223 
    224     /** The projection to use when querying the phones table */
    225     static final String[] PHONES_PROJECTION = new String[] {
    226         PhoneLookup.NUMBER,
    227         PhoneLookup.DISPLAY_NAME,
    228         PhoneLookup._ID
    229     };
    230 
    231     /**
    232      * Updates the message waiting indicator (voicemail) notification.
    233      *
    234      * @param visible true if there are messages waiting
    235      */
    236     /* package */ void updateMwi(boolean visible) {
    237         if (DBG) log("updateMwi(): " + visible);
    238 
    239         if (visible) {
    240             int resId = android.R.drawable.stat_notify_voicemail;
    241 
    242             // This Notification can get a lot fancier once we have more
    243             // information about the current voicemail messages.
    244             // (For example, the current voicemail system can't tell
    245             // us the caller-id or timestamp of a message, or tell us the
    246             // message count.)
    247 
    248             // But for now, the UI is ultra-simple: if the MWI indication
    249             // is supposed to be visible, just show a single generic
    250             // notification.
    251 
    252             String notificationTitle = mContext.getString(R.string.notification_voicemail_title);
    253             String vmNumber = mPhone.getVoiceMailNumber();
    254             if (DBG) log("- got vm number: '" + vmNumber + "'");
    255 
    256             // Watch out: vmNumber may be null, for two possible reasons:
    257             //
    258             //   (1) This phone really has no voicemail number
    259             //
    260             //   (2) This phone *does* have a voicemail number, but
    261             //       the SIM isn't ready yet.
    262             //
    263             // Case (2) *does* happen in practice if you have voicemail
    264             // messages when the device first boots: we get an MWI
    265             // notification as soon as we register on the network, but the
    266             // SIM hasn't finished loading yet.
    267             //
    268             // So handle case (2) by retrying the lookup after a short
    269             // delay.
    270 
    271             if ((vmNumber == null) && !mPhone.getIccRecordsLoaded()) {
    272                 if (DBG) log("- Null vm number: SIM records not loaded (yet)...");
    273 
    274                 // TODO: rather than retrying after an arbitrary delay, it
    275                 // would be cleaner to instead just wait for a
    276                 // SIM_RECORDS_LOADED notification.
    277                 // (Unfortunately right now there's no convenient way to
    278                 // get that notification in phone app code.  We'd first
    279                 // want to add a call like registerForSimRecordsLoaded()
    280                 // to Phone.java and GSMPhone.java, and *then* we could
    281                 // listen for that in the CallNotifier class.)
    282 
    283                 // Limit the number of retries (in case the SIM is broken
    284                 // or missing and can *never* load successfully.)
    285                 if (mVmNumberRetriesRemaining-- > 0) {
    286                     if (DBG) log("  - Retrying in " + VM_NUMBER_RETRY_DELAY_MILLIS + " msec...");
    287                     mApp.notifier.sendMwiChangedDelayed(VM_NUMBER_RETRY_DELAY_MILLIS);
    288                     return;
    289                 } else {
    290                     Log.w(LOG_TAG, "NotificationMgr.updateMwi: getVoiceMailNumber() failed after "
    291                           + MAX_VM_NUMBER_RETRIES + " retries; giving up.");
    292                     // ...and continue with vmNumber==null, just as if the
    293                     // phone had no VM number set up in the first place.
    294                 }
    295             }
    296 
    297             if (TelephonyCapabilities.supportsVoiceMessageCount(mPhone)) {
    298                 int vmCount = mPhone.getVoiceMessageCount();
    299                 String titleFormat = mContext.getString(R.string.notification_voicemail_title_count);
    300                 notificationTitle = String.format(titleFormat, vmCount);
    301             }
    302 
    303             String notificationText;
    304             if (TextUtils.isEmpty(vmNumber)) {
    305                 notificationText = mContext.getString(
    306                         R.string.notification_voicemail_no_vm_number);
    307             } else {
    308                 notificationText = String.format(
    309                         mContext.getString(R.string.notification_voicemail_text_format),
    310                         PhoneNumberUtils.formatNumber(vmNumber));
    311             }
    312 
    313             Intent intent = new Intent(Intent.ACTION_CALL,
    314                     Uri.fromParts(PhoneAccount.SCHEME_VOICEMAIL, "", null));
    315             PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
    316 
    317             SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
    318             Uri ringtoneUri;
    319             String uriString = prefs.getString(
    320                     CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_RINGTONE_KEY, null);
    321             if (!TextUtils.isEmpty(uriString)) {
    322                 ringtoneUri = Uri.parse(uriString);
    323             } else {
    324                 ringtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI;
    325             }
    326 
    327             Notification.Builder builder = new Notification.Builder(mContext);
    328             builder.setSmallIcon(resId)
    329                     .setWhen(System.currentTimeMillis())
    330                     .setContentTitle(notificationTitle)
    331                     .setContentText(notificationText)
    332                     .setContentIntent(pendingIntent)
    333                     .setSound(ringtoneUri)
    334                     .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
    335                     .setOngoing(true);
    336 
    337             CallFeaturesSetting.migrateVoicemailVibrationSettingsIfNeeded(prefs);
    338             final boolean vibrate = prefs.getBoolean(
    339                     CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_VIBRATE_KEY, false);
    340             if (vibrate) {
    341                 builder.setDefaults(Notification.DEFAULT_VIBRATE);
    342             }
    343 
    344             final Notification notification = builder.build();
    345             List<UserInfo> users = mUserManager.getUsers(true);
    346             for (int i = 0; i < users.size(); i++) {
    347                 final UserInfo user = users.get(i);
    348                 final UserHandle userHandle = user.getUserHandle();
    349                 if (!mUserManager.hasUserRestriction(
    350                         UserManager.DISALLOW_OUTGOING_CALLS, userHandle)
    351                             && !user.isManagedProfile()) {
    352                     mNotificationManager.notifyAsUser(
    353                             null /* tag */, VOICEMAIL_NOTIFICATION, notification, userHandle);
    354                 }
    355             }
    356         } else {
    357             mNotificationManager.cancelAsUser(
    358                     null /* tag */, VOICEMAIL_NOTIFICATION, UserHandle.ALL);
    359         }
    360     }
    361 
    362     /**
    363      * Updates the message call forwarding indicator notification.
    364      *
    365      * @param visible true if there are messages waiting
    366      */
    367     /* package */ void updateCfi(boolean visible) {
    368         if (DBG) log("updateCfi(): " + visible);
    369         if (visible) {
    370             // If Unconditional Call Forwarding (forward all calls) for VOICE
    371             // is enabled, just show a notification.  We'll default to expanded
    372             // view for now, so the there is less confusion about the icon.  If
    373             // it is deemed too weird to have CF indications as expanded views,
    374             // then we'll flip the flag back.
    375 
    376             // TODO: We may want to take a look to see if the notification can
    377             // display the target to forward calls to.  This will require some
    378             // effort though, since there are multiple layers of messages that
    379             // will need to propagate that information.
    380 
    381             Notification.Builder builder = new Notification.Builder(mContext)
    382                     .setSmallIcon(R.drawable.stat_sys_phone_call_forward)
    383                     .setContentTitle(mContext.getString(R.string.labelCF))
    384                     .setContentText(mContext.getString(R.string.sum_cfu_enabled_indicator))
    385                     .setShowWhen(false)
    386                     .setOngoing(true);
    387 
    388             Intent intent = new Intent(Intent.ACTION_MAIN);
    389             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    390             intent.setClassName("com.android.phone", "com.android.phone.CallFeaturesSetting");
    391             PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
    392 
    393             List<UserInfo> users = mUserManager.getUsers(true);
    394             for (int i = 0; i < users.size(); i++) {
    395                 UserHandle userHandle = users.get(i).getUserHandle();
    396                 builder.setContentIntent(userHandle.isOwner() ? contentIntent : null);
    397                     mNotificationManager.notifyAsUser(
    398                             null /* tag */, CALL_FORWARD_NOTIFICATION, builder.build(), userHandle);
    399             }
    400         } else {
    401             mNotificationManager.cancelAsUser(
    402                     null /* tag */, CALL_FORWARD_NOTIFICATION, UserHandle.ALL);
    403         }
    404     }
    405 
    406     /**
    407      * Shows the "data disconnected due to roaming" notification, which
    408      * appears when you lose data connectivity because you're roaming and
    409      * you have the "data roaming" feature turned off.
    410      */
    411     /* package */ void showDataDisconnectedRoaming() {
    412         if (DBG) log("showDataDisconnectedRoaming()...");
    413 
    414         // "Mobile network settings" screen / dialog
    415         Intent intent = new Intent(mContext, com.android.phone.MobileNetworkSettings.class);
    416         PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
    417 
    418         final CharSequence contentText = mContext.getText(R.string.roaming_reenable_message);
    419 
    420         final Notification.Builder builder = new Notification.Builder(mContext)
    421                 .setSmallIcon(android.R.drawable.stat_sys_warning)
    422                 .setContentTitle(mContext.getText(R.string.roaming))
    423                 .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
    424                 .setContentText(contentText);
    425 
    426         List<UserInfo> users = mUserManager.getUsers(true);
    427         for (int i = 0; i < users.size(); i++) {
    428             UserHandle userHandle = users.get(i).getUserHandle();
    429             builder.setContentIntent(userHandle.isOwner() ? contentIntent : null);
    430             final Notification notif =
    431                     new Notification.BigTextStyle(builder).bigText(contentText).build();
    432             mNotificationManager.notifyAsUser(
    433                     null /* tag */, DATA_DISCONNECTED_ROAMING_NOTIFICATION, notif, userHandle);
    434         }
    435     }
    436 
    437     /**
    438      * Turns off the "data disconnected due to roaming" notification.
    439      */
    440     /* package */ void hideDataDisconnectedRoaming() {
    441         if (DBG) log("hideDataDisconnectedRoaming()...");
    442         mNotificationManager.cancel(DATA_DISCONNECTED_ROAMING_NOTIFICATION);
    443     }
    444 
    445     /**
    446      * Display the network selection "no service" notification
    447      * @param operator is the numeric operator number
    448      */
    449     private void showNetworkSelection(String operator) {
    450         if (DBG) log("showNetworkSelection(" + operator + ")...");
    451 
    452         Notification.Builder builder = new Notification.Builder(mContext)
    453                 .setSmallIcon(android.R.drawable.stat_sys_warning)
    454                 .setContentTitle(mContext.getString(R.string.notification_network_selection_title))
    455                 .setContentText(
    456                         mContext.getString(R.string.notification_network_selection_text, operator))
    457                 .setShowWhen(false)
    458                 .setOngoing(true);
    459 
    460         // create the target network operators settings intent
    461         Intent intent = new Intent(Intent.ACTION_MAIN);
    462         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
    463                 Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
    464         // Use NetworkSetting to handle the selection intent
    465         intent.setComponent(new ComponentName("com.android.phone",
    466                 "com.android.phone.NetworkSetting"));
    467         PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
    468 
    469         List<UserInfo> users = mUserManager.getUsers(true);
    470         for (int i = 0; i < users.size(); i++) {
    471             UserHandle userHandle = users.get(i).getUserHandle();
    472             builder.setContentIntent(userHandle.isOwner() ? contentIntent : null);
    473             mNotificationManager.notifyAsUser(
    474                     null /* tag */,
    475                     SELECTED_OPERATOR_FAIL_NOTIFICATION,
    476                     builder.build(),
    477                     userHandle);
    478         }
    479     }
    480 
    481     /**
    482      * Turn off the network selection "no service" notification
    483      */
    484     private void cancelNetworkSelection() {
    485         if (DBG) log("cancelNetworkSelection()...");
    486         mNotificationManager.cancelAsUser(
    487                 null /* tag */, SELECTED_OPERATOR_FAIL_NOTIFICATION, UserHandle.ALL);
    488     }
    489 
    490     /**
    491      * Update notification about no service of user selected operator
    492      *
    493      * @param serviceState Phone service state
    494      */
    495     void updateNetworkSelection(int serviceState) {
    496         if (TelephonyCapabilities.supportsNetworkSelection(mPhone)) {
    497             // get the shared preference of network_selection.
    498             // empty is auto mode, otherwise it is the operator alpha name
    499             // in case there is no operator name, check the operator numeric
    500             SharedPreferences sp =
    501                     PreferenceManager.getDefaultSharedPreferences(mContext);
    502             String networkSelection =
    503                     sp.getString(PhoneBase.NETWORK_SELECTION_NAME_KEY, "");
    504             if (TextUtils.isEmpty(networkSelection)) {
    505                 networkSelection =
    506                         sp.getString(PhoneBase.NETWORK_SELECTION_KEY, "");
    507             }
    508 
    509             if (DBG) log("updateNetworkSelection()..." + "state = " +
    510                     serviceState + " new network " + networkSelection);
    511 
    512             if (serviceState == ServiceState.STATE_OUT_OF_SERVICE
    513                     && !TextUtils.isEmpty(networkSelection)) {
    514                 if (!mSelectedUnavailableNotify) {
    515                     showNetworkSelection(networkSelection);
    516                     mSelectedUnavailableNotify = true;
    517                 }
    518             } else {
    519                 if (mSelectedUnavailableNotify) {
    520                     cancelNetworkSelection();
    521                     mSelectedUnavailableNotify = false;
    522                 }
    523             }
    524         }
    525     }
    526 
    527     /* package */ void postTransientNotification(int notifyId, CharSequence msg) {
    528         if (mToast != null) {
    529             mToast.cancel();
    530         }
    531 
    532         mToast = Toast.makeText(mContext, msg, Toast.LENGTH_LONG);
    533         mToast.show();
    534     }
    535 
    536     private void log(String msg) {
    537         Log.d(LOG_TAG, msg);
    538     }
    539 }
    540