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.service.notification.ZenModeConfig;
38import android.util.Log;
39import android.widget.SeekBar;
40import android.widget.SeekBar.OnSeekBarChangeListener;
41
42import com.android.internal.annotations.GuardedBy;
43
44/**
45 * Turns a {@link SeekBar} into a volume control.
46 * @hide
47 */
48public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callback {
49    private static final String TAG = "SeekBarVolumizer";
50
51    public interface Callback {
52        void onSampleStarting(SeekBarVolumizer sbv);
53        void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch);
54        void onMuted(boolean muted, boolean zenMuted);
55    }
56
57    private final Context mContext;
58    private final H mUiHandler = new H();
59    private final Callback mCallback;
60    private final Uri mDefaultUri;
61    private final AudioManager mAudioManager;
62    private final NotificationManager mNotificationManager;
63    private final int mStreamType;
64    private final int mMaxStreamVolume;
65    private boolean mAffectedByRingerMode;
66    private boolean mNotificationOrRing;
67    private final Receiver mReceiver = new Receiver();
68
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
75    // synchronized.
76    @GuardedBy("this")
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;
83    private int mZenMode;
84
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;
90
91    private NotificationManager.Policy mNotificationPolicy;
92    private boolean mAllowAlarms;
93    private boolean mAllowMedia;
94    private boolean mAllowRinger;
95
96    public SeekBarVolumizer(Context context, int streamType, Uri defaultUri, Callback callback) {
97        mContext = context;
98        mAudioManager = context.getSystemService(AudioManager.class);
99        mNotificationManager = context.getSystemService(NotificationManager.class);
100        mNotificationPolicy = mNotificationManager.getNotificationPolicy();
101        mAllowAlarms = (mNotificationPolicy.priorityCategories & NotificationManager.Policy
102                .PRIORITY_CATEGORY_ALARMS) != 0;
103        mAllowMedia = (mNotificationPolicy.priorityCategories & NotificationManager.Policy
104                .PRIORITY_CATEGORY_MEDIA) != 0;
105        mAllowRinger = !ZenModeConfig.areAllPriorityOnlyNotificationZenSoundsMuted(
106                mNotificationPolicy);
107        mStreamType = streamType;
108        mAffectedByRingerMode = mAudioManager.isStreamAffectedByRingerMode(mStreamType);
109        mNotificationOrRing = isNotificationOrRing(mStreamType);
110        if (mNotificationOrRing) {
111            mRingerMode = mAudioManager.getRingerModeInternal();
112        }
113        mZenMode = mNotificationManager.getZenMode();
114        mMaxStreamVolume = mAudioManager.getStreamMaxVolume(mStreamType);
115        mCallback = callback;
116        mOriginalStreamVolume = mAudioManager.getStreamVolume(mStreamType);
117        mLastAudibleStreamVolume = mAudioManager.getLastAudibleStreamVolume(mStreamType);
118        mMuted = mAudioManager.isStreamMute(mStreamType);
119        if (mCallback != null) {
120            mCallback.onMuted(mMuted, isZenMuted());
121        }
122        if (defaultUri == null) {
123            if (mStreamType == AudioManager.STREAM_RING) {
124                defaultUri = Settings.System.DEFAULT_RINGTONE_URI;
125            } else if (mStreamType == AudioManager.STREAM_NOTIFICATION) {
126                defaultUri = Settings.System.DEFAULT_NOTIFICATION_URI;
127            } else {
128                defaultUri = Settings.System.DEFAULT_ALARM_ALERT_URI;
129            }
130        }
131        mDefaultUri = defaultUri;
132    }
133
134    private static boolean isNotificationOrRing(int stream) {
135        return stream == AudioManager.STREAM_RING || stream == AudioManager.STREAM_NOTIFICATION;
136    }
137
138    private static boolean isAlarmsStream(int stream) {
139        return stream == AudioManager.STREAM_ALARM;
140    }
141
142    private static boolean isMediaStream(int stream) {
143        return stream == AudioManager.STREAM_MUSIC;
144    }
145
146    public void setSeekBar(SeekBar seekBar) {
147        if (mSeekBar != null) {
148            mSeekBar.setOnSeekBarChangeListener(null);
149        }
150        mSeekBar = seekBar;
151        mSeekBar.setOnSeekBarChangeListener(null);
152        mSeekBar.setMax(mMaxStreamVolume);
153        updateSeekBar();
154        mSeekBar.setOnSeekBarChangeListener(this);
155    }
156
157    private boolean isZenMuted() {
158        return mNotificationOrRing && mZenMode == Global.ZEN_MODE_ALARMS
159                || mZenMode == Global.ZEN_MODE_NO_INTERRUPTIONS
160                || (mZenMode == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
161                    && ((!mAllowAlarms && isAlarmsStream(mStreamType))
162                        || (!mAllowMedia && isMediaStream(mStreamType))
163                        || (!mAllowRinger && isNotificationOrRing(mStreamType))));
164    }
165
166    protected void updateSeekBar() {
167        final boolean zenMuted = isZenMuted();
168        mSeekBar.setEnabled(!zenMuted);
169        if (zenMuted) {
170            mSeekBar.setProgress(mLastAudibleStreamVolume, true);
171        } else if (mNotificationOrRing && mRingerMode == AudioManager.RINGER_MODE_VIBRATE) {
172            mSeekBar.setProgress(0, true);
173        } else if (mMuted) {
174            mSeekBar.setProgress(0, true);
175        } else {
176            mSeekBar.setProgress(mLastProgress > -1 ? mLastProgress : mOriginalStreamVolume, true);
177        }
178    }
179
180    @Override
181    public boolean handleMessage(Message msg) {
182        switch (msg.what) {
183            case MSG_SET_STREAM_VOLUME:
184                if (mMuted && mLastProgress > 0) {
185                    mAudioManager.adjustStreamVolume(mStreamType, AudioManager.ADJUST_UNMUTE, 0);
186                } else if (!mMuted && mLastProgress == 0) {
187                    mAudioManager.adjustStreamVolume(mStreamType, AudioManager.ADJUST_MUTE, 0);
188                }
189                mAudioManager.setStreamVolume(mStreamType, mLastProgress,
190                        AudioManager.FLAG_SHOW_UI_WARNINGS);
191                break;
192            case MSG_START_SAMPLE:
193                onStartSample();
194                break;
195            case MSG_STOP_SAMPLE:
196                onStopSample();
197                break;
198            case MSG_INIT_SAMPLE:
199                onInitSample();
200                break;
201            default:
202                Log.e(TAG, "invalid SeekBarVolumizer message: "+msg.what);
203        }
204        return true;
205    }
206
207    private void onInitSample() {
208        synchronized (this) {
209            mRingtone = RingtoneManager.getRingtone(mContext, mDefaultUri);
210            if (mRingtone != null) {
211                mRingtone.setStreamType(mStreamType);
212            }
213        }
214    }
215
216    private void postStartSample() {
217        if (mHandler == null) return;
218        mHandler.removeMessages(MSG_START_SAMPLE);
219        mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_START_SAMPLE),
220                isSamplePlaying() ? CHECK_RINGTONE_PLAYBACK_DELAY_MS : 0);
221    }
222
223    private void onStartSample() {
224        if (!isSamplePlaying()) {
225            if (mCallback != null) {
226                mCallback.onSampleStarting(this);
227            }
228
229            synchronized (this) {
230                if (mRingtone != null) {
231                    try {
232                        mRingtone.setAudioAttributes(new AudioAttributes.Builder(mRingtone
233                                .getAudioAttributes())
234                                .setFlags(AudioAttributes.FLAG_BYPASS_MUTE)
235                                .build());
236                        mRingtone.play();
237                    } catch (Throwable e) {
238                        Log.w(TAG, "Error playing ringtone, stream " + mStreamType, e);
239                    }
240                }
241            }
242        }
243    }
244
245    private void postStopSample() {
246        if (mHandler == null) return;
247        // remove pending delayed start messages
248        mHandler.removeMessages(MSG_START_SAMPLE);
249        mHandler.removeMessages(MSG_STOP_SAMPLE);
250        mHandler.sendMessage(mHandler.obtainMessage(MSG_STOP_SAMPLE));
251    }
252
253    private void onStopSample() {
254        synchronized (this) {
255            if (mRingtone != null) {
256                mRingtone.stop();
257            }
258        }
259    }
260
261    public void stop() {
262        if (mHandler == null) return;  // already stopped
263        postStopSample();
264        mContext.getContentResolver().unregisterContentObserver(mVolumeObserver);
265        mReceiver.setListening(false);
266        mSeekBar.setOnSeekBarChangeListener(null);
267        mHandler.getLooper().quitSafely();
268        mHandler = null;
269        mVolumeObserver = null;
270    }
271
272    public void start() {
273        if (mHandler != null) return;  // already started
274        HandlerThread thread = new HandlerThread(TAG + ".CallbackHandler");
275        thread.start();
276        mHandler = new Handler(thread.getLooper(), this);
277        mHandler.sendEmptyMessage(MSG_INIT_SAMPLE);
278        mVolumeObserver = new Observer(mHandler);
279        mContext.getContentResolver().registerContentObserver(
280                System.getUriFor(System.VOLUME_SETTINGS[mStreamType]),
281                false, mVolumeObserver);
282        mReceiver.setListening(true);
283    }
284
285    public void revertVolume() {
286        mAudioManager.setStreamVolume(mStreamType, mOriginalStreamVolume, 0);
287    }
288
289    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) {
290        if (fromTouch) {
291            postSetVolume(progress);
292        }
293        if (mCallback != null) {
294            mCallback.onProgressChanged(seekBar, progress, fromTouch);
295        }
296    }
297
298    private void postSetVolume(int progress) {
299        if (mHandler == null) return;
300        // Do the volume changing separately to give responsive UI
301        mLastProgress = progress;
302        mHandler.removeMessages(MSG_SET_STREAM_VOLUME);
303        mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_STREAM_VOLUME));
304    }
305
306    public void onStartTrackingTouch(SeekBar seekBar) {
307    }
308
309    public void onStopTrackingTouch(SeekBar seekBar) {
310        postStartSample();
311    }
312
313    public boolean isSamplePlaying() {
314        synchronized (this) {
315            return mRingtone != null && mRingtone.isPlaying();
316        }
317    }
318
319    public void startSample() {
320        postStartSample();
321    }
322
323    public void stopSample() {
324        postStopSample();
325    }
326
327    public SeekBar getSeekBar() {
328        return mSeekBar;
329    }
330
331    public void changeVolumeBy(int amount) {
332        mSeekBar.incrementProgressBy(amount);
333        postSetVolume(mSeekBar.getProgress());
334        postStartSample();
335        mVolumeBeforeMute = -1;
336    }
337
338    public void muteVolume() {
339        if (mVolumeBeforeMute != -1) {
340            mSeekBar.setProgress(mVolumeBeforeMute, true);
341            postSetVolume(mVolumeBeforeMute);
342            postStartSample();
343            mVolumeBeforeMute = -1;
344        } else {
345            mVolumeBeforeMute = mSeekBar.getProgress();
346            mSeekBar.setProgress(0, true);
347            postStopSample();
348            postSetVolume(0);
349        }
350    }
351
352    public void onSaveInstanceState(VolumeStore volumeStore) {
353        if (mLastProgress >= 0) {
354            volumeStore.volume = mLastProgress;
355            volumeStore.originalVolume = mOriginalStreamVolume;
356        }
357    }
358
359    public void onRestoreInstanceState(VolumeStore volumeStore) {
360        if (volumeStore.volume != -1) {
361            mOriginalStreamVolume = volumeStore.originalVolume;
362            mLastProgress = volumeStore.volume;
363            postSetVolume(mLastProgress);
364        }
365    }
366
367    private final class H extends Handler {
368        private static final int UPDATE_SLIDER = 1;
369
370        @Override
371        public void handleMessage(Message msg) {
372            if (msg.what == UPDATE_SLIDER) {
373                if (mSeekBar != null) {
374                    mLastProgress = msg.arg1;
375                    mLastAudibleStreamVolume = msg.arg2;
376                    final boolean muted = ((Boolean)msg.obj).booleanValue();
377                    if (muted != mMuted) {
378                        mMuted = muted;
379                        if (mCallback != null) {
380                            mCallback.onMuted(mMuted, isZenMuted());
381                        }
382                    }
383                    updateSeekBar();
384                }
385            }
386        }
387
388        public void postUpdateSlider(int volume, int lastAudibleVolume, boolean mute) {
389            obtainMessage(UPDATE_SLIDER, volume, lastAudibleVolume, new Boolean(mute)).sendToTarget();
390        }
391    }
392
393    private void updateSlider() {
394        if (mSeekBar != null && mAudioManager != null) {
395            final int volume = mAudioManager.getStreamVolume(mStreamType);
396            final int lastAudibleVolume = mAudioManager.getLastAudibleStreamVolume(mStreamType);
397            final boolean mute = mAudioManager.isStreamMute(mStreamType);
398            mUiHandler.postUpdateSlider(volume, lastAudibleVolume, mute);
399        }
400    }
401
402    private final class Observer extends ContentObserver {
403        public Observer(Handler handler) {
404            super(handler);
405        }
406
407        @Override
408        public void onChange(boolean selfChange) {
409            super.onChange(selfChange);
410            updateSlider();
411        }
412    }
413
414    private final class Receiver extends BroadcastReceiver {
415        private boolean mListening;
416
417        public void setListening(boolean listening) {
418            if (mListening == listening) return;
419            mListening = listening;
420            if (listening) {
421                final IntentFilter filter = new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION);
422                filter.addAction(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION);
423                filter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED);
424                filter.addAction(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED);
425                filter.addAction(AudioManager.STREAM_DEVICES_CHANGED_ACTION);
426                mContext.registerReceiver(this, filter);
427            } else {
428                mContext.unregisterReceiver(this);
429            }
430        }
431
432        @Override
433        public void onReceive(Context context, Intent intent) {
434            final String action = intent.getAction();
435            if (AudioManager.VOLUME_CHANGED_ACTION.equals(action)) {
436                int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
437                int streamValue = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, -1);
438                updateVolumeSlider(streamType, streamValue);
439            } else if (AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION.equals(action)) {
440                if (mNotificationOrRing) {
441                    mRingerMode = mAudioManager.getRingerModeInternal();
442                }
443                if (mAffectedByRingerMode) {
444                    updateSlider();
445                }
446            } else if (AudioManager.STREAM_DEVICES_CHANGED_ACTION.equals(action)) {
447                int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
448                int streamVolume = mAudioManager.getStreamVolume(streamType);
449                updateVolumeSlider(streamType, streamVolume);
450            } else if (NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED.equals(action)) {
451                mZenMode = mNotificationManager.getZenMode();
452                updateSlider();
453            } else if (NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED.equals(action)) {
454                mNotificationPolicy = mNotificationManager.getNotificationPolicy();
455                mAllowAlarms = (mNotificationPolicy.priorityCategories & NotificationManager.Policy
456                        .PRIORITY_CATEGORY_ALARMS) != 0;
457                mAllowMedia = (mNotificationPolicy.priorityCategories
458                        & NotificationManager.Policy.PRIORITY_CATEGORY_MEDIA) != 0;
459                mAllowRinger = !ZenModeConfig.areAllPriorityOnlyNotificationZenSoundsMuted(
460                        mNotificationPolicy);
461                updateSlider();
462            }
463        }
464
465        private void updateVolumeSlider(int streamType, int streamValue) {
466            final boolean streamMatch = mNotificationOrRing ? isNotificationOrRing(streamType)
467                    : (streamType == mStreamType);
468            if (mSeekBar != null && streamMatch && streamValue != -1) {
469                final boolean muted = mAudioManager.isStreamMute(mStreamType)
470                        || streamValue == 0;
471                mUiHandler.postUpdateSlider(streamValue, mLastAudibleStreamVolume, muted);
472            }
473        }
474    }
475}
476