blob: 2156c05a8ba8603c1ca2c14a8a97f5a1bbad1574 [file] [log] [blame]
John Spurlock81bef1d72014-05-16 15:50:13 -04001/*
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 com.android.settings.notification;
18
Chaohui Wangf3115662022-04-26 20:04:39 +080019import static com.android.internal.jank.InteractionJankMonitor.CUJ_SETTINGS_SLIDER;
20
John Spurlock81bef1d72014-05-16 15:50:13 -040021import android.content.ContentResolver;
22import android.content.Context;
23import android.media.AudioManager;
24import android.net.Uri;
John Spurlock81bef1d72014-05-16 15:50:13 -040025import android.preference.SeekBarVolumizer;
John Spurlockfdebfc32014-11-14 14:32:01 -050026import android.text.TextUtils;
John Spurlock81bef1d72014-05-16 15:50:13 -040027import android.util.AttributeSet;
John Spurlock81bef1d72014-05-16 15:50:13 -040028import android.view.View;
Dan Sandlercb9f3512014-09-02 01:05:27 +020029import android.widget.ImageView;
John Spurlock81bef1d72014-05-16 15:50:13 -040030import android.widget.SeekBar;
John Spurlockfdebfc32014-11-14 14:32:01 -050031import android.widget.TextView;
John Spurlock81bef1d72014-05-16 15:50:13 -040032
Fan Zhang23f8d592018-08-28 15:11:40 -070033import androidx.annotation.VisibleForTesting;
34import androidx.preference.PreferenceViewHolder;
35
Chaohui Wangf3115662022-04-26 20:04:39 +080036import com.android.internal.jank.InteractionJankMonitor;
John Spurlock81bef1d72014-05-16 15:50:13 -040037import com.android.settings.R;
Edgar Wangcdff5c62021-06-08 09:10:41 +000038import com.android.settings.widget.SeekBarPreference;
John Spurlock81bef1d72014-05-16 15:50:13 -040039
Michael Mikhailb222b962023-07-07 18:37:45 +000040import java.text.NumberFormat;
41import java.util.Locale;
John Spurlockfdebfc32014-11-14 14:32:01 -050042import java.util.Objects;
43
John Spurlock81bef1d72014-05-16 15:50:13 -040044/** A slider preference that directly controls an audio stream volume (no dialog) **/
Edgar Wangcdff5c62021-06-08 09:10:41 +000045public class VolumeSeekBarPreference extends SeekBarPreference {
John Spurlock81bef1d72014-05-16 15:50:13 -040046 private static final String TAG = "VolumeSeekBarPreference";
John Spurlock81bef1d72014-05-16 15:50:13 -040047
Chaohui Wangf3115662022-04-26 20:04:39 +080048 private final InteractionJankMonitor mJankMonitor = InteractionJankMonitor.getInstance();
49
jackqdyuleif25c9b02019-02-27 10:05:56 -080050 protected SeekBar mSeekBar;
John Spurlock81bef1d72014-05-16 15:50:13 -040051 private int mStream;
Michael Mikhailb222b962023-07-07 18:37:45 +000052 private SeekBarVolumizer mVolumizer;
Michael Mikhail2f393302023-06-21 16:58:20 +000053 @VisibleForTesting
Michael Mikhailb222b962023-07-07 18:37:45 +000054 SeekBarVolumizerFactory mSeekBarVolumizerFactory;
John Spurlock81bef1d72014-05-16 15:50:13 -040055 private Callback mCallback;
Michael Mikhail2f393302023-06-21 16:58:20 +000056 private Listener mListener;
Dan Sandlercb9f3512014-09-02 01:05:27 +020057 private ImageView mIconView;
John Spurlockfdebfc32014-11-14 14:32:01 -050058 private TextView mSuppressionTextView;
Michael Mikhail2f393302023-06-21 16:58:20 +000059 private TextView mTitle;
John Spurlockfdebfc32014-11-14 14:32:01 -050060 private String mSuppressionText;
John Spurlockc868f702014-11-28 15:39:11 -050061 private boolean mMuted;
John Spurlockf82154d2015-06-10 17:46:58 -040062 private boolean mZenMuted;
John Spurlockc868f702014-11-28 15:39:11 -050063 private int mIconResId;
64 private int mMuteIconResId;
John Spurlockaaaa54e2014-12-08 15:20:33 -050065 private boolean mStopped;
Doris Lingb7490be2017-08-08 16:52:42 -070066 @VisibleForTesting
67 AudioManager mAudioManager;
Michael Mikhailb222b962023-07-07 18:37:45 +000068 private Locale mLocale;
69 private NumberFormat mNumberFormat;
John Spurlock81bef1d72014-05-16 15:50:13 -040070
Fabrice Di Megliodbaf0802014-06-17 11:37:55 -070071 public VolumeSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr,
72 int defStyleRes) {
73 super(context, attrs, defStyleAttr, defStyleRes);
John Spurlockfdebfc32014-11-14 14:32:01 -050074 setLayoutResource(R.layout.preference_volume_slider);
Doris Lingb7490be2017-08-08 16:52:42 -070075 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
Michael Mikhailb222b962023-07-07 18:37:45 +000076 mSeekBarVolumizerFactory = new SeekBarVolumizerFactory(context);
Fabrice Di Megliodbaf0802014-06-17 11:37:55 -070077 }
78
79 public VolumeSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr) {
Fan Zhangbfb006a2017-01-25 15:32:17 -080080 super(context, attrs, defStyleAttr);
81 setLayoutResource(R.layout.preference_volume_slider);
Doris Lingb7490be2017-08-08 16:52:42 -070082 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
Michael Mikhailb222b962023-07-07 18:37:45 +000083 mSeekBarVolumizerFactory = new SeekBarVolumizerFactory(context);
Fabrice Di Megliodbaf0802014-06-17 11:37:55 -070084 }
85
John Spurlock81bef1d72014-05-16 15:50:13 -040086 public VolumeSeekBarPreference(Context context, AttributeSet attrs) {
Fan Zhangbfb006a2017-01-25 15:32:17 -080087 super(context, attrs);
88 setLayoutResource(R.layout.preference_volume_slider);
Doris Lingb7490be2017-08-08 16:52:42 -070089 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
Michael Mikhailb222b962023-07-07 18:37:45 +000090 mSeekBarVolumizerFactory = new SeekBarVolumizerFactory(context);
Fabrice Di Megliodbaf0802014-06-17 11:37:55 -070091 }
92
93 public VolumeSeekBarPreference(Context context) {
Fan Zhangbfb006a2017-01-25 15:32:17 -080094 super(context);
95 setLayoutResource(R.layout.preference_volume_slider);
Doris Lingb7490be2017-08-08 16:52:42 -070096 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
Michael Mikhailb222b962023-07-07 18:37:45 +000097 mSeekBarVolumizerFactory = new SeekBarVolumizerFactory(context);
John Spurlock81bef1d72014-05-16 15:50:13 -040098 }
99
100 public void setStream(int stream) {
101 mStream = stream;
Doris Lingb7490be2017-08-08 16:52:42 -0700102 setMax(mAudioManager.getStreamMaxVolume(mStream));
ryanywlin156fd912018-04-25 10:56:36 +0800103 // Use getStreamMinVolumeInt for non-public stream type
104 // eg: AudioManager.STREAM_BLUETOOTH_SCO
105 setMin(mAudioManager.getStreamMinVolumeInt(mStream));
Doris Lingb7490be2017-08-08 16:52:42 -0700106 setProgress(mAudioManager.getStreamVolume(mStream));
John Spurlock81bef1d72014-05-16 15:50:13 -0400107 }
108
109 public void setCallback(Callback callback) {
110 mCallback = callback;
111 }
112
Michael Mikhail2f393302023-06-21 16:58:20 +0000113 public void setListener(Listener listener) {
114 mListener = listener;
115 }
116
John Spurlockaaaa54e2014-12-08 15:20:33 -0500117 public void onActivityResume() {
118 if (mStopped) {
119 init();
120 }
121 }
122
Jason Monk39b46742015-09-10 15:52:51 -0400123 public void onActivityPause() {
John Spurlockaaaa54e2014-12-08 15:20:33 -0500124 mStopped = true;
John Spurlock81bef1d72014-05-16 15:50:13 -0400125 if (mVolumizer != null) {
126 mVolumizer.stop();
Sheng Wang176ee342016-08-19 17:09:32 +0800127 mVolumizer = null;
John Spurlock81bef1d72014-05-16 15:50:13 -0400128 }
129 }
Sheng Wang176ee342016-08-19 17:09:32 +0800130
John Spurlock81bef1d72014-05-16 15:50:13 -0400131 @Override
Jason Monk39b46742015-09-10 15:52:51 -0400132 public void onBindViewHolder(PreferenceViewHolder view) {
133 super.onBindViewHolder(view);
John Spurlockaaaa54e2014-12-08 15:20:33 -0500134 mSeekBar = (SeekBar) view.findViewById(com.android.internal.R.id.seekbar);
135 mIconView = (ImageView) view.findViewById(com.android.internal.R.id.icon);
136 mSuppressionTextView = (TextView) view.findViewById(R.id.suppression_text);
Michael Mikhail2f393302023-06-21 16:58:20 +0000137 mTitle = (TextView) view.findViewById(com.android.internal.R.id.title);
John Spurlockaaaa54e2014-12-08 15:20:33 -0500138 init();
139 }
140
jackqdyuleif25c9b02019-02-27 10:05:56 -0800141 protected void init() {
John Spurlockaaaa54e2014-12-08 15:20:33 -0500142 if (mSeekBar == null) return;
Mill Chenc69899c2023-07-07 20:52:13 +0800143 // It's unnecessary to set up relevant volumizer configuration if preference is disabled.
144 if (!isEnabled()) {
145 mSeekBar.setEnabled(false);
146 return;
147 }
John Spurlock81bef1d72014-05-16 15:50:13 -0400148 final SeekBarVolumizer.Callback sbvc = new SeekBarVolumizer.Callback() {
149 @Override
150 public void onSampleStarting(SeekBarVolumizer sbv) {
151 if (mCallback != null) {
152 mCallback.onSampleStarting(sbv);
153 }
154 }
John Spurlockc868f702014-11-28 15:39:11 -0500155 @Override
156 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) {
157 if (mCallback != null) {
158 mCallback.onStreamValueChanged(mStream, progress);
159 }
Michael Mikhailb222b962023-07-07 18:37:45 +0000160 overrideSeekBarStateDescription(formatStateDescription(progress));
John Spurlockc868f702014-11-28 15:39:11 -0500161 }
162 @Override
John Spurlockf82154d2015-06-10 17:46:58 -0400163 public void onMuted(boolean muted, boolean zenMuted) {
164 if (mMuted == muted && mZenMuted == zenMuted) return;
John Spurlockc868f702014-11-28 15:39:11 -0500165 mMuted = muted;
John Spurlockf82154d2015-06-10 17:46:58 -0400166 mZenMuted = zenMuted;
John Spurlockc868f702014-11-28 15:39:11 -0500167 updateIconView();
Michael Mikhail2f393302023-06-21 16:58:20 +0000168 if (mListener != null) {
169 mListener.onUpdateMuteState();
170 }
John Spurlockc868f702014-11-28 15:39:11 -0500171 }
Hugh Chen432d0732020-08-26 14:32:53 +0800172 @Override
173 public void onStartTrackingTouch(SeekBarVolumizer sbv) {
174 if (mCallback != null) {
175 mCallback.onStartTrackingTouch(sbv);
176 }
Chaohui Wangf3115662022-04-26 20:04:39 +0800177 mJankMonitor.begin(InteractionJankMonitor.Configuration.Builder
178 .withView(CUJ_SETTINGS_SLIDER, mSeekBar)
179 .setTag(getKey()));
180 }
181 @Override
182 public void onStopTrackingTouch(SeekBarVolumizer sbv) {
183 mJankMonitor.end(CUJ_SETTINGS_SLIDER);
Hugh Chen432d0732020-08-26 14:32:53 +0800184 }
John Spurlock81bef1d72014-05-16 15:50:13 -0400185 };
186 final Uri sampleUri = mStream == AudioManager.STREAM_MUSIC ? getMediaVolumeUri() : null;
John Spurlock85315532014-05-27 13:12:35 -0400187 if (mVolumizer == null) {
Michael Mikhailb222b962023-07-07 18:37:45 +0000188 mVolumizer = mSeekBarVolumizerFactory.create(mStream, sampleUri, sbvc);
John Spurlock85315532014-05-27 13:12:35 -0400189 }
John Spurlockaf05fda2014-10-08 12:34:50 -0400190 mVolumizer.start();
John Spurlock85315532014-05-27 13:12:35 -0400191 mVolumizer.setSeekBar(mSeekBar);
John Spurlockc868f702014-11-28 15:39:11 -0500192 updateIconView();
John Spurlockfdebfc32014-11-14 14:32:01 -0500193 updateSuppressionText();
Michael Mikhail2f393302023-06-21 16:58:20 +0000194 if (mListener != null) {
195 mListener.onUpdateMuteState();
196 }
Dan Sandlercb9f3512014-09-02 01:05:27 +0200197 }
198
jackqdyuleif25c9b02019-02-27 10:05:56 -0800199 protected void updateIconView() {
John Spurlockc868f702014-11-28 15:39:11 -0500200 if (mIconView == null) return;
201 if (mIconResId != 0) {
202 mIconView.setImageResource(mIconResId);
Michael Mikhail2f393302023-06-21 16:58:20 +0000203 } else if (mMuteIconResId != 0 && isMuted()) {
John Spurlockc868f702014-11-28 15:39:11 -0500204 mIconView.setImageResource(mMuteIconResId);
205 } else {
206 mIconView.setImageDrawable(getIcon());
207 }
208 }
209
Dan Sandlercb9f3512014-09-02 01:05:27 +0200210 public void showIcon(int resId) {
211 // Instead of using setIcon, which will trigger listeners, this just decorates the
212 // preference temporarily with a new icon.
John Spurlockc868f702014-11-28 15:39:11 -0500213 if (mIconResId == resId) return;
214 mIconResId = resId;
215 updateIconView();
216 }
217
218 public void setMuteIcon(int resId) {
219 if (mMuteIconResId == resId) return;
220 mMuteIconResId = resId;
221 updateIconView();
John Spurlock81bef1d72014-05-16 15:50:13 -0400222 }
223
224 private Uri getMediaVolumeUri() {
225 return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
Fabrice Di Megliodbaf0802014-06-17 11:37:55 -0700226 + getContext().getPackageName()
John Spurlock81bef1d72014-05-16 15:50:13 -0400227 + "/" + R.raw.media_volume);
228 }
229
Michael Mikhailb222b962023-07-07 18:37:45 +0000230 @VisibleForTesting
231 CharSequence formatStateDescription(int progress) {
232 // This code follows the same approach in ProgressBar.java, but it rounds down the percent
233 // to match it with what the talkback feature says after any progress change. (b/285458191)
234 // Cache the locale-appropriate NumberFormat. Configuration locale is guaranteed
235 // non-null, so the first time this is called we will always get the appropriate
236 // NumberFormat, then never regenerate it unless the locale changes on the fly.
237 Locale curLocale = getContext().getResources().getConfiguration().getLocales().get(0);
238 if (mLocale == null || !mLocale.equals(curLocale)) {
239 mLocale = curLocale;
240 mNumberFormat = NumberFormat.getPercentInstance(mLocale);
241 }
242 return mNumberFormat.format(getPercent(progress));
243 }
244
245 @VisibleForTesting
246 double getPercent(float progress) {
247 final float maxProgress = getMax();
248 final float minProgress = getMin();
249 final float diffProgress = maxProgress - minProgress;
250 if (diffProgress <= 0.0f) {
251 return 0.0f;
252 }
253 final float percent = (progress - minProgress) / diffProgress;
254 return Math.floor(Math.max(0.0f, Math.min(1.0f, percent)) * 100) / 100;
255 }
256
John Spurlockfdebfc32014-11-14 14:32:01 -0500257 public void setSuppressionText(String text) {
258 if (Objects.equals(text, mSuppressionText)) return;
259 mSuppressionText = text;
260 updateSuppressionText();
261 }
262
Michael Mikhail2f393302023-06-21 16:58:20 +0000263 protected boolean isMuted() {
264 return mMuted && !mZenMuted;
265 }
266
jackqdyuleif25c9b02019-02-27 10:05:56 -0800267 protected void updateSuppressionText() {
John Spurlockfdebfc32014-11-14 14:32:01 -0500268 if (mSuppressionTextView != null && mSeekBar != null) {
269 mSuppressionTextView.setText(mSuppressionText);
270 final boolean showSuppression = !TextUtils.isEmpty(mSuppressionText);
Julia Reynolds0ddba4c2018-04-17 09:11:43 -0400271 mSuppressionTextView.setVisibility(showSuppression ? View.VISIBLE : View.GONE);
John Spurlockfdebfc32014-11-14 14:32:01 -0500272 }
273 }
274
Michael Mikhail2f393302023-06-21 16:58:20 +0000275 /**
276 * Update content description of title to improve talkback announcements.
277 */
278 protected void updateContentDescription(CharSequence contentDescription) {
279 if (mTitle == null) return;
280 mTitle.setContentDescription(contentDescription);
281 }
282
Michael Mikhail7c9958a2023-06-26 20:16:30 +0000283 protected void setAccessibilityLiveRegion(int mode) {
284 if (mTitle == null) return;
285 mTitle.setAccessibilityLiveRegion(mode);
286 }
287
John Spurlock81bef1d72014-05-16 15:50:13 -0400288 public interface Callback {
289 void onSampleStarting(SeekBarVolumizer sbv);
Dan Sandlercb9f3512014-09-02 01:05:27 +0200290 void onStreamValueChanged(int stream, int progress);
Hugh Chen432d0732020-08-26 14:32:53 +0800291
292 /**
293 * Callback reporting that the seek bar is start tracking.
294 */
295 void onStartTrackingTouch(SeekBarVolumizer sbv);
John Spurlock81bef1d72014-05-16 15:50:13 -0400296 }
Michael Mikhail2f393302023-06-21 16:58:20 +0000297
298 /**
299 * Listener to view updates in volumeSeekbarPreference.
300 */
301 public interface Listener {
302
303 /**
304 * Listener to mute state updates.
305 */
306 void onUpdateMuteState();
307 }
John Spurlock81bef1d72014-05-16 15:50:13 -0400308}