• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 com.android.settings.notification;
18 
19 import static com.android.internal.jank.InteractionJankMonitor.CUJ_SETTINGS_SLIDER;
20 
21 import android.content.ContentResolver;
22 import android.content.Context;
23 import android.media.AudioManager;
24 import android.net.Uri;
25 import android.preference.SeekBarVolumizer;
26 import android.text.TextUtils;
27 import android.util.AttributeSet;
28 import android.view.View;
29 import android.widget.ImageView;
30 import android.widget.SeekBar;
31 import android.widget.TextView;
32 
33 import androidx.annotation.VisibleForTesting;
34 import androidx.preference.PreferenceViewHolder;
35 
36 import com.android.internal.jank.InteractionJankMonitor;
37 import com.android.settings.R;
38 import com.android.settings.widget.SeekBarPreference;
39 
40 import java.text.NumberFormat;
41 import java.util.Locale;
42 import java.util.Objects;
43 
44 /** A slider preference that directly controls an audio stream volume (no dialog) **/
45 public class VolumeSeekBarPreference extends SeekBarPreference {
46     private static final String TAG = "VolumeSeekBarPreference";
47 
48     private final InteractionJankMonitor mJankMonitor = InteractionJankMonitor.getInstance();
49 
50     protected SeekBar mSeekBar;
51     private int mStream;
52     private SeekBarVolumizer mVolumizer;
53     @VisibleForTesting
54     SeekBarVolumizerFactory mSeekBarVolumizerFactory;
55     private Callback mCallback;
56     private Listener mListener;
57     private ImageView mIconView;
58     private TextView mSuppressionTextView;
59     private TextView mTitle;
60     private String mSuppressionText;
61     private boolean mMuted;
62     private boolean mZenMuted;
63     private int mIconResId;
64     private int mMuteIconResId;
65     private boolean mStopped;
66     @VisibleForTesting
67     AudioManager mAudioManager;
68     private Locale mLocale;
69     private NumberFormat mNumberFormat;
70 
VolumeSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)71     public VolumeSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr,
72             int defStyleRes) {
73         super(context, attrs, defStyleAttr, defStyleRes);
74         setLayoutResource(R.layout.preference_volume_slider);
75         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
76         mSeekBarVolumizerFactory = new SeekBarVolumizerFactory(context);
77     }
78 
VolumeSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr)79     public VolumeSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr) {
80         super(context, attrs, defStyleAttr);
81         setLayoutResource(R.layout.preference_volume_slider);
82         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
83         mSeekBarVolumizerFactory = new SeekBarVolumizerFactory(context);
84     }
85 
VolumeSeekBarPreference(Context context, AttributeSet attrs)86     public VolumeSeekBarPreference(Context context, AttributeSet attrs) {
87         super(context, attrs);
88         setLayoutResource(R.layout.preference_volume_slider);
89         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
90         mSeekBarVolumizerFactory = new SeekBarVolumizerFactory(context);
91     }
92 
VolumeSeekBarPreference(Context context)93     public VolumeSeekBarPreference(Context context) {
94         super(context);
95         setLayoutResource(R.layout.preference_volume_slider);
96         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
97         mSeekBarVolumizerFactory = new SeekBarVolumizerFactory(context);
98     }
99 
setStream(int stream)100     public void setStream(int stream) {
101         mStream = stream;
102         setMax(mAudioManager.getStreamMaxVolume(mStream));
103         // Use getStreamMinVolumeInt for non-public stream type
104         // eg: AudioManager.STREAM_BLUETOOTH_SCO
105         setMin(mAudioManager.getStreamMinVolumeInt(mStream));
106         setProgress(mAudioManager.getStreamVolume(mStream));
107     }
108 
setCallback(Callback callback)109     public void setCallback(Callback callback) {
110         mCallback = callback;
111     }
112 
setListener(Listener listener)113     public void setListener(Listener listener) {
114         mListener = listener;
115     }
116 
onActivityResume()117     public void onActivityResume() {
118         if (mStopped) {
119             init();
120         }
121     }
122 
onActivityPause()123     public void onActivityPause() {
124         mStopped = true;
125         if (mVolumizer != null) {
126             mVolumizer.stop();
127             mVolumizer = null;
128         }
129     }
130 
131     @Override
onBindViewHolder(PreferenceViewHolder view)132     public void onBindViewHolder(PreferenceViewHolder view) {
133         super.onBindViewHolder(view);
134         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);
137         mTitle = (TextView) view.findViewById(com.android.internal.R.id.title);
138         init();
139     }
140 
init()141     protected void init() {
142         if (mSeekBar == null) return;
143         final SeekBarVolumizer.Callback sbvc = new SeekBarVolumizer.Callback() {
144             @Override
145             public void onSampleStarting(SeekBarVolumizer sbv) {
146                 if (mCallback != null) {
147                     mCallback.onSampleStarting(sbv);
148                 }
149             }
150             @Override
151             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) {
152                 if (mCallback != null) {
153                     mCallback.onStreamValueChanged(mStream, progress);
154                 }
155                 overrideSeekBarStateDescription(formatStateDescription(progress));
156             }
157             @Override
158             public void onMuted(boolean muted, boolean zenMuted) {
159                 if (mMuted == muted && mZenMuted == zenMuted) return;
160                 mMuted = muted;
161                 mZenMuted = zenMuted;
162                 updateIconView();
163                 if (mListener != null) {
164                     mListener.onUpdateMuteState();
165                 }
166             }
167             @Override
168             public void onStartTrackingTouch(SeekBarVolumizer sbv) {
169                 if (mCallback != null) {
170                     mCallback.onStartTrackingTouch(sbv);
171                 }
172                 mJankMonitor.begin(InteractionJankMonitor.Configuration.Builder
173                         .withView(CUJ_SETTINGS_SLIDER, mSeekBar)
174                         .setTag(getKey()));
175             }
176             @Override
177             public void onStopTrackingTouch(SeekBarVolumizer sbv) {
178                 mJankMonitor.end(CUJ_SETTINGS_SLIDER);
179             }
180         };
181         final Uri sampleUri = mStream == AudioManager.STREAM_MUSIC ? getMediaVolumeUri() : null;
182         if (mVolumizer == null) {
183             mVolumizer = mSeekBarVolumizerFactory.create(mStream, sampleUri, sbvc);
184         }
185         mVolumizer.start();
186         mVolumizer.setSeekBar(mSeekBar);
187         updateIconView();
188         updateSuppressionText();
189         if (mListener != null) {
190             mListener.onUpdateMuteState();
191         }
192         if (!isEnabled()) {
193             mSeekBar.setEnabled(false);
194             mVolumizer.stop();
195         }
196     }
197 
updateIconView()198     protected void updateIconView() {
199         if (mIconView == null) return;
200         if (mIconResId != 0) {
201             mIconView.setImageResource(mIconResId);
202         } else if (mMuteIconResId != 0 && isMuted()) {
203             mIconView.setImageResource(mMuteIconResId);
204         } else {
205             mIconView.setImageDrawable(getIcon());
206         }
207     }
208 
showIcon(int resId)209     public void showIcon(int resId) {
210         // Instead of using setIcon, which will trigger listeners, this just decorates the
211         // preference temporarily with a new icon.
212         if (mIconResId == resId) return;
213         mIconResId = resId;
214         updateIconView();
215     }
216 
setMuteIcon(int resId)217     public void setMuteIcon(int resId) {
218         if (mMuteIconResId == resId) return;
219         mMuteIconResId = resId;
220         updateIconView();
221     }
222 
getMediaVolumeUri()223     private Uri getMediaVolumeUri() {
224         return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
225                 + getContext().getPackageName()
226                 + "/" + R.raw.media_volume);
227     }
228 
229     @VisibleForTesting
formatStateDescription(int progress)230     CharSequence formatStateDescription(int progress) {
231         // This code follows the same approach in ProgressBar.java, but it rounds down the percent
232         // to match it with what the talkback feature says after any progress change. (b/285458191)
233         // Cache the locale-appropriate NumberFormat.  Configuration locale is guaranteed
234         // non-null, so the first time this is called we will always get the appropriate
235         // NumberFormat, then never regenerate it unless the locale changes on the fly.
236         Locale curLocale = getContext().getResources().getConfiguration().getLocales().get(0);
237         if (mLocale == null || !mLocale.equals(curLocale)) {
238             mLocale = curLocale;
239             mNumberFormat = NumberFormat.getPercentInstance(mLocale);
240         }
241         return mNumberFormat.format(getPercent(progress));
242     }
243 
244     @VisibleForTesting
getPercent(float progress)245     double getPercent(float progress) {
246         final float maxProgress = getMax();
247         final float minProgress = getMin();
248         final float diffProgress = maxProgress - minProgress;
249         if (diffProgress <= 0.0f) {
250             return 0.0f;
251         }
252         final float percent = (progress - minProgress) / diffProgress;
253         return Math.floor(Math.max(0.0f, Math.min(1.0f, percent)) * 100) / 100;
254     }
255 
setSuppressionText(String text)256     public void setSuppressionText(String text) {
257         if (Objects.equals(text, mSuppressionText)) return;
258         mSuppressionText = text;
259         updateSuppressionText();
260     }
261 
isMuted()262     protected boolean isMuted() {
263         return mMuted && !mZenMuted;
264     }
265 
updateSuppressionText()266     protected void updateSuppressionText() {
267         if (mSuppressionTextView != null && mSeekBar != null) {
268             mSuppressionTextView.setText(mSuppressionText);
269             final boolean showSuppression = !TextUtils.isEmpty(mSuppressionText);
270             mSuppressionTextView.setVisibility(showSuppression ? View.VISIBLE : View.GONE);
271         }
272     }
273 
274     /**
275      * Update content description of title to improve talkback announcements.
276      */
updateContentDescription(CharSequence contentDescription)277     protected void updateContentDescription(CharSequence contentDescription) {
278         if (mTitle == null) return;
279         mTitle.setContentDescription(contentDescription);
280     }
281 
setAccessibilityLiveRegion(int mode)282     protected void setAccessibilityLiveRegion(int mode) {
283         if (mTitle == null) return;
284         mTitle.setAccessibilityLiveRegion(mode);
285     }
286 
287     public interface Callback {
onSampleStarting(SeekBarVolumizer sbv)288         void onSampleStarting(SeekBarVolumizer sbv);
onStreamValueChanged(int stream, int progress)289         void onStreamValueChanged(int stream, int progress);
290 
291         /**
292          * Callback reporting that the seek bar is start tracking.
293          */
onStartTrackingTouch(SeekBarVolumizer sbv)294         void onStartTrackingTouch(SeekBarVolumizer sbv);
295     }
296 
297     /**
298      * Listener to view updates in volumeSeekbarPreference.
299      */
300     public interface Listener {
301 
302         /**
303          * Listener to mute state updates.
304          */
onUpdateMuteState()305         void onUpdateMuteState();
306     }
307 }
308