Home | History | Annotate | Download | only in notification
      1 /*
      2  * Copyright (C) 2014 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.settings.notification;
     18 
     19 import android.content.ContentResolver;
     20 import android.content.Context;
     21 import android.content.pm.PackageManager;
     22 import android.database.ContentObserver;
     23 import android.database.Cursor;
     24 import android.database.sqlite.SQLiteException;
     25 import android.media.AudioManager;
     26 import android.media.RingtoneManager;
     27 import android.net.Uri;
     28 import android.os.AsyncTask;
     29 import android.os.Bundle;
     30 import android.os.Handler;
     31 import android.os.Looper;
     32 import android.os.Message;
     33 import android.os.Vibrator;
     34 import android.preference.Preference;
     35 import android.preference.Preference.OnPreferenceChangeListener;
     36 import android.preference.PreferenceCategory;
     37 import android.preference.SeekBarVolumizer;
     38 import android.preference.TwoStatePreference;
     39 import android.provider.MediaStore;
     40 import android.provider.OpenableColumns;
     41 import android.provider.SearchIndexableResource;
     42 import android.provider.Settings;
     43 import android.util.Log;
     44 
     45 import android.widget.SeekBar;
     46 import com.android.internal.widget.LockPatternUtils;
     47 import com.android.settings.R;
     48 import com.android.settings.SettingsPreferenceFragment;
     49 import com.android.settings.Utils;
     50 import com.android.settings.search.BaseSearchIndexProvider;
     51 import com.android.settings.search.Indexable;
     52 
     53 import java.util.ArrayList;
     54 import java.util.Arrays;
     55 import java.util.List;
     56 
     57 public class NotificationSettings extends SettingsPreferenceFragment implements Indexable {
     58     private static final String TAG = "NotificationSettings";
     59 
     60     private static final String KEY_SOUND = "sound";
     61     private static final String KEY_MEDIA_VOLUME = "media_volume";
     62     private static final String KEY_ALARM_VOLUME = "alarm_volume";
     63     private static final String KEY_RING_VOLUME = "ring_volume";
     64     private static final String KEY_NOTIFICATION_VOLUME = "notification_volume";
     65     private static final String KEY_PHONE_RINGTONE = "ringtone";
     66     private static final String KEY_NOTIFICATION_RINGTONE = "notification_ringtone";
     67     private static final String KEY_VIBRATE_WHEN_RINGING = "vibrate_when_ringing";
     68     private static final String KEY_NOTIFICATION = "notification";
     69     private static final String KEY_NOTIFICATION_PULSE = "notification_pulse";
     70     private static final String KEY_LOCK_SCREEN_NOTIFICATIONS = "lock_screen_notifications";
     71     private static final String KEY_NOTIFICATION_ACCESS = "manage_notification_access";
     72 
     73     private static final int SAMPLE_CUTOFF = 2000;  // manually cap sample playback at 2 seconds
     74 
     75     private final VolumePreferenceCallback mVolumeCallback = new VolumePreferenceCallback();
     76     private final H mHandler = new H();
     77     private final SettingsObserver mSettingsObserver = new SettingsObserver();
     78 
     79     private Context mContext;
     80     private PackageManager mPM;
     81     private boolean mVoiceCapable;
     82     private Vibrator mVibrator;
     83     private VolumeSeekBarPreference mRingOrNotificationPreference;
     84 
     85     private Preference mPhoneRingtonePreference;
     86     private Preference mNotificationRingtonePreference;
     87     private TwoStatePreference mVibrateWhenRinging;
     88     private TwoStatePreference mNotificationPulse;
     89     private DropDownPreference mLockscreen;
     90     private Preference mNotificationAccess;
     91     private boolean mSecure;
     92     private int mLockscreenSelectedValue;
     93 
     94     @Override
     95     public void onCreate(Bundle savedInstanceState) {
     96         super.onCreate(savedInstanceState);
     97         mContext = getActivity();
     98         mPM = mContext.getPackageManager();
     99         mVoiceCapable = Utils.isVoiceCapable(mContext);
    100         mSecure = new LockPatternUtils(getActivity()).isSecure();
    101 
    102         mVibrator = (Vibrator) getActivity().getSystemService(Context.VIBRATOR_SERVICE);
    103         if (mVibrator != null && !mVibrator.hasVibrator()) {
    104             mVibrator = null;
    105         }
    106 
    107         addPreferencesFromResource(R.xml.notification_settings);
    108 
    109         final PreferenceCategory sound = (PreferenceCategory) findPreference(KEY_SOUND);
    110         initVolumePreference(KEY_MEDIA_VOLUME, AudioManager.STREAM_MUSIC);
    111         initVolumePreference(KEY_ALARM_VOLUME, AudioManager.STREAM_ALARM);
    112         if (mVoiceCapable) {
    113             mRingOrNotificationPreference =
    114                     initVolumePreference(KEY_RING_VOLUME, AudioManager.STREAM_RING);
    115             sound.removePreference(sound.findPreference(KEY_NOTIFICATION_VOLUME));
    116         } else {
    117             mRingOrNotificationPreference =
    118                     initVolumePreference(KEY_NOTIFICATION_VOLUME, AudioManager.STREAM_NOTIFICATION);
    119             sound.removePreference(sound.findPreference(KEY_RING_VOLUME));
    120         }
    121         initRingtones(sound);
    122         initVibrateWhenRinging(sound);
    123 
    124         final PreferenceCategory notification = (PreferenceCategory)
    125                 findPreference(KEY_NOTIFICATION);
    126         initPulse(notification);
    127         initLockscreenNotifications(notification);
    128 
    129         mNotificationAccess = findPreference(KEY_NOTIFICATION_ACCESS);
    130         refreshNotificationListeners();
    131     }
    132 
    133     @Override
    134     public void onResume() {
    135         super.onResume();
    136         refreshNotificationListeners();
    137         lookupRingtoneNames();
    138         mSettingsObserver.register(true);
    139     }
    140 
    141     @Override
    142     public void onPause() {
    143         super.onPause();
    144         mVolumeCallback.stopSample();
    145         mSettingsObserver.register(false);
    146     }
    147 
    148     // === Volumes ===
    149 
    150     private VolumeSeekBarPreference initVolumePreference(String key, int stream) {
    151         final VolumeSeekBarPreference volumePref = (VolumeSeekBarPreference) findPreference(key);
    152         volumePref.setCallback(mVolumeCallback);
    153         volumePref.setStream(stream);
    154         return volumePref;
    155     }
    156 
    157     private void updateRingOrNotificationIcon(int progress) {
    158         mRingOrNotificationPreference.showIcon(progress > 0
    159                     ? R.drawable.ring_notif
    160                     : (mVibrator == null
    161                             ? R.drawable.ring_notif_mute
    162                             : R.drawable.ring_notif_vibrate));
    163     }
    164 
    165     private final class VolumePreferenceCallback implements VolumeSeekBarPreference.Callback {
    166         private SeekBarVolumizer mCurrent;
    167 
    168         @Override
    169         public void onSampleStarting(SeekBarVolumizer sbv) {
    170             if (mCurrent != null && mCurrent != sbv) {
    171                 mCurrent.stopSample();
    172             }
    173             mCurrent = sbv;
    174             if (mCurrent != null) {
    175                 mHandler.removeMessages(H.STOP_SAMPLE);
    176                 mHandler.sendEmptyMessageDelayed(H.STOP_SAMPLE, SAMPLE_CUTOFF);
    177             }
    178         }
    179 
    180         @Override
    181         public void onStreamValueChanged(int stream, int progress) {
    182             if (stream == AudioManager.STREAM_RING) {
    183                 mHandler.removeMessages(H.UPDATE_RINGER_ICON);
    184                 mHandler.obtainMessage(H.UPDATE_RINGER_ICON, progress, 0).sendToTarget();
    185             }
    186         }
    187 
    188         public void stopSample() {
    189             if (mCurrent != null) {
    190                 mCurrent.stopSample();
    191             }
    192         }
    193     };
    194 
    195 
    196     // === Phone & notification ringtone ===
    197 
    198     private void initRingtones(PreferenceCategory root) {
    199         mPhoneRingtonePreference = root.findPreference(KEY_PHONE_RINGTONE);
    200         if (mPhoneRingtonePreference != null && !mVoiceCapable) {
    201             root.removePreference(mPhoneRingtonePreference);
    202             mPhoneRingtonePreference = null;
    203         }
    204         mNotificationRingtonePreference = root.findPreference(KEY_NOTIFICATION_RINGTONE);
    205     }
    206 
    207     private void lookupRingtoneNames() {
    208         AsyncTask.execute(mLookupRingtoneNames);
    209     }
    210 
    211     private final Runnable mLookupRingtoneNames = new Runnable() {
    212         @Override
    213         public void run() {
    214             if (mPhoneRingtonePreference != null) {
    215                 final CharSequence summary = updateRingtoneName(
    216                         mContext, RingtoneManager.TYPE_RINGTONE);
    217                 if (summary != null) {
    218                     mHandler.obtainMessage(H.UPDATE_PHONE_RINGTONE, summary).sendToTarget();
    219                 }
    220             }
    221             if (mNotificationRingtonePreference != null) {
    222                 final CharSequence summary = updateRingtoneName(
    223                         mContext, RingtoneManager.TYPE_NOTIFICATION);
    224                 if (summary != null) {
    225                     mHandler.obtainMessage(H.UPDATE_NOTIFICATION_RINGTONE, summary).sendToTarget();
    226                 }
    227             }
    228         }
    229     };
    230 
    231     private static CharSequence updateRingtoneName(Context context, int type) {
    232         if (context == null) {
    233             Log.e(TAG, "Unable to update ringtone name, no context provided");
    234             return null;
    235         }
    236         Uri ringtoneUri = RingtoneManager.getActualDefaultRingtoneUri(context, type);
    237         CharSequence summary = context.getString(com.android.internal.R.string.ringtone_unknown);
    238         // Is it a silent ringtone?
    239         if (ringtoneUri == null) {
    240             summary = context.getString(com.android.internal.R.string.ringtone_silent);
    241         } else {
    242             Cursor cursor = null;
    243             try {
    244                 if (MediaStore.AUTHORITY.equals(ringtoneUri.getAuthority())) {
    245                     // Fetch the ringtone title from the media provider
    246                     cursor = context.getContentResolver().query(ringtoneUri,
    247                             new String[] { MediaStore.Audio.Media.TITLE }, null, null, null);
    248                 } else if (ContentResolver.SCHEME_CONTENT.equals(ringtoneUri.getScheme())) {
    249                     cursor = context.getContentResolver().query(ringtoneUri,
    250                             new String[] { OpenableColumns.DISPLAY_NAME }, null, null, null);
    251                 }
    252                 if (cursor != null) {
    253                     if (cursor.moveToFirst()) {
    254                         summary = cursor.getString(0);
    255                     }
    256                 }
    257             } catch (SQLiteException sqle) {
    258                 // Unknown title for the ringtone
    259             } catch (IllegalArgumentException iae) {
    260                 // Some other error retrieving the column from the provider
    261             } finally {
    262                 if (cursor != null) {
    263                     cursor.close();
    264                 }
    265             }
    266         }
    267         return summary;
    268     }
    269 
    270     // === Vibrate when ringing ===
    271 
    272     private void initVibrateWhenRinging(PreferenceCategory root) {
    273         mVibrateWhenRinging = (TwoStatePreference) root.findPreference(KEY_VIBRATE_WHEN_RINGING);
    274         if (mVibrateWhenRinging == null) {
    275             Log.i(TAG, "Preference not found: " + KEY_VIBRATE_WHEN_RINGING);
    276             return;
    277         }
    278         if (!mVoiceCapable) {
    279             root.removePreference(mVibrateWhenRinging);
    280             mVibrateWhenRinging = null;
    281             return;
    282         }
    283         mVibrateWhenRinging.setPersistent(false);
    284         updateVibrateWhenRinging();
    285         mVibrateWhenRinging.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
    286             @Override
    287             public boolean onPreferenceChange(Preference preference, Object newValue) {
    288                 final boolean val = (Boolean) newValue;
    289                 return Settings.System.putInt(getContentResolver(),
    290                         Settings.System.VIBRATE_WHEN_RINGING,
    291                         val ? 1 : 0);
    292             }
    293         });
    294     }
    295 
    296     private void updateVibrateWhenRinging() {
    297         if (mVibrateWhenRinging == null) return;
    298         mVibrateWhenRinging.setChecked(Settings.System.getInt(getContentResolver(),
    299                 Settings.System.VIBRATE_WHEN_RINGING, 0) != 0);
    300     }
    301 
    302     // === Pulse notification light ===
    303 
    304     private void initPulse(PreferenceCategory parent) {
    305         mNotificationPulse = (TwoStatePreference) parent.findPreference(KEY_NOTIFICATION_PULSE);
    306         if (mNotificationPulse == null) {
    307             Log.i(TAG, "Preference not found: " + KEY_NOTIFICATION_PULSE);
    308             return;
    309         }
    310         if (!getResources()
    311                 .getBoolean(com.android.internal.R.bool.config_intrusiveNotificationLed)) {
    312             parent.removePreference(mNotificationPulse);
    313         } else {
    314             updatePulse();
    315             mNotificationPulse.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
    316                 @Override
    317                 public boolean onPreferenceChange(Preference preference, Object newValue) {
    318                     final boolean val = (Boolean)newValue;
    319                     return Settings.System.putInt(getContentResolver(),
    320                             Settings.System.NOTIFICATION_LIGHT_PULSE,
    321                             val ? 1 : 0);
    322                 }
    323             });
    324         }
    325     }
    326 
    327     private void updatePulse() {
    328         if (mNotificationPulse == null) {
    329             return;
    330         }
    331         try {
    332             mNotificationPulse.setChecked(Settings.System.getInt(getContentResolver(),
    333                     Settings.System.NOTIFICATION_LIGHT_PULSE) == 1);
    334         } catch (Settings.SettingNotFoundException snfe) {
    335             Log.e(TAG, Settings.System.NOTIFICATION_LIGHT_PULSE + " not found");
    336         }
    337     }
    338 
    339     // === Lockscreen (public / private) notifications ===
    340 
    341     private void initLockscreenNotifications(PreferenceCategory parent) {
    342         mLockscreen = (DropDownPreference) parent.findPreference(KEY_LOCK_SCREEN_NOTIFICATIONS);
    343         if (mLockscreen == null) {
    344             Log.i(TAG, "Preference not found: " + KEY_LOCK_SCREEN_NOTIFICATIONS);
    345             return;
    346         }
    347 
    348         mLockscreen.addItem(R.string.lock_screen_notifications_summary_show,
    349                 R.string.lock_screen_notifications_summary_show);
    350         if (mSecure) {
    351             mLockscreen.addItem(R.string.lock_screen_notifications_summary_hide,
    352                     R.string.lock_screen_notifications_summary_hide);
    353         }
    354         mLockscreen.addItem(R.string.lock_screen_notifications_summary_disable,
    355                 R.string.lock_screen_notifications_summary_disable);
    356         updateLockscreenNotifications();
    357         mLockscreen.setCallback(new DropDownPreference.Callback() {
    358             @Override
    359             public boolean onItemSelected(int pos, Object value) {
    360                 final int val = (Integer) value;
    361                 if (val == mLockscreenSelectedValue) {
    362                     return true;
    363                 }
    364                 final boolean enabled = val != R.string.lock_screen_notifications_summary_disable;
    365                 final boolean show = val == R.string.lock_screen_notifications_summary_show;
    366                 Settings.Secure.putInt(getContentResolver(),
    367                         Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, show ? 1 : 0);
    368                 Settings.Secure.putInt(getContentResolver(),
    369                         Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, enabled ? 1 : 0);
    370                 mLockscreenSelectedValue = val;
    371                 return true;
    372             }
    373         });
    374     }
    375 
    376     private void updateLockscreenNotifications() {
    377         if (mLockscreen == null) {
    378             return;
    379         }
    380         final boolean enabled = getLockscreenNotificationsEnabled();
    381         final boolean allowPrivate = !mSecure || getLockscreenAllowPrivateNotifications();
    382         mLockscreenSelectedValue = !enabled ? R.string.lock_screen_notifications_summary_disable :
    383                 allowPrivate ? R.string.lock_screen_notifications_summary_show :
    384                 R.string.lock_screen_notifications_summary_hide;
    385         mLockscreen.setSelectedValue(mLockscreenSelectedValue);
    386     }
    387 
    388     private boolean getLockscreenNotificationsEnabled() {
    389         return Settings.Secure.getInt(getContentResolver(),
    390                 Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, 0) != 0;
    391     }
    392 
    393     private boolean getLockscreenAllowPrivateNotifications() {
    394         return Settings.Secure.getInt(getContentResolver(),
    395                 Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS, 0) != 0;
    396     }
    397 
    398     // === Notification listeners ===
    399 
    400     private void refreshNotificationListeners() {
    401         if (mNotificationAccess != null) {
    402             final int total = NotificationAccessSettings.getListenersCount(mPM);
    403             if (total == 0) {
    404                 getPreferenceScreen().removePreference(mNotificationAccess);
    405             } else {
    406                 final int n = NotificationAccessSettings.getEnabledListenersCount(mContext);
    407                 if (n == 0) {
    408                     mNotificationAccess.setSummary(getResources().getString(
    409                             R.string.manage_notification_access_summary_zero));
    410                 } else {
    411                     mNotificationAccess.setSummary(String.format(getResources().getQuantityString(
    412                             R.plurals.manage_notification_access_summary_nonzero,
    413                             n, n)));
    414                 }
    415             }
    416         }
    417     }
    418 
    419     // === Callbacks ===
    420 
    421     private final class SettingsObserver extends ContentObserver {
    422         private final Uri VIBRATE_WHEN_RINGING_URI =
    423                 Settings.System.getUriFor(Settings.System.VIBRATE_WHEN_RINGING);
    424         private final Uri NOTIFICATION_LIGHT_PULSE_URI =
    425                 Settings.System.getUriFor(Settings.System.NOTIFICATION_LIGHT_PULSE);
    426         private final Uri LOCK_SCREEN_PRIVATE_URI =
    427                 Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
    428         private final Uri LOCK_SCREEN_SHOW_URI =
    429                 Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS);
    430 
    431         public SettingsObserver() {
    432             super(mHandler);
    433         }
    434 
    435         public void register(boolean register) {
    436             final ContentResolver cr = getContentResolver();
    437             if (register) {
    438                 cr.registerContentObserver(VIBRATE_WHEN_RINGING_URI, false, this);
    439                 cr.registerContentObserver(NOTIFICATION_LIGHT_PULSE_URI, false, this);
    440                 cr.registerContentObserver(LOCK_SCREEN_PRIVATE_URI, false, this);
    441                 cr.registerContentObserver(LOCK_SCREEN_SHOW_URI, false, this);
    442             } else {
    443                 cr.unregisterContentObserver(this);
    444             }
    445         }
    446 
    447         @Override
    448         public void onChange(boolean selfChange, Uri uri) {
    449             super.onChange(selfChange, uri);
    450             if (VIBRATE_WHEN_RINGING_URI.equals(uri)) {
    451                 updateVibrateWhenRinging();
    452             }
    453             if (NOTIFICATION_LIGHT_PULSE_URI.equals(uri)) {
    454                 updatePulse();
    455             }
    456             if (LOCK_SCREEN_PRIVATE_URI.equals(uri) || LOCK_SCREEN_SHOW_URI.equals(uri)) {
    457                 updateLockscreenNotifications();
    458             }
    459         }
    460     }
    461 
    462     private final class H extends Handler {
    463         private static final int UPDATE_PHONE_RINGTONE = 1;
    464         private static final int UPDATE_NOTIFICATION_RINGTONE = 2;
    465         private static final int STOP_SAMPLE = 3;
    466         private static final int UPDATE_RINGER_ICON = 4;
    467 
    468         private H() {
    469             super(Looper.getMainLooper());
    470         }
    471 
    472         @Override
    473         public void handleMessage(Message msg) {
    474             switch (msg.what) {
    475                 case UPDATE_PHONE_RINGTONE:
    476                     mPhoneRingtonePreference.setSummary((CharSequence) msg.obj);
    477                     break;
    478                 case UPDATE_NOTIFICATION_RINGTONE:
    479                     mNotificationRingtonePreference.setSummary((CharSequence) msg.obj);
    480                     break;
    481                 case STOP_SAMPLE:
    482                     mVolumeCallback.stopSample();
    483                     break;
    484                 case UPDATE_RINGER_ICON:
    485                     updateRingOrNotificationIcon(msg.arg1);
    486                     break;
    487             }
    488         }
    489     }
    490 
    491     // === Indexing ===
    492 
    493     public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
    494             new BaseSearchIndexProvider() {
    495 
    496         public List<SearchIndexableResource> getXmlResourcesToIndex(
    497                 Context context, boolean enabled) {
    498             final SearchIndexableResource sir = new SearchIndexableResource(context);
    499             sir.xmlResId = R.xml.notification_settings;
    500             return Arrays.asList(sir);
    501         }
    502 
    503         public List<String> getNonIndexableKeys(Context context) {
    504             final ArrayList<String> rt = new ArrayList<String>();
    505             if (Utils.isVoiceCapable(context)) {
    506                 rt.add(KEY_NOTIFICATION_VOLUME);
    507             } else {
    508                 rt.add(KEY_RING_VOLUME);
    509                 rt.add(KEY_PHONE_RINGTONE);
    510                 rt.add(KEY_VIBRATE_WHEN_RINGING);
    511             }
    512             return rt;
    513         }
    514     };
    515 }
    516