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