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
17package android.preference;
18
19import android.app.NotificationManager;
20import android.content.BroadcastReceiver;
21import android.content.Context;
22import android.content.Intent;
23import android.content.IntentFilter;
24import android.database.ContentObserver;
25import android.media.AudioAttributes;
26import android.media.AudioManager;
27import android.media.Ringtone;
28import android.media.RingtoneManager;
29import android.net.Uri;
30import android.os.Handler;
31import android.os.HandlerThread;
32import android.os.Message;
33import android.preference.VolumePreference.VolumeStore;
34import android.provider.Settings;
35import android.provider.Settings.Global;
36import android.provider.Settings.System;
37import android.util.Log;
38import android.widget.SeekBar;
39import android.widget.SeekBar.OnSeekBarChangeListener;
40
41import com.android.internal.annotations.GuardedBy;
42
43/**
44 * Turns a {@link SeekBar} into a volume control.
45 * @hide
46 */
47public 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);
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
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);
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
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 = 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