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 {
onSampleStarting(SeekBarVolumizer sbv)49         void onSampleStarting(SeekBarVolumizer sbv);
onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch)50         void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch);
onMuted(boolean muted, boolean zenMuted)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 
SeekBarVolumizer(Context context, int streamType, Uri defaultUri, Callback callback)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 
isNotificationOrRing(int stream)115     private static boolean isNotificationOrRing(int stream) {
116         return stream == AudioManager.STREAM_RING || stream == AudioManager.STREAM_NOTIFICATION;
117     }
118 
setSeekBar(SeekBar seekBar)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 
isZenMuted()130     private boolean isZenMuted() {
131         return mNotificationOrRing && mZenMode == Global.ZEN_MODE_ALARMS
132                 || mZenMode == Global.ZEN_MODE_NO_INTERRUPTIONS;
133     }
134 
updateSeekBar()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
handleMessage(Message msg)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 
onInitSample()176     private void onInitSample() {
177         mRingtone = RingtoneManager.getRingtone(mContext, mDefaultUri);
178         if (mRingtone != null) {
179             mRingtone.setStreamType(mStreamType);
180         }
181     }
182 
postStartSample()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 
onStartSample()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 
postStopSample()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 
onStopSample()218     private void onStopSample() {
219         if (mRingtone != null) {
220             mRingtone.stop();
221         }
222     }
223 
stop()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 
start()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 
revertVolume()248     public void revertVolume() {
249         mAudioManager.setStreamVolume(mStreamType, mOriginalStreamVolume, 0);
250     }
251 
onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch)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 
postSetVolume(int progress)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 
onStartTrackingTouch(SeekBar seekBar)269     public void onStartTrackingTouch(SeekBar seekBar) {
270     }
271 
onStopTrackingTouch(SeekBar seekBar)272     public void onStopTrackingTouch(SeekBar seekBar) {
273         postStartSample();
274     }
275 
isSamplePlaying()276     public boolean isSamplePlaying() {
277         return mRingtone != null && mRingtone.isPlaying();
278     }
279 
startSample()280     public void startSample() {
281         postStartSample();
282     }
283 
stopSample()284     public void stopSample() {
285         postStopSample();
286     }
287 
getSeekBar()288     public SeekBar getSeekBar() {
289         return mSeekBar;
290     }
291 
changeVolumeBy(int amount)292     public void changeVolumeBy(int amount) {
293         mSeekBar.incrementProgressBy(amount);
294         postSetVolume(mSeekBar.getProgress());
295         postStartSample();
296         mVolumeBeforeMute = -1;
297     }
298 
muteVolume()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 
onSaveInstanceState(VolumeStore volumeStore)313     public void onSaveInstanceState(VolumeStore volumeStore) {
314         if (mLastProgress >= 0) {
315             volumeStore.volume = mLastProgress;
316             volumeStore.originalVolume = mOriginalStreamVolume;
317         }
318     }
319 
onRestoreInstanceState(VolumeStore volumeStore)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
handleMessage(Message msg)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