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