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