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