Home | History | Annotate | Download | only in preference
      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 android.preference;
     18 
     19 import android.app.NotificationManager;
     20 import android.content.BroadcastReceiver;
     21 import android.content.Context;
     22 import android.content.Intent;
     23 import android.content.IntentFilter;
     24 import android.database.ContentObserver;
     25 import android.media.AudioAttributes;
     26 import android.media.AudioManager;
     27 import android.media.Ringtone;
     28 import android.media.RingtoneManager;
     29 import android.net.Uri;
     30 import android.os.Handler;
     31 import android.os.HandlerThread;
     32 import android.os.Message;
     33 import android.preference.VolumePreference.VolumeStore;
     34 import android.provider.Settings;
     35 import android.provider.Settings.Global;
     36 import android.provider.Settings.System;
     37 import android.service.notification.ZenModeConfig;
     38 import android.util.Log;
     39 import android.widget.SeekBar;
     40 import android.widget.SeekBar.OnSeekBarChangeListener;
     41 
     42 import com.android.internal.annotations.GuardedBy;
     43 
     44 /**
     45  * Turns a {@link SeekBar} into a volume control.
     46  * @hide
     47  */
     48 public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callback {
     49     private static final String TAG = "SeekBarVolumizer";
     50 
     51     public interface Callback {
     52         void onSampleStarting(SeekBarVolumizer sbv);
     53         void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch);
     54         void onMuted(boolean muted, boolean zenMuted);
     55     }
     56 
     57     private final Context mContext;
     58     private final H mUiHandler = new H();
     59     private final Callback mCallback;
     60     private final Uri mDefaultUri;
     61     private final AudioManager mAudioManager;
     62     private final NotificationManager mNotificationManager;
     63     private final int mStreamType;
     64     private final int mMaxStreamVolume;
     65     private boolean mAffectedByRingerMode;
     66     private boolean mNotificationOrRing;
     67     private final Receiver mReceiver = new Receiver();
     68 
     69     private Handler mHandler;
     70     private Observer mVolumeObserver;
     71     private int mOriginalStreamVolume;
     72     private int mLastAudibleStreamVolume;
     73     // When the old handler is destroyed and a new one is created, there could be a situation where
     74     // this is accessed at the same time in different handlers. So, access to this field needs to be
     75     // synchronized.
     76     @GuardedBy("this")
     77     private Ringtone mRingtone;
     78     private int mLastProgress = -1;
     79     private boolean mMuted;
     80     private SeekBar mSeekBar;
     81     private int mVolumeBeforeMute = -1;
     82     private int mRingerMode;
     83     private int mZenMode;
     84 
     85     private static final int MSG_SET_STREAM_VOLUME = 0;
     86     private static final int MSG_START_SAMPLE = 1;
     87     private static final int MSG_STOP_SAMPLE = 2;
     88     private static final int MSG_INIT_SAMPLE = 3;
     89     private static final int CHECK_RINGTONE_PLAYBACK_DELAY_MS = 1000;
     90 
     91     private NotificationManager.Policy mNotificationPolicy;
     92     private boolean mAllowAlarms;
     93     private boolean mAllowMedia;
     94     private boolean mAllowRinger;
     95 
     96     public SeekBarVolumizer(Context context, int streamType, Uri defaultUri, Callback callback) {
     97         mContext = context;
     98         mAudioManager = context.getSystemService(AudioManager.class);
     99         mNotificationManager = context.getSystemService(NotificationManager.class);
    100         mNotificationPolicy = mNotificationManager.getNotificationPolicy();
    101         mAllowAlarms = (mNotificationPolicy.priorityCategories & NotificationManager.Policy
    102                 .PRIORITY_CATEGORY_ALARMS) != 0;
    103         mAllowMedia = (mNotificationPolicy.priorityCategories & NotificationManager.Policy
    104                 .PRIORITY_CATEGORY_MEDIA) != 0;
    105         mAllowRinger = !ZenModeConfig.areAllPriorityOnlyNotificationZenSoundsMuted(
    106                 mNotificationPolicy);
    107         mStreamType = streamType;
    108         mAffectedByRingerMode = mAudioManager.isStreamAffectedByRingerMode(mStreamType);
    109         mNotificationOrRing = isNotificationOrRing(mStreamType);
    110         if (mNotificationOrRing) {
    111             mRingerMode = mAudioManager.getRingerModeInternal();
    112         }
    113         mZenMode = mNotificationManager.getZenMode();
    114         mMaxStreamVolume = mAudioManager.getStreamMaxVolume(mStreamType);
    115         mCallback = callback;
    116         mOriginalStreamVolume = mAudioManager.getStreamVolume(mStreamType);
    117         mLastAudibleStreamVolume = mAudioManager.getLastAudibleStreamVolume(mStreamType);
    118         mMuted = mAudioManager.isStreamMute(mStreamType);
    119         if (mCallback != null) {
    120             mCallback.onMuted(mMuted, isZenMuted());
    121         }
    122         if (defaultUri == null) {
    123             if (mStreamType == AudioManager.STREAM_RING) {
    124                 defaultUri = Settings.System.DEFAULT_RINGTONE_URI;
    125             } else if (mStreamType == AudioManager.STREAM_NOTIFICATION) {
    126                 defaultUri = Settings.System.DEFAULT_NOTIFICATION_URI;
    127             } else {
    128                 defaultUri = Settings.System.DEFAULT_ALARM_ALERT_URI;
    129             }
    130         }
    131         mDefaultUri = defaultUri;
    132     }
    133 
    134     private static boolean isNotificationOrRing(int stream) {
    135         return stream == AudioManager.STREAM_RING || stream == AudioManager.STREAM_NOTIFICATION;
    136     }
    137 
    138     private static boolean isAlarmsStream(int stream) {
    139         return stream == AudioManager.STREAM_ALARM;
    140     }
    141 
    142     private static boolean isMediaStream(int stream) {
    143         return stream == AudioManager.STREAM_MUSIC;
    144     }
    145 
    146     public void setSeekBar(SeekBar seekBar) {
    147         if (mSeekBar != null) {
    148             mSeekBar.setOnSeekBarChangeListener(null);
    149         }
    150         mSeekBar = seekBar;
    151         mSeekBar.setOnSeekBarChangeListener(null);
    152         mSeekBar.setMax(mMaxStreamVolume);
    153         updateSeekBar();
    154         mSeekBar.setOnSeekBarChangeListener(this);
    155     }
    156 
    157     private boolean isZenMuted() {
    158         return mNotificationOrRing && mZenMode == Global.ZEN_MODE_ALARMS
    159                 || mZenMode == Global.ZEN_MODE_NO_INTERRUPTIONS
    160                 || (mZenMode == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
    161                     && ((!mAllowAlarms && isAlarmsStream(mStreamType))
    162                         || (!mAllowMedia && isMediaStream(mStreamType))
    163                         || (!mAllowRinger && isNotificationOrRing(mStreamType))));
    164     }
    165 
    166     protected void updateSeekBar() {
    167         final boolean zenMuted = isZenMuted();
    168         mSeekBar.setEnabled(!zenMuted);
    169         if (zenMuted) {
    170             mSeekBar.setProgress(mLastAudibleStreamVolume, true);
    171         } else if (mNotificationOrRing && mRingerMode == AudioManager.RINGER_MODE_VIBRATE) {
    172             mSeekBar.setProgress(0, true);
    173         } else if (mMuted) {
    174             mSeekBar.setProgress(0, true);
    175         } else {
    176             mSeekBar.setProgress(mLastProgress > -1 ? mLastProgress : mOriginalStreamVolume, true);
    177         }
    178     }
    179 
    180     @Override
    181     public boolean handleMessage(Message msg) {
    182         switch (msg.what) {
    183             case MSG_SET_STREAM_VOLUME:
    184                 if (mMuted && mLastProgress > 0) {
    185                     mAudioManager.adjustStreamVolume(mStreamType, AudioManager.ADJUST_UNMUTE, 0);
    186                 } else if (!mMuted && mLastProgress == 0) {
    187                     mAudioManager.adjustStreamVolume(mStreamType, AudioManager.ADJUST_MUTE, 0);
    188                 }
    189                 mAudioManager.setStreamVolume(mStreamType, mLastProgress,
    190                         AudioManager.FLAG_SHOW_UI_WARNINGS);
    191                 break;
    192             case MSG_START_SAMPLE:
    193                 onStartSample();
    194                 break;
    195             case MSG_STOP_SAMPLE:
    196                 onStopSample();
    197                 break;
    198             case MSG_INIT_SAMPLE:
    199                 onInitSample();
    200                 break;
    201             default:
    202                 Log.e(TAG, "invalid SeekBarVolumizer message: "+msg.what);
    203         }
    204         return true;
    205     }
    206 
    207     private void onInitSample() {
    208         synchronized (this) {
    209             mRingtone = RingtoneManager.getRingtone(mContext, mDefaultUri);
    210             if (mRingtone != null) {
    211                 mRingtone.setStreamType(mStreamType);
    212             }
    213         }
    214     }
    215 
    216     private void postStartSample() {
    217         if (mHandler == null) return;
    218         mHandler.removeMessages(MSG_START_SAMPLE);
    219         mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_START_SAMPLE),
    220                 isSamplePlaying() ? CHECK_RINGTONE_PLAYBACK_DELAY_MS : 0);
    221     }
    222 
    223     private void onStartSample() {
    224         if (!isSamplePlaying()) {
    225             if (mCallback != null) {
    226                 mCallback.onSampleStarting(this);
    227             }
    228 
    229             synchronized (this) {
    230                 if (mRingtone != null) {
    231                     try {
    232                         mRingtone.setAudioAttributes(new AudioAttributes.Builder(mRingtone
    233                                 .getAudioAttributes())
    234                                 .setFlags(AudioAttributes.FLAG_BYPASS_MUTE)
    235                                 .build());
    236                         mRingtone.play();
    237                     } catch (Throwable e) {
    238                         Log.w(TAG, "Error playing ringtone, stream " + mStreamType, e);
    239                     }
    240                 }
    241             }
    242         }
    243     }
    244 
    245     private void postStopSample() {
    246         if (mHandler == null) return;
    247         // remove pending delayed start messages
    248         mHandler.removeMessages(MSG_START_SAMPLE);
    249         mHandler.removeMessages(MSG_STOP_SAMPLE);
    250         mHandler.sendMessage(mHandler.obtainMessage(MSG_STOP_SAMPLE));
    251     }
    252 
    253     private void onStopSample() {
    254         synchronized (this) {
    255             if (mRingtone != null) {
    256                 mRingtone.stop();
    257             }
    258         }
    259     }
    260 
    261     public void stop() {
    262         if (mHandler == null) return;  // already stopped
    263         postStopSample();
    264         mContext.getContentResolver().unregisterContentObserver(mVolumeObserver);
    265         mReceiver.setListening(false);
    266         mSeekBar.setOnSeekBarChangeListener(null);
    267         mHandler.getLooper().quitSafely();
    268         mHandler = null;
    269         mVolumeObserver = null;
    270     }
    271 
    272     public void start() {
    273         if (mHandler != null) return;  // already started
    274         HandlerThread thread = new HandlerThread(TAG + ".CallbackHandler");
    275         thread.start();
    276         mHandler = new Handler(thread.getLooper(), this);
    277         mHandler.sendEmptyMessage(MSG_INIT_SAMPLE);
    278         mVolumeObserver = new Observer(mHandler);
    279         mContext.getContentResolver().registerContentObserver(
    280                 System.getUriFor(System.VOLUME_SETTINGS[mStreamType]),
    281                 false, mVolumeObserver);
    282         mReceiver.setListening(true);
    283     }
    284 
    285     public void revertVolume() {
    286         mAudioManager.setStreamVolume(mStreamType, mOriginalStreamVolume, 0);
    287     }
    288 
    289     public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) {
    290         if (fromTouch) {
    291             postSetVolume(progress);
    292         }
    293         if (mCallback != null) {
    294             mCallback.onProgressChanged(seekBar, progress, fromTouch);
    295         }
    296     }
    297 
    298     private void postSetVolume(int progress) {
    299         if (mHandler == null) return;
    300         // Do the volume changing separately to give responsive UI
    301         mLastProgress = progress;
    302         mHandler.removeMessages(MSG_SET_STREAM_VOLUME);
    303         mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_STREAM_VOLUME));
    304     }
    305 
    306     public void onStartTrackingTouch(SeekBar seekBar) {
    307     }
    308 
    309     public void onStopTrackingTouch(SeekBar seekBar) {
    310         postStartSample();
    311     }
    312 
    313     public boolean isSamplePlaying() {
    314         synchronized (this) {
    315             return mRingtone != null && mRingtone.isPlaying();
    316         }
    317     }
    318 
    319     public void startSample() {
    320         postStartSample();
    321     }
    322 
    323     public void stopSample() {
    324         postStopSample();
    325     }
    326 
    327     public SeekBar getSeekBar() {
    328         return mSeekBar;
    329     }
    330 
    331     public void changeVolumeBy(int amount) {
    332         mSeekBar.incrementProgressBy(amount);
    333         postSetVolume(mSeekBar.getProgress());
    334         postStartSample();
    335         mVolumeBeforeMute = -1;
    336     }
    337 
    338     public void muteVolume() {
    339         if (mVolumeBeforeMute != -1) {
    340             mSeekBar.setProgress(mVolumeBeforeMute, true);
    341             postSetVolume(mVolumeBeforeMute);
    342             postStartSample();
    343             mVolumeBeforeMute = -1;
    344         } else {
    345             mVolumeBeforeMute = mSeekBar.getProgress();
    346             mSeekBar.setProgress(0, true);
    347             postStopSample();
    348             postSetVolume(0);
    349         }
    350     }
    351 
    352     public void onSaveInstanceState(VolumeStore volumeStore) {
    353         if (mLastProgress >= 0) {
    354             volumeStore.volume = mLastProgress;
    355             volumeStore.originalVolume = mOriginalStreamVolume;
    356         }
    357     }
    358 
    359     public void onRestoreInstanceState(VolumeStore volumeStore) {
    360         if (volumeStore.volume != -1) {
    361             mOriginalStreamVolume = volumeStore.originalVolume;
    362             mLastProgress = volumeStore.volume;
    363             postSetVolume(mLastProgress);
    364         }
    365     }
    366 
    367     private final class H extends Handler {
    368         private static final int UPDATE_SLIDER = 1;
    369 
    370         @Override
    371         public void handleMessage(Message msg) {
    372             if (msg.what == UPDATE_SLIDER) {
    373                 if (mSeekBar != null) {
    374                     mLastProgress = msg.arg1;
    375                     mLastAudibleStreamVolume = msg.arg2;
    376                     final boolean muted = ((Boolean)msg.obj).booleanValue();
    377                     if (muted != mMuted) {
    378                         mMuted = muted;
    379                         if (mCallback != null) {
    380                             mCallback.onMuted(mMuted, isZenMuted());
    381                         }
    382                     }
    383                     updateSeekBar();
    384                 }
    385             }
    386         }
    387 
    388         public void postUpdateSlider(int volume, int lastAudibleVolume, boolean mute) {
    389             obtainMessage(UPDATE_SLIDER, volume, lastAudibleVolume, new Boolean(mute)).sendToTarget();
    390         }
    391     }
    392 
    393     private void updateSlider() {
    394         if (mSeekBar != null && mAudioManager != null) {
    395             final int volume = mAudioManager.getStreamVolume(mStreamType);
    396             final int lastAudibleVolume = mAudioManager.getLastAudibleStreamVolume(mStreamType);
    397             final boolean mute = mAudioManager.isStreamMute(mStreamType);
    398             mUiHandler.postUpdateSlider(volume, lastAudibleVolume, mute);
    399         }
    400     }
    401 
    402     private final class Observer extends ContentObserver {
    403         public Observer(Handler handler) {
    404             super(handler);
    405         }
    406 
    407         @Override
    408         public void onChange(boolean selfChange) {
    409             super.onChange(selfChange);
    410             updateSlider();
    411         }
    412     }
    413 
    414     private final class Receiver extends BroadcastReceiver {
    415         private boolean mListening;
    416 
    417         public void setListening(boolean listening) {
    418             if (mListening == listening) return;
    419             mListening = listening;
    420             if (listening) {
    421                 final IntentFilter filter = new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION);
    422                 filter.addAction(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION);
    423                 filter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED);
    424                 filter.addAction(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED);
    425                 filter.addAction(AudioManager.STREAM_DEVICES_CHANGED_ACTION);
    426                 mContext.registerReceiver(this, filter);
    427             } else {
    428                 mContext.unregisterReceiver(this);
    429             }
    430         }
    431 
    432         @Override
    433         public void onReceive(Context context, Intent intent) {
    434             final String action = intent.getAction();
    435             if (AudioManager.VOLUME_CHANGED_ACTION.equals(action)) {
    436                 int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
    437                 int streamValue = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, -1);
    438                 updateVolumeSlider(streamType, streamValue);
    439             } else if (AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION.equals(action)) {
    440                 if (mNotificationOrRing) {
    441                     mRingerMode = mAudioManager.getRingerModeInternal();
    442                 }
    443                 if (mAffectedByRingerMode) {
    444                     updateSlider();
    445                 }
    446             } else if (AudioManager.STREAM_DEVICES_CHANGED_ACTION.equals(action)) {
    447                 int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
    448                 int streamVolume = mAudioManager.getStreamVolume(streamType);
    449                 updateVolumeSlider(streamType, streamVolume);
    450             } else if (NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED.equals(action)) {
    451                 mZenMode = mNotificationManager.getZenMode();
    452                 updateSlider();
    453             } else if (NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED.equals(action)) {
    454                 mNotificationPolicy = mNotificationManager.getNotificationPolicy();
    455                 mAllowAlarms = (mNotificationPolicy.priorityCategories & NotificationManager.Policy
    456                         .PRIORITY_CATEGORY_ALARMS) != 0;
    457                 mAllowMedia = (mNotificationPolicy.priorityCategories
    458                         & NotificationManager.Policy.PRIORITY_CATEGORY_MEDIA) != 0;
    459                 mAllowRinger = !ZenModeConfig.areAllPriorityOnlyNotificationZenSoundsMuted(
    460                         mNotificationPolicy);
    461                 updateSlider();
    462             }
    463         }
    464 
    465         private void updateVolumeSlider(int streamType, int streamValue) {
    466             final boolean streamMatch = mNotificationOrRing ? isNotificationOrRing(streamType)
    467                     : (streamType == mStreamType);
    468             if (mSeekBar != null && streamMatch && streamValue != -1) {
    469                 final boolean muted = mAudioManager.isStreamMute(mStreamType)
    470                         || streamValue == 0;
    471                 mUiHandler.postUpdateSlider(streamValue, mLastAudibleStreamVolume, muted);
    472             }
    473         }
    474     }
    475 }
    476