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