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 {
onSampleStarting(SeekBarVolumizer sbv)51         void onSampleStarting(SeekBarVolumizer sbv);
onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch)52         void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch);
onMuted(boolean muted, boolean zenMuted)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 
SeekBarVolumizer(Context context, int streamType, Uri defaultUri, Callback callback)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 
isNotificationOrRing(int stream)121     private static boolean isNotificationOrRing(int stream) {
122         return stream == AudioManager.STREAM_RING || stream == AudioManager.STREAM_NOTIFICATION;
123     }
124 
setSeekBar(SeekBar seekBar)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 
isZenMuted()136     private boolean isZenMuted() {
137         return mNotificationOrRing && mZenMode == Global.ZEN_MODE_ALARMS
138                 || mZenMode == Global.ZEN_MODE_NO_INTERRUPTIONS;
139     }
140 
updateSeekBar()141     protected void updateSeekBar() {
142         final boolean zenMuted = isZenMuted();
143         mSeekBar.setEnabled(!zenMuted);
144         if (zenMuted) {
145             mSeekBar.setProgress(mLastAudibleStreamVolume);
146         } else if (mNotificationOrRing && mRingerMode == AudioManager.RINGER_MODE_VIBRATE) {
147             mSeekBar.setProgress(0);
148         } else if (mMuted) {
149             mSeekBar.setProgress(0);
150         } else {
151             mSeekBar.setProgress(mLastProgress > -1 ? mLastProgress : mOriginalStreamVolume);
152         }
153     }
154 
155     @Override
handleMessage(Message msg)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 
onInitSample()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 
postStartSample()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 
onStartSample()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 
postStopSample()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 
onStopSample()229     private void onStopSample() {
230         synchronized (this) {
231             if (mRingtone != null) {
232                 mRingtone.stop();
233             }
234         }
235     }
236 
stop()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 
start()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 
revertVolume()261     public void revertVolume() {
262         mAudioManager.setStreamVolume(mStreamType, mOriginalStreamVolume, 0);
263     }
264 
onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch)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 
postSetVolume(int progress)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 
onStartTrackingTouch(SeekBar seekBar)282     public void onStartTrackingTouch(SeekBar seekBar) {
283     }
284 
onStopTrackingTouch(SeekBar seekBar)285     public void onStopTrackingTouch(SeekBar seekBar) {
286         postStartSample();
287     }
288 
isSamplePlaying()289     public boolean isSamplePlaying() {
290         synchronized (this) {
291             return mRingtone != null && mRingtone.isPlaying();
292         }
293     }
294 
startSample()295     public void startSample() {
296         postStartSample();
297     }
298 
stopSample()299     public void stopSample() {
300         postStopSample();
301     }
302 
getSeekBar()303     public SeekBar getSeekBar() {
304         return mSeekBar;
305     }
306 
changeVolumeBy(int amount)307     public void changeVolumeBy(int amount) {
308         mSeekBar.incrementProgressBy(amount);
309         postSetVolume(mSeekBar.getProgress());
310         postStartSample();
311         mVolumeBeforeMute = -1;
312     }
313 
muteVolume()314     public void muteVolume() {
315         if (mVolumeBeforeMute != -1) {
316             mSeekBar.setProgress(mVolumeBeforeMute);
317             postSetVolume(mVolumeBeforeMute);
318             postStartSample();
319             mVolumeBeforeMute = -1;
320         } else {
321             mVolumeBeforeMute = mSeekBar.getProgress();
322             mSeekBar.setProgress(0);
323             postStopSample();
324             postSetVolume(0);
325         }
326     }
327 
onSaveInstanceState(VolumeStore volumeStore)328     public void onSaveInstanceState(VolumeStore volumeStore) {
329         if (mLastProgress >= 0) {
330             volumeStore.volume = mLastProgress;
331             volumeStore.originalVolume = mOriginalStreamVolume;
332         }
333     }
334 
onRestoreInstanceState(VolumeStore volumeStore)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
handleMessage(Message msg)347         public void handleMessage(Message msg) {
348             if (msg.what == UPDATE_SLIDER) {
349                 if (mSeekBar != null) {
350                     mLastProgress = msg.arg1;
351                     mLastAudibleStreamVolume = Math.abs(msg.arg2);
352                     final boolean muted = msg.arg2 < 0;
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             final int arg2 = lastAudibleVolume * (mute ? -1 : 1);
366             obtainMessage(UPDATE_SLIDER, volume, arg2).sendToTarget();
367         }
368     }
369 
370     private void updateSlider() {
371         if (mSeekBar != null && mAudioManager != null) {
372             final int volume = mAudioManager.getStreamVolume(mStreamType);
373             final int lastAudibleVolume = mAudioManager.getLastAudibleStreamVolume(mStreamType);
374             final boolean mute = mAudioManager.isStreamMute(mStreamType);
375             mUiHandler.postUpdateSlider(volume, lastAudibleVolume, mute);
376         }
377     }
378 
379     private final class Observer extends ContentObserver {
380         public Observer(Handler handler) {
381             super(handler);
382         }
383 
384         @Override
385         public void onChange(boolean selfChange) {
386             super.onChange(selfChange);
387             updateSlider();
388         }
389     }
390 
391     private final class Receiver extends BroadcastReceiver {
392         private boolean mListening;
393 
394         public void setListening(boolean listening) {
395             if (mListening == listening) return;
396             mListening = listening;
397             if (listening) {
398                 final IntentFilter filter = new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION);
399                 filter.addAction(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION);
400                 filter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED);
401                 filter.addAction(AudioManager.STREAM_DEVICES_CHANGED_ACTION);
402                 mContext.registerReceiver(this, filter);
403             } else {
404                 mContext.unregisterReceiver(this);
405             }
406         }
407 
408         @Override
409         public void onReceive(Context context, Intent intent) {
410             final String action = intent.getAction();
411             if (AudioManager.VOLUME_CHANGED_ACTION.equals(action)) {
412                 int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
413                 int streamValue = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, -1);
414                 updateVolumeSlider(streamType, streamValue);
415             } else if (AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION.equals(action)) {
416                 if (mNotificationOrRing) {
417                     mRingerMode = mAudioManager.getRingerModeInternal();
418                 }
419                 if (mAffectedByRingerMode) {
420                     updateSlider();
421                 }
422             } else if (AudioManager.STREAM_DEVICES_CHANGED_ACTION.equals(action)) {
423                 int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
424                 int streamVolume = mAudioManager.getStreamVolume(streamType);
425                 updateVolumeSlider(streamType, streamVolume);
426             } else if (NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED.equals(action)) {
427                 mZenMode = mNotificationManager.getZenMode();
428                 updateSlider();
429             }
430         }
431 
432         private void updateVolumeSlider(int streamType, int streamValue) {
433             final boolean streamMatch = mNotificationOrRing ? isNotificationOrRing(streamType)
434                     : (streamType == mStreamType);
435             if (mSeekBar != null && streamMatch && streamValue != -1) {
436                 final boolean muted = mAudioManager.isStreamMute(mStreamType)
437                         || streamValue == 0;
438                 mUiHandler.postUpdateSlider(streamValue, mLastAudibleStreamVolume, muted);
439             }
440         }
441     }
442 }
443