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         // It's unnecessary to set up relevant volumizer configuration if preference is disabled.
144         if (!isEnabled()) {
145             mSeekBar.setEnabled(false);
146             return;
147         }
148         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             }
155             @Override
156             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromTouch) {
157                 if (mCallback != null) {
158                     mCallback.onStreamValueChanged(mStream, progress);
159                 }
160                 overrideSeekBarStateDescription(formatStateDescription(progress));
161             }
162             @Override
163             public void onMuted(boolean muted, boolean zenMuted) {
164                 if (mMuted == muted && mZenMuted == zenMuted) return;
165                 mMuted = muted;
166                 mZenMuted = zenMuted;
167                 updateIconView();
168                 if (mListener != null) {
169                     mListener.onUpdateMuteState();
170                 }
171             }
172             @Override
173             public void onStartTrackingTouch(SeekBarVolumizer sbv) {
174                 if (mCallback != null) {
175                     mCallback.onStartTrackingTouch(sbv);
176                 }
177                 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);
184             }
185         };
186         final Uri sampleUri = mStream == AudioManager.STREAM_MUSIC ? getMediaVolumeUri() : null;
187         if (mVolumizer == null) {
188             mVolumizer = mSeekBarVolumizerFactory.create(mStream, sampleUri, sbvc);
189         }
190         mVolumizer.start();
191         mVolumizer.setSeekBar(mSeekBar);
192         updateIconView();
193         updateSuppressionText();
194         if (mListener != null) {
195             mListener.onUpdateMuteState();
196         }
197     }
198 
updateIconView()199     protected void updateIconView() {
200         if (mIconView == null) return;
201         if (mIconResId != 0) {
202             mIconView.setImageResource(mIconResId);
203         } else if (mMuteIconResId != 0 && isMuted()) {
204             mIconView.setImageResource(mMuteIconResId);
205         } else {
206             mIconView.setImageDrawable(getIcon());
207         }
208     }
209 
showIcon(int resId)210     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.
213         if (mIconResId == resId) return;
214         mIconResId = resId;
215         updateIconView();
216     }
217 
setMuteIcon(int resId)218     public void setMuteIcon(int resId) {
219         if (mMuteIconResId == resId) return;
220         mMuteIconResId = resId;
221         updateIconView();
222     }
223 
getMediaVolumeUri()224     private Uri getMediaVolumeUri() {
225         return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
226                 + getContext().getPackageName()
227                 + "/" + R.raw.media_volume);
228     }
229 
230     @VisibleForTesting
formatStateDescription(int progress)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
getPercent(float progress)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 
setSuppressionText(String text)257     public void setSuppressionText(String text) {
258         if (Objects.equals(text, mSuppressionText)) return;
259         mSuppressionText = text;
260         updateSuppressionText();
261     }
262 
isMuted()263     protected boolean isMuted() {
264         return mMuted && !mZenMuted;
265     }
266 
updateSuppressionText()267     protected void updateSuppressionText() {
268         if (mSuppressionTextView != null && mSeekBar != null) {
269             mSuppressionTextView.setText(mSuppressionText);
270             final boolean showSuppression = !TextUtils.isEmpty(mSuppressionText);
271             mSuppressionTextView.setVisibility(showSuppression ? View.VISIBLE : View.GONE);
272         }
273     }
274 
275     /**
276      * Update content description of title to improve talkback announcements.
277      */
updateContentDescription(CharSequence contentDescription)278     protected void updateContentDescription(CharSequence contentDescription) {
279         if (mTitle == null) return;
280         mTitle.setContentDescription(contentDescription);
281     }
282 
setAccessibilityLiveRegion(int mode)283     protected void setAccessibilityLiveRegion(int mode) {
284         if (mTitle == null) return;
285         mTitle.setAccessibilityLiveRegion(mode);
286     }
287 
288     public interface Callback {
onSampleStarting(SeekBarVolumizer sbv)289         void onSampleStarting(SeekBarVolumizer sbv);
onStreamValueChanged(int stream, int progress)290         void onStreamValueChanged(int stream, int progress);
291 
292         /**
293          * Callback reporting that the seek bar is start tracking.
294          */
onStartTrackingTouch(SeekBarVolumizer sbv)295         void onStartTrackingTouch(SeekBarVolumizer sbv);
296     }
297 
298     /**
299      * Listener to view updates in volumeSeekbarPreference.
300      */
301     public interface Listener {
302 
303         /**
304          * Listener to mute state updates.
305          */
onUpdateMuteState()306         void onUpdateMuteState();
307     }
308 }
309