2 * Copyright (C) 2014 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package android.preference;
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;
41 import com.android.internal.annotations.GuardedBy;
44 * Turns a {@link SeekBar} into a volume control.
47 public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callback {
48 private static final String TAG = "SeekBarVolumizer";
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);
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 final boolean mVoiceCapable;
65 private boolean mAffectedByRingerMode;
66 private boolean mNotificationOrRing;
67 private final Receiver mReceiver = new Receiver();
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
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;
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;
91 public SeekBarVolumizer(Context context, int streamType, Uri defaultUri, Callback callback) {
93 mAudioManager = context.getSystemService(AudioManager.class);
94 mNotificationManager = context.getSystemService(NotificationManager.class);
95 mStreamType = streamType;
96 mAffectedByRingerMode = mAudioManager.isStreamAffectedByRingerMode(mStreamType);
97 mNotificationOrRing = isNotificationOrRing(mStreamType);
98 if (mNotificationOrRing) {
99 mRingerMode = mAudioManager.getRingerModeInternal();
101 mZenMode = mNotificationManager.getZenMode();
102 mMaxStreamVolume = mAudioManager.getStreamMaxVolume(mStreamType);
103 mCallback = callback;
104 mOriginalStreamVolume = mAudioManager.getStreamVolume(mStreamType);
105 mLastAudibleStreamVolume = mAudioManager.getLastAudibleStreamVolume(mStreamType);
106 mMuted = mAudioManager.isStreamMute(mStreamType);
107 if (mCallback != null) {
108 mCallback.onMuted(mMuted, isZenMuted());
110 if (defaultUri == null) {
111 if (mStreamType == AudioManager.STREAM_RING) {
112 defaultUri = Settings.System.DEFAULT_RINGTONE_URI;
113 } else if (mStreamType == AudioManager.STREAM_NOTIFICATION) {
114 defaultUri = Settings.System.DEFAULT_NOTIFICATION_URI;
116 defaultUri = Settings.System.DEFAULT_ALARM_ALERT_URI;
119 mDefaultUri = defaultUri;
120 mVoiceCapable = context.getResources().getBoolean(
121 com.android.internal.R.bool.config_voice_capable);
124 private static boolean isNotificationOrRing(int stream) {
125 return stream == AudioManager.STREAM_RING || stream == AudioManager.STREAM_NOTIFICATION;
128 private boolean isNotificationStreamLinked() {
129 return mVoiceCapable && Settings.Secure.getInt(mContext.getContentResolver(),
130 Settings.Secure.VOLUME_LINK_NOTIFICATION, 1) == 1;
133 public void setSeekBar(SeekBar seekBar) {
134 if (mSeekBar != null) {
135 mSeekBar.setOnSeekBarChangeListener(null);
138 mSeekBar.setOnSeekBarChangeListener(null);
139 mSeekBar.setMax(mMaxStreamVolume);
141 mSeekBar.setOnSeekBarChangeListener(this);
144 private boolean isZenMuted() {
145 return mNotificationOrRing && mZenMode == Global.ZEN_MODE_ALARMS
146 || mZenMode == Global.ZEN_MODE_NO_INTERRUPTIONS;
149 protected void updateSeekBar() {
150 final boolean zenMuted = isZenMuted();
151 mSeekBar.setEnabled(!zenMuted);
153 mSeekBar.setProgress(mLastAudibleStreamVolume);
154 } else if (mNotificationOrRing && mRingerMode == AudioManager.RINGER_MODE_VIBRATE) {
155 mSeekBar.setProgress(0);
156 mSeekBar.setEnabled(mStreamType == AudioManager.STREAM_RING);
158 mSeekBar.setProgress(0);
160 mSeekBar.setEnabled(enableSeekBar());
161 mSeekBar.setProgress(mLastProgress > -1 ? mLastProgress : mOriginalStreamVolume);
165 private boolean enableSeekBar() {
166 return !(mStreamType == AudioManager.STREAM_NOTIFICATION && isNotificationStreamLinked());
170 public boolean handleMessage(Message msg) {
172 case MSG_SET_STREAM_VOLUME:
173 if (mMuted && mLastProgress > 0) {
174 mAudioManager.adjustStreamVolume(mStreamType, AudioManager.ADJUST_UNMUTE, 0);
175 } else if (!mMuted && mLastProgress == 0) {
176 mAudioManager.adjustStreamVolume(mStreamType, AudioManager.ADJUST_MUTE, 0);
178 mAudioManager.setStreamVolume(mStreamType, mLastProgress,
179 AudioManager.FLAG_SHOW_UI_WARNINGS);
181 case MSG_START_SAMPLE:
184 case MSG_STOP_SAMPLE:
187 case MSG_INIT_SAMPLE:
191 Log.e(TAG, "invalid SeekBarVolumizer message: "+msg.what);
196 private void onInitSample() {
197 synchronized (this) {
198 mRingtone = RingtoneManager.getRingtone(mContext, mDefaultUri);
199 if (mRingtone != null) {
200 mRingtone.setStreamType(mStreamType);
205 private void postStartSample() {
206 if (mHandler == null) return;
207 mHandler.removeMessages(MSG_START_SAMPLE);
208 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_START_SAMPLE),
209 isSamplePlaying() ? CHECK_RINGTONE_PLAYBACK_DELAY_MS : 0);
212 private void onStartSample() {
213 if (!isSamplePlaying()) {
214 if (mCallback != null) {
215 mCallback.onSampleStarting(this);
218 synchronized (this) {
219 if (mRingtone != null) {
221 mRingtone.setAudioAttributes(new AudioAttributes.Builder(mRingtone
222 .getAudioAttributes())
223 .setFlags(AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY |
224 AudioAttributes.FLAG_BYPASS_MUTE)
227 } catch (Throwable e) {
228 Log.w(TAG, "Error playing ringtone, stream " + mStreamType, e);
235 private void postStopSample() {
236 if (mHandler == null) return;
237 // remove pending delayed start messages
238 mHandler.removeMessages(MSG_START_SAMPLE);
239 mHandler.removeMessages(MSG_STOP_SAMPLE);
240 mHandler.sendMessage(mHandler.obtainMessage(MSG_STOP_SAMPLE));
243 private void onStopSample() {
244 synchronized (this) {
245 if (mRingtone != null) {
252 if (mHandler == null) return; // already stopped
254 mContext.getContentResolver().unregisterContentObserver(mVolumeObserver);
255 mReceiver.setListening(false);
256 mSeekBar.setOnSeekBarChangeListener(null);
257 mHandler.getLooper().quitSafely();
259 mVolumeObserver = null;
262 public void start() {
263 if (mHandler != null) return; // already started
264 HandlerThread thread = new HandlerThread(TAG + ".CallbackHandler");
266 mHandler = new Handler(thread.getLooper(), this);
267 mHandler.sendEmptyMessage(MSG_INIT_SAMPLE);
268 mVolumeObserver = new Observer(mHandler);
269 mContext.getContentResolver().registerContentObserver(
270 System.getUriFor(System.VOLUME_SETTINGS[mStreamType]),
271 false, mVolumeObserver);
272 mReceiver.setListening(true);
275 public void revertVolume() {
276 mAudioManager.setStreamVolume(mStreamType, mOriginalStreamVolume, 0);
279 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) {
280 if (fromTouch && enableSeekBar()) {
281 postSetVolume(progress);
283 if (mCallback != null) {
284 mCallback.onProgressChanged(seekBar, progress, fromTouch);
288 private void postSetVolume(int progress) {
289 if (mHandler == null) return;
290 // Do the volume changing separately to give responsive UI
291 mLastProgress = progress;
292 mHandler.removeMessages(MSG_SET_STREAM_VOLUME);
293 mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_STREAM_VOLUME));
296 public void onStartTrackingTouch(SeekBar seekBar) {
299 public void onStopTrackingTouch(SeekBar seekBar) {
303 public boolean isSamplePlaying() {
304 synchronized (this) {
305 return mRingtone != null && mRingtone.isPlaying();
309 public void startSample() {
313 public void stopSample() {
317 public SeekBar getSeekBar() {
321 public void changeVolumeBy(int amount) {
322 mSeekBar.incrementProgressBy(amount);
323 postSetVolume(mSeekBar.getProgress());
325 mVolumeBeforeMute = -1;
328 public void muteVolume() {
329 if (mVolumeBeforeMute != -1) {
330 mSeekBar.setProgress(mVolumeBeforeMute);
331 postSetVolume(mVolumeBeforeMute);
333 mVolumeBeforeMute = -1;
335 mVolumeBeforeMute = mSeekBar.getProgress();
336 mSeekBar.setProgress(0);
342 public void onSaveInstanceState(VolumeStore volumeStore) {
343 if (mLastProgress >= 0) {
344 volumeStore.volume = mLastProgress;
345 volumeStore.originalVolume = mOriginalStreamVolume;
349 public void onRestoreInstanceState(VolumeStore volumeStore) {
350 if (volumeStore.volume != -1) {
351 mOriginalStreamVolume = volumeStore.originalVolume;
352 mLastProgress = volumeStore.volume;
353 postSetVolume(mLastProgress);
357 private final class H extends Handler {
358 private static final int UPDATE_SLIDER = 1;
361 public void handleMessage(Message msg) {
362 if (msg.what == UPDATE_SLIDER) {
363 if (mSeekBar != null) {
364 mLastProgress = msg.arg1;
365 mLastAudibleStreamVolume = msg.arg2;
366 final boolean muted = ((Boolean)msg.obj).booleanValue();
367 if (muted != mMuted) {
369 if (mCallback != null) {
370 mCallback.onMuted(mMuted, isZenMuted());
378 public void postUpdateSlider(int volume, int lastAudibleVolume, boolean mute) {
379 obtainMessage(UPDATE_SLIDER, volume, lastAudibleVolume, new Boolean(mute)).sendToTarget();
383 private void updateSlider() {
384 if (mSeekBar != null && mAudioManager != null) {
385 final int volume = mAudioManager.getStreamVolume(mStreamType);
386 final int lastAudibleVolume = mAudioManager.getLastAudibleStreamVolume(mStreamType);
387 final boolean mute = mAudioManager.isStreamMute(mStreamType);
388 mUiHandler.postUpdateSlider(volume, lastAudibleVolume, mute);
392 private final class Observer extends ContentObserver {
393 public Observer(Handler handler) {
398 public void onChange(boolean selfChange) {
399 super.onChange(selfChange);
404 private final class Receiver extends BroadcastReceiver {
405 private boolean mListening;
407 public void setListening(boolean listening) {
408 if (mListening == listening) return;
409 mListening = listening;
411 final IntentFilter filter = new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION);
412 filter.addAction(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION);
413 filter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED);
414 filter.addAction(AudioManager.STREAM_DEVICES_CHANGED_ACTION);
415 mContext.registerReceiver(this, filter);
417 mContext.unregisterReceiver(this);
422 public void onReceive(Context context, Intent intent) {
423 final String action = intent.getAction();
424 if (AudioManager.VOLUME_CHANGED_ACTION.equals(action)) {
425 int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
426 int streamValue = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, -1);
427 updateVolumeSlider(streamType, streamValue);
428 } else if (AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION.equals(action)) {
429 if (mNotificationOrRing) {
430 mRingerMode = mAudioManager.getRingerModeInternal();
432 if (mAffectedByRingerMode) {
435 } else if (AudioManager.STREAM_DEVICES_CHANGED_ACTION.equals(action)) {
436 int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
437 int streamVolume = mAudioManager.getStreamVolume(streamType);
438 updateVolumeSlider(streamType, streamVolume);
439 } else if (NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED.equals(action)) {
440 mZenMode = mNotificationManager.getZenMode();
445 private void updateVolumeSlider(int streamType, int streamValue) {
446 final boolean streamMatch = mNotificationOrRing && isNotificationStreamLinked()
447 ? isNotificationOrRing(streamType)
448 : (streamType == mStreamType);
449 if (mSeekBar != null && streamMatch && streamValue != -1) {
450 final boolean muted = mAudioManager.isStreamMute(mStreamType)
452 mUiHandler.postUpdateSlider(streamValue, mLastAudibleStreamVolume, muted);