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