blob: 0cdad9f8de9344ab619c1f4628730251843b3c6d [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.preference;
import android.annotation.NonNull;
import android.app.NotificationManager;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.ContentObserver;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.media.audiopolicy.AudioProductStrategy;
import android.media.audiopolicy.AudioVolumeGroup;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.preference.VolumePreference.VolumeStore;
import android.provider.Settings;
import android.provider.Settings.Global;
import android.provider.Settings.System;
import android.service.notification.ZenModeConfig;
import android.util.Log;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.SomeArgs;
/**
* Turns a {@link SeekBar} into a volume control.
* @hide
*
* @deprecated Use the <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
* <a href="{@docRoot}reference/androidx/preference/package-summary.html">
* Preference Library</a> for consistent behavior across all devices. For more information on
* using the AndroidX Preference Library see
* <a href="{@docRoot}guide/topics/ui/settings.html">Settings</a>.
*/
@Deprecated
public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callback {
private static final String TAG = "SeekBarVolumizer";
public interface Callback {
void onSampleStarting(SeekBarVolumizer sbv);
void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch);
void onMuted(boolean muted, boolean zenMuted);
}
private static final int MSG_GROUP_VOLUME_CHANGED = 1;
private final Handler mVolumeHandler = new VolumeHandler();
private AudioAttributes mAttributes;
private int mVolumeGroupId;
private final AudioManager.VolumeGroupCallback mVolumeGroupCallback =
new AudioManager.VolumeGroupCallback() {
@Override
public void onAudioVolumeGroupChanged(int group, int flags) {
if (mHandler == null) {
return;
}
SomeArgs args = SomeArgs.obtain();
args.arg1 = group;
args.arg2 = flags;
mVolumeHandler.sendMessage(mHandler.obtainMessage(MSG_GROUP_VOLUME_CHANGED, args));
}
};
@UnsupportedAppUsage
private final Context mContext;
private final H mUiHandler = new H();
private final Callback mCallback;
private final Uri mDefaultUri;
@UnsupportedAppUsage
private final AudioManager mAudioManager;
private final NotificationManager mNotificationManager;
@UnsupportedAppUsage
private final int mStreamType;
private final int mMaxStreamVolume;
private boolean mAffectedByRingerMode;
private boolean mNotificationOrRing;
private final Receiver mReceiver = new Receiver();
private Handler mHandler;
private Observer mVolumeObserver;
@UnsupportedAppUsage
private int mOriginalStreamVolume;
private int mLastAudibleStreamVolume;
// When the old handler is destroyed and a new one is created, there could be a situation where
// this is accessed at the same time in different handlers. So, access to this field needs to be
// synchronized.
@GuardedBy("this")
@UnsupportedAppUsage
private Ringtone mRingtone;
@UnsupportedAppUsage
private int mLastProgress = -1;
private boolean mMuted;
@UnsupportedAppUsage
private SeekBar mSeekBar;
private int mVolumeBeforeMute = -1;
private int mRingerMode;
private int mZenMode;
private boolean mPlaySample;
private static final int MSG_SET_STREAM_VOLUME = 0;
private static final int MSG_START_SAMPLE = 1;
private static final int MSG_STOP_SAMPLE = 2;
private static final int MSG_INIT_SAMPLE = 3;
private static final int CHECK_RINGTONE_PLAYBACK_DELAY_MS = 1000;
private NotificationManager.Policy mNotificationPolicy;
private boolean mAllowAlarms;
private boolean mAllowMedia;
private boolean mAllowRinger;
@UnsupportedAppUsage
public SeekBarVolumizer(Context context, int streamType, Uri defaultUri, Callback callback) {
this(context, streamType, defaultUri, callback, true /* playSample */);
}
public SeekBarVolumizer(
Context context,
int streamType,
Uri defaultUri,
Callback callback,
boolean playSample) {
mContext = context;
mAudioManager = context.getSystemService(AudioManager.class);
mNotificationManager = context.getSystemService(NotificationManager.class);
mNotificationPolicy = mNotificationManager.getConsolidatedNotificationPolicy();
mAllowAlarms = (mNotificationPolicy.priorityCategories & NotificationManager.Policy
.PRIORITY_CATEGORY_ALARMS) != 0;
mAllowMedia = (mNotificationPolicy.priorityCategories & NotificationManager.Policy
.PRIORITY_CATEGORY_MEDIA) != 0;
mAllowRinger = !ZenModeConfig.areAllPriorityOnlyRingerSoundsMuted(
mNotificationPolicy);
mStreamType = streamType;
mAffectedByRingerMode = mAudioManager.isStreamAffectedByRingerMode(mStreamType);
mNotificationOrRing = isNotificationOrRing(mStreamType);
if (mNotificationOrRing) {
mRingerMode = mAudioManager.getRingerModeInternal();
}
mZenMode = mNotificationManager.getZenMode();
if (hasAudioProductStrategies()) {
mVolumeGroupId = getVolumeGroupIdForLegacyStreamType(mStreamType);
mAttributes = getAudioAttributesForLegacyStreamType(
mStreamType);
}
mMaxStreamVolume = mAudioManager.getStreamMaxVolume(mStreamType);
mCallback = callback;
mOriginalStreamVolume = mAudioManager.getStreamVolume(mStreamType);
mLastAudibleStreamVolume = mAudioManager.getLastAudibleStreamVolume(mStreamType);
mMuted = mAudioManager.isStreamMute(mStreamType);
mPlaySample = playSample;
if (mCallback != null) {
mCallback.onMuted(mMuted, isZenMuted());
}
if (defaultUri == null) {
if (mStreamType == AudioManager.STREAM_RING) {
defaultUri = Settings.System.DEFAULT_RINGTONE_URI;
} else if (mStreamType == AudioManager.STREAM_NOTIFICATION) {
defaultUri = Settings.System.DEFAULT_NOTIFICATION_URI;
} else {
defaultUri = Settings.System.DEFAULT_ALARM_ALERT_URI;
}
}
mDefaultUri = defaultUri;
}
private boolean hasAudioProductStrategies() {
return AudioManager.getAudioProductStrategies().size() > 0;
}
private int getVolumeGroupIdForLegacyStreamType(int streamType) {
for (final AudioProductStrategy productStrategy :
AudioManager.getAudioProductStrategies()) {
int volumeGroupId = productStrategy.getVolumeGroupIdForLegacyStreamType(streamType);
if (volumeGroupId != AudioVolumeGroup.DEFAULT_VOLUME_GROUP) {
return volumeGroupId;
}
}
return AudioManager.getAudioProductStrategies().stream()
.map(strategy -> strategy.getVolumeGroupIdForAudioAttributes(
AudioProductStrategy.sDefaultAttributes))
.filter(volumeGroupId -> volumeGroupId != AudioVolumeGroup.DEFAULT_VOLUME_GROUP)
.findFirst()
.orElse(AudioVolumeGroup.DEFAULT_VOLUME_GROUP);
}
private @NonNull AudioAttributes getAudioAttributesForLegacyStreamType(int streamType) {
for (final AudioProductStrategy productStrategy :
AudioManager.getAudioProductStrategies()) {
AudioAttributes aa = productStrategy.getAudioAttributesForLegacyStreamType(streamType);
if (aa != null) {
return aa;
}
}
return new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
.setUsage(AudioAttributes.USAGE_UNKNOWN).build();
}
private static boolean isNotificationOrRing(int stream) {
return stream == AudioManager.STREAM_RING || stream == AudioManager.STREAM_NOTIFICATION;
}
private static boolean isAlarmsStream(int stream) {
return stream == AudioManager.STREAM_ALARM;
}
private static boolean isMediaStream(int stream) {
return stream == AudioManager.STREAM_MUSIC;
}
public void setSeekBar(SeekBar seekBar) {
if (mSeekBar != null) {
mSeekBar.setOnSeekBarChangeListener(null);
}
mSeekBar = seekBar;
mSeekBar.setOnSeekBarChangeListener(null);
mSeekBar.setMax(mMaxStreamVolume);
updateSeekBar();
mSeekBar.setOnSeekBarChangeListener(this);
}
private boolean isZenMuted() {
return mNotificationOrRing && mZenMode == Global.ZEN_MODE_ALARMS
|| mZenMode == Global.ZEN_MODE_NO_INTERRUPTIONS
|| (mZenMode == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
&& ((!mAllowAlarms && isAlarmsStream(mStreamType))
|| (!mAllowMedia && isMediaStream(mStreamType))
|| (!mAllowRinger && isNotificationOrRing(mStreamType))));
}
protected void updateSeekBar() {
final boolean zenMuted = isZenMuted();
mSeekBar.setEnabled(!zenMuted);
if (zenMuted) {
mSeekBar.setProgress(mLastAudibleStreamVolume, true);
} else if (mNotificationOrRing && mRingerMode == AudioManager.RINGER_MODE_VIBRATE) {
mSeekBar.setProgress(0, true);
} else if (mMuted) {
mSeekBar.setProgress(0, true);
} else {
mSeekBar.setProgress(mLastProgress > -1 ? mLastProgress : mOriginalStreamVolume, true);
}
}
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_SET_STREAM_VOLUME:
if (mMuted && mLastProgress > 0) {
mAudioManager.adjustStreamVolume(mStreamType, AudioManager.ADJUST_UNMUTE, 0);
} else if (!mMuted && mLastProgress == 0) {
mAudioManager.adjustStreamVolume(mStreamType, AudioManager.ADJUST_MUTE, 0);
}
mAudioManager.setStreamVolume(mStreamType, mLastProgress,
AudioManager.FLAG_SHOW_UI_WARNINGS);
break;
case MSG_START_SAMPLE:
if (mPlaySample) {
onStartSample();
}
break;
case MSG_STOP_SAMPLE:
if (mPlaySample) {
onStopSample();
}
break;
case MSG_INIT_SAMPLE:
if (mPlaySample) {
onInitSample();
}
break;
default:
Log.e(TAG, "invalid SeekBarVolumizer message: "+msg.what);
}
return true;
}
private void onInitSample() {
synchronized (this) {
mRingtone = RingtoneManager.getRingtone(mContext, mDefaultUri);
if (mRingtone != null) {
mRingtone.setStreamType(mStreamType);
}
}
}
private void postStartSample() {
if (mHandler == null) return;
mHandler.removeMessages(MSG_START_SAMPLE);
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_START_SAMPLE),
isSamplePlaying() ? CHECK_RINGTONE_PLAYBACK_DELAY_MS : 0);
}
private void onStartSample() {
if (!isSamplePlaying()) {
if (mCallback != null) {
mCallback.onSampleStarting(this);
}
synchronized (this) {
if (mRingtone != null) {
try {
mRingtone.setAudioAttributes(new AudioAttributes.Builder(mRingtone
.getAudioAttributes())
.setFlags(AudioAttributes.FLAG_BYPASS_MUTE)
.build());
mRingtone.play();
} catch (Throwable e) {
Log.w(TAG, "Error playing ringtone, stream " + mStreamType, e);
}
}
}
}
}
private void postStopSample() {
if (mHandler == null) return;
// remove pending delayed start messages
mHandler.removeMessages(MSG_START_SAMPLE);
mHandler.removeMessages(MSG_STOP_SAMPLE);
mHandler.sendMessage(mHandler.obtainMessage(MSG_STOP_SAMPLE));
}
private void onStopSample() {
synchronized (this) {
if (mRingtone != null) {
mRingtone.stop();
}
}
}
@UnsupportedAppUsage
public void stop() {
if (mHandler == null) return; // already stopped
postStopSample();
mContext.getContentResolver().unregisterContentObserver(mVolumeObserver);
mReceiver.setListening(false);
if (hasAudioProductStrategies()) {
unregisterVolumeGroupCb();
}
mSeekBar.setOnSeekBarChangeListener(null);
mHandler.getLooper().quitSafely();
mHandler = null;
mVolumeObserver = null;
}
public void start() {
if (mHandler != null) return; // already started
HandlerThread thread = new HandlerThread(TAG + ".CallbackHandler");
thread.start();
mHandler = new Handler(thread.getLooper(), this);
mHandler.sendEmptyMessage(MSG_INIT_SAMPLE);
mVolumeObserver = new Observer(mHandler);
mContext.getContentResolver().registerContentObserver(
System.getUriFor(System.VOLUME_SETTINGS_INT[mStreamType]),
false, mVolumeObserver);
mReceiver.setListening(true);
if (hasAudioProductStrategies()) {
registerVolumeGroupCb();
}
}
public void revertVolume() {
mAudioManager.setStreamVolume(mStreamType, mOriginalStreamVolume, 0);
}
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) {
if (fromTouch) {
postSetVolume(progress);
}
if (mCallback != null) {
mCallback.onProgressChanged(seekBar, progress, fromTouch);
}
}
private void postSetVolume(int progress) {
if (mHandler == null) return;
// Do the volume changing separately to give responsive UI
mLastProgress = progress;
mHandler.removeMessages(MSG_SET_STREAM_VOLUME);
mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_STREAM_VOLUME));
}
public void onStartTrackingTouch(SeekBar seekBar) {
}
public void onStopTrackingTouch(SeekBar seekBar) {
postStartSample();
}
public boolean isSamplePlaying() {
synchronized (this) {
return mRingtone != null && mRingtone.isPlaying();
}
}
public void startSample() {
postStartSample();
}
public void stopSample() {
postStopSample();
}
public SeekBar getSeekBar() {
return mSeekBar;
}
public void changeVolumeBy(int amount) {
mSeekBar.incrementProgressBy(amount);
postSetVolume(mSeekBar.getProgress());
postStartSample();
mVolumeBeforeMute = -1;
}
public void muteVolume() {
if (mVolumeBeforeMute != -1) {
mSeekBar.setProgress(mVolumeBeforeMute, true);
postSetVolume(mVolumeBeforeMute);
postStartSample();
mVolumeBeforeMute = -1;
} else {
mVolumeBeforeMute = mSeekBar.getProgress();
mSeekBar.setProgress(0, true);
postStopSample();
postSetVolume(0);
}
}
public void onSaveInstanceState(VolumeStore volumeStore) {
if (mLastProgress >= 0) {
volumeStore.volume = mLastProgress;
volumeStore.originalVolume = mOriginalStreamVolume;
}
}
public void onRestoreInstanceState(VolumeStore volumeStore) {
if (volumeStore.volume != -1) {
mOriginalStreamVolume = volumeStore.originalVolume;
mLastProgress = volumeStore.volume;
postSetVolume(mLastProgress);
}
}
private final class H extends Handler {
private static final int UPDATE_SLIDER = 1;
@Override
public void handleMessage(Message msg) {
if (msg.what == UPDATE_SLIDER) {
if (mSeekBar != null) {
mLastProgress = msg.arg1;
mLastAudibleStreamVolume = msg.arg2;
final boolean muted = ((Boolean)msg.obj).booleanValue();
if (muted != mMuted) {
mMuted = muted;
if (mCallback != null) {
mCallback.onMuted(mMuted, isZenMuted());
}
}
updateSeekBar();
}
}
}
public void postUpdateSlider(int volume, int lastAudibleVolume, boolean mute) {
obtainMessage(UPDATE_SLIDER, volume, lastAudibleVolume, new Boolean(mute)).sendToTarget();
}
}
private void updateSlider() {
if (mSeekBar != null && mAudioManager != null) {
final int volume = mAudioManager.getStreamVolume(mStreamType);
final int lastAudibleVolume = mAudioManager.getLastAudibleStreamVolume(mStreamType);
final boolean mute = mAudioManager.isStreamMute(mStreamType);
mUiHandler.postUpdateSlider(volume, lastAudibleVolume, mute);
}
}
private final class Observer extends ContentObserver {
public Observer(Handler handler) {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
updateSlider();
}
}
private final class Receiver extends BroadcastReceiver {
private boolean mListening;
public void setListening(boolean listening) {
if (mListening == listening) return;
mListening = listening;
if (listening) {
final IntentFilter filter = new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION);
filter.addAction(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION);
filter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED);
filter.addAction(NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED);
filter.addAction(AudioManager.STREAM_DEVICES_CHANGED_ACTION);
mContext.registerReceiver(this, filter);
} else {
mContext.unregisterReceiver(this);
}
}
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (AudioManager.VOLUME_CHANGED_ACTION.equals(action)) {
int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
int streamValue = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, -1);
if (hasAudioProductStrategies()) {
updateVolumeSlider(streamType, streamValue);
}
} else if (AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION.equals(action)) {
if (mNotificationOrRing) {
mRingerMode = mAudioManager.getRingerModeInternal();
}
if (mAffectedByRingerMode) {
updateSlider();
}
} else if (AudioManager.STREAM_DEVICES_CHANGED_ACTION.equals(action)) {
int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
if (hasAudioProductStrategies()) {
int streamVolume = mAudioManager.getStreamVolume(streamType);
updateVolumeSlider(streamType, streamVolume);
} else {
int volumeGroup = getVolumeGroupIdForLegacyStreamType(streamType);
if (volumeGroup != AudioVolumeGroup.DEFAULT_VOLUME_GROUP
&& volumeGroup == mVolumeGroupId) {
int streamVolume = mAudioManager.getStreamVolume(streamType);
updateVolumeSlider(streamType, streamVolume);
}
}
} else if (NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED.equals(action)) {
mZenMode = mNotificationManager.getZenMode();
updateSlider();
} else if (NotificationManager.ACTION_NOTIFICATION_POLICY_CHANGED.equals(action)) {
mNotificationPolicy = mNotificationManager.getConsolidatedNotificationPolicy();
mAllowAlarms = (mNotificationPolicy.priorityCategories & NotificationManager.Policy
.PRIORITY_CATEGORY_ALARMS) != 0;
mAllowMedia = (mNotificationPolicy.priorityCategories
& NotificationManager.Policy.PRIORITY_CATEGORY_MEDIA) != 0;
mAllowRinger = !ZenModeConfig.areAllPriorityOnlyRingerSoundsMuted(
mNotificationPolicy);
updateSlider();
}
}
private void updateVolumeSlider(int streamType, int streamValue) {
final boolean streamMatch = mNotificationOrRing ? isNotificationOrRing(streamType)
: (streamType == mStreamType);
if (mSeekBar != null && streamMatch && streamValue != -1) {
final boolean muted = mAudioManager.isStreamMute(mStreamType)
|| streamValue == 0;
mUiHandler.postUpdateSlider(streamValue, mLastAudibleStreamVolume, muted);
}
}
}
private void registerVolumeGroupCb() {
if (mVolumeGroupId != AudioVolumeGroup.DEFAULT_VOLUME_GROUP) {
mAudioManager.registerVolumeGroupCallback(Runnable::run, mVolumeGroupCallback);
updateSlider();
}
}
private void unregisterVolumeGroupCb() {
if (mVolumeGroupId != AudioVolumeGroup.DEFAULT_VOLUME_GROUP) {
mAudioManager.unregisterVolumeGroupCallback(mVolumeGroupCallback);
}
}
private class VolumeHandler extends Handler {
@Override
public void handleMessage(Message msg) {
SomeArgs args = (SomeArgs) msg.obj;
switch (msg.what) {
case MSG_GROUP_VOLUME_CHANGED:
int group = (int) args.arg1;
if (mVolumeGroupId != group
|| mVolumeGroupId == AudioVolumeGroup.DEFAULT_VOLUME_GROUP) {
return;
}
updateSlider();
break;
}
}
}
}