1 /* 2 * Copyright (C) 2011 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.widget; 18 19 import static android.view.HapticFeedbackConstants.CLOCK_TICK; 20 21 import static com.android.internal.jank.InteractionJankMonitor.CUJ_SETTINGS_SLIDER; 22 23 import android.content.Context; 24 import android.content.res.TypedArray; 25 import android.os.Parcel; 26 import android.os.Parcelable; 27 import android.text.TextUtils; 28 import android.util.AttributeSet; 29 import android.view.KeyEvent; 30 import android.view.View; 31 import android.view.accessibility.AccessibilityNodeInfo; 32 import android.widget.SeekBar; 33 import android.widget.SeekBar.OnSeekBarChangeListener; 34 35 import androidx.core.content.res.TypedArrayUtils; 36 import androidx.preference.PreferenceViewHolder; 37 38 import com.android.internal.jank.InteractionJankMonitor; 39 import com.android.settingslib.RestrictedPreference; 40 41 /** 42 * Based on android.preference.SeekBarPreference, but uses support preference as base. 43 */ 44 public class SeekBarPreference extends RestrictedPreference 45 implements OnSeekBarChangeListener, View.OnKeyListener { 46 47 public static final int HAPTIC_FEEDBACK_MODE_NONE = 0; 48 public static final int HAPTIC_FEEDBACK_MODE_ON_TICKS = 1; 49 public static final int HAPTIC_FEEDBACK_MODE_ON_ENDS = 2; 50 51 private final InteractionJankMonitor mJankMonitor = InteractionJankMonitor.getInstance(); 52 private int mProgress; 53 private int mMax; 54 private int mMin; 55 private boolean mTrackingTouch; 56 57 private boolean mContinuousUpdates; 58 private int mHapticFeedbackMode = HAPTIC_FEEDBACK_MODE_NONE; 59 private int mDefaultProgress = -1; 60 61 private SeekBar mSeekBar; 62 private boolean mShouldBlink; 63 private int mAccessibilityRangeInfoType = AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_INT; 64 private CharSequence mOverrideSeekBarStateDescription; 65 private CharSequence mSeekBarContentDescription; 66 private CharSequence mSeekBarStateDescription; 67 SeekBarPreference( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)68 public SeekBarPreference( 69 Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 70 super(context, attrs, defStyleAttr, defStyleRes); 71 72 TypedArray a = context.obtainStyledAttributes( 73 attrs, com.android.internal.R.styleable.ProgressBar, defStyleAttr, defStyleRes); 74 setMax(a.getInt(com.android.internal.R.styleable.ProgressBar_max, mMax)); 75 setMin(a.getInt(com.android.internal.R.styleable.ProgressBar_min, mMin)); 76 a.recycle(); 77 78 a = context.obtainStyledAttributes(attrs, 79 com.android.internal.R.styleable.SeekBarPreference, defStyleAttr, defStyleRes); 80 final int layoutResId = a.getResourceId( 81 com.android.internal.R.styleable.SeekBarPreference_layout, 82 com.android.internal.R.layout.preference_widget_seekbar); 83 a.recycle(); 84 85 setSelectable(false); 86 87 setLayoutResource(layoutResId); 88 } 89 SeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr)90 public SeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr) { 91 this(context, attrs, defStyleAttr, 0); 92 } 93 SeekBarPreference(Context context, AttributeSet attrs)94 public SeekBarPreference(Context context, AttributeSet attrs) { 95 this(context, attrs, TypedArrayUtils.getAttr(context, 96 androidx.preference.R.attr.seekBarPreferenceStyle, 97 com.android.internal.R.attr.seekBarPreferenceStyle)); 98 } 99 SeekBarPreference(Context context)100 public SeekBarPreference(Context context) { 101 this(context, null); 102 } 103 setShouldBlink(boolean shouldBlink)104 public void setShouldBlink(boolean shouldBlink) { 105 mShouldBlink = shouldBlink; 106 notifyChanged(); 107 } 108 109 @Override isSelectable()110 public boolean isSelectable() { 111 if(isDisabledByAdmin()) { 112 return true; 113 } else { 114 return super.isSelectable(); 115 } 116 } 117 118 @Override onBindViewHolder(PreferenceViewHolder view)119 public void onBindViewHolder(PreferenceViewHolder view) { 120 super.onBindViewHolder(view); 121 view.itemView.setOnKeyListener(this); 122 mSeekBar = (SeekBar) view.findViewById( 123 com.android.internal.R.id.seekbar); 124 mSeekBar.setOnSeekBarChangeListener(this); 125 mSeekBar.setMax(mMax); 126 mSeekBar.setMin(mMin); 127 mSeekBar.setProgress(mProgress); 128 mSeekBar.setEnabled(isEnabled()); 129 final CharSequence title = getTitle(); 130 if (!TextUtils.isEmpty(mSeekBarContentDescription)) { 131 mSeekBar.setContentDescription(mSeekBarContentDescription); 132 } else if (!TextUtils.isEmpty(title)) { 133 mSeekBar.setContentDescription(title); 134 } 135 if (!TextUtils.isEmpty(mSeekBarStateDescription)) { 136 mSeekBar.setStateDescription(mSeekBarStateDescription); 137 } 138 if (mSeekBar instanceof DefaultIndicatorSeekBar) { 139 ((DefaultIndicatorSeekBar) mSeekBar).setDefaultProgress(mDefaultProgress); 140 } 141 if (mShouldBlink) { 142 View v = view.itemView; 143 v.post(() -> { 144 if (v.getBackground() != null) { 145 final int centerX = v.getWidth() / 2; 146 final int centerY = v.getHeight() / 2; 147 v.getBackground().setHotspot(centerX, centerY); 148 } 149 v.setPressed(true); 150 v.setPressed(false); 151 mShouldBlink = false; 152 }); 153 } 154 mSeekBar.setAccessibilityDelegate(new View.AccessibilityDelegate() { 155 @Override 156 public void onInitializeAccessibilityNodeInfo(View view, AccessibilityNodeInfo info) { 157 super.onInitializeAccessibilityNodeInfo(view, info); 158 // Update the range info with the correct type 159 AccessibilityNodeInfo.RangeInfo rangeInfo = info.getRangeInfo(); 160 if (rangeInfo != null) { 161 info.setRangeInfo(AccessibilityNodeInfo.RangeInfo.obtain( 162 mAccessibilityRangeInfoType, rangeInfo.getMin(), 163 rangeInfo.getMax(), rangeInfo.getCurrent())); 164 } 165 if (mOverrideSeekBarStateDescription != null) { 166 info.setStateDescription(mOverrideSeekBarStateDescription); 167 } 168 } 169 }); 170 } 171 172 @Override onSetInitialValue(boolean restoreValue, Object defaultValue)173 protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { 174 setProgress(restoreValue ? getPersistedInt(mProgress) 175 : (Integer) defaultValue); 176 } 177 178 @Override onGetDefaultValue(TypedArray a, int index)179 protected Object onGetDefaultValue(TypedArray a, int index) { 180 return a.getInt(index, 0); 181 } 182 183 @Override onKey(View v, int keyCode, KeyEvent event)184 public boolean onKey(View v, int keyCode, KeyEvent event) { 185 if (event.getAction() != KeyEvent.ACTION_DOWN) { 186 return false; 187 } 188 189 SeekBar seekBar = (SeekBar) v.findViewById(com.android.internal.R.id.seekbar); 190 if (seekBar == null) { 191 return false; 192 } 193 return seekBar.onKeyDown(keyCode, event); 194 } 195 setMax(int max)196 public void setMax(int max) { 197 if (max != mMax) { 198 mMax = max; 199 notifyChanged(); 200 } 201 } 202 setMin(int min)203 public void setMin(int min) { 204 if (min != mMin) { 205 mMin = min; 206 notifyChanged(); 207 } 208 } 209 getMax()210 public int getMax() { 211 return mMax; 212 } 213 getMin()214 public int getMin() { 215 return mMin; 216 } 217 setProgress(int progress)218 public void setProgress(int progress) { 219 setProgress(progress, true); 220 } 221 222 /** 223 * Sets the progress point to draw a single tick mark representing a default value. 224 */ setDefaultProgress(int defaultProgress)225 public void setDefaultProgress(int defaultProgress) { 226 if (mDefaultProgress != defaultProgress) { 227 mDefaultProgress = defaultProgress; 228 if (mSeekBar instanceof DefaultIndicatorSeekBar) { 229 ((DefaultIndicatorSeekBar) mSeekBar).setDefaultProgress(mDefaultProgress); 230 } 231 } 232 } 233 234 /** 235 * When {@code continuousUpdates} is true, update the persisted setting immediately as the thumb 236 * is dragged along the SeekBar. Otherwise, only update the value of the setting when the thumb 237 * is dropped. 238 */ setContinuousUpdates(boolean continuousUpdates)239 public void setContinuousUpdates(boolean continuousUpdates) { 240 mContinuousUpdates = continuousUpdates; 241 } 242 243 /** 244 * Sets the haptic feedback mode. HAPTIC_FEEDBACK_MODE_ON_TICKS means to perform haptic feedback 245 * as the SeekBar's progress is updated; HAPTIC_FEEDBACK_MODE_ON_ENDS means to perform haptic 246 * feedback as the SeekBar's progress value is equal to the min/max value. 247 * 248 * @param hapticFeedbackMode the haptic feedback mode. 249 */ setHapticFeedbackMode(int hapticFeedbackMode)250 public void setHapticFeedbackMode(int hapticFeedbackMode) { 251 mHapticFeedbackMode = hapticFeedbackMode; 252 } 253 setProgress(int progress, boolean notifyChanged)254 private void setProgress(int progress, boolean notifyChanged) { 255 if (progress > mMax) { 256 progress = mMax; 257 } 258 if (progress < mMin) { 259 progress = mMin; 260 } 261 if (progress != mProgress) { 262 mProgress = progress; 263 persistInt(progress); 264 if (notifyChanged) { 265 notifyChanged(); 266 } 267 } 268 } 269 getProgress()270 public int getProgress() { 271 return mProgress; 272 } 273 274 /** 275 * Persist the seekBar's progress value if callChangeListener 276 * returns true, otherwise set the seekBar's progress to the stored value 277 */ syncProgress(SeekBar seekBar)278 void syncProgress(SeekBar seekBar) { 279 int progress = seekBar.getProgress(); 280 if (progress != mProgress) { 281 if (callChangeListener(progress)) { 282 setProgress(progress, false); 283 switch (mHapticFeedbackMode) { 284 case HAPTIC_FEEDBACK_MODE_ON_TICKS: 285 seekBar.performHapticFeedback(CLOCK_TICK); 286 break; 287 case HAPTIC_FEEDBACK_MODE_ON_ENDS: 288 if (progress == mMax || progress == mMin) { 289 seekBar.performHapticFeedback(CLOCK_TICK); 290 } 291 break; 292 } 293 } else { 294 seekBar.setProgress(mProgress); 295 } 296 } 297 } 298 299 @Override onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)300 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 301 if (fromUser && (mContinuousUpdates || !mTrackingTouch)) { 302 syncProgress(seekBar); 303 } 304 } 305 306 @Override onStartTrackingTouch(SeekBar seekBar)307 public void onStartTrackingTouch(SeekBar seekBar) { 308 mTrackingTouch = true; 309 mJankMonitor.begin(InteractionJankMonitor.Configuration.Builder 310 .withView(CUJ_SETTINGS_SLIDER, seekBar) 311 .setTag(getKey())); 312 } 313 314 @Override onStopTrackingTouch(SeekBar seekBar)315 public void onStopTrackingTouch(SeekBar seekBar) { 316 mTrackingTouch = false; 317 if (seekBar.getProgress() != mProgress) { 318 syncProgress(seekBar); 319 } 320 mJankMonitor.end(CUJ_SETTINGS_SLIDER); 321 } 322 323 /** 324 * Specify the type of range this seek bar represents. 325 * 326 * @param rangeInfoType The type of range to be shared with accessibility 327 * 328 * @see android.view.accessibility.AccessibilityNodeInfo.RangeInfo 329 */ setAccessibilityRangeInfoType(int rangeInfoType)330 public void setAccessibilityRangeInfoType(int rangeInfoType) { 331 mAccessibilityRangeInfoType = rangeInfoType; 332 } 333 setSeekBarContentDescription(CharSequence contentDescription)334 public void setSeekBarContentDescription(CharSequence contentDescription) { 335 mSeekBarContentDescription = contentDescription; 336 if (mSeekBar != null) { 337 mSeekBar.setContentDescription(contentDescription); 338 } 339 } 340 341 /** 342 * Specify the state description for this seek bar represents. 343 * 344 * @param stateDescription the state description of seek bar 345 */ setSeekBarStateDescription(CharSequence stateDescription)346 public void setSeekBarStateDescription(CharSequence stateDescription) { 347 mSeekBarStateDescription = stateDescription; 348 if (mSeekBar != null) { 349 mSeekBar.setStateDescription(stateDescription); 350 } 351 } 352 353 /** 354 * Overrides the state description of {@link SeekBar} with given content. 355 */ overrideSeekBarStateDescription(CharSequence stateDescription)356 public void overrideSeekBarStateDescription(CharSequence stateDescription) { 357 mOverrideSeekBarStateDescription = stateDescription; 358 } 359 360 @Override onSaveInstanceState()361 protected Parcelable onSaveInstanceState() { 362 /* 363 * Suppose a client uses this preference type without persisting. We 364 * must save the instance state so it is able to, for example, survive 365 * orientation changes. 366 */ 367 368 final Parcelable superState = super.onSaveInstanceState(); 369 if (isPersistent()) { 370 // No need to save instance state since it's persistent 371 return superState; 372 } 373 374 // Save the instance state 375 final SavedState myState = new SavedState(superState); 376 myState.progress = mProgress; 377 myState.max = mMax; 378 myState.min = mMin; 379 return myState; 380 } 381 382 @Override onRestoreInstanceState(Parcelable state)383 protected void onRestoreInstanceState(Parcelable state) { 384 if (!state.getClass().equals(SavedState.class)) { 385 // Didn't save state for us in onSaveInstanceState 386 super.onRestoreInstanceState(state); 387 return; 388 } 389 390 // Restore the instance state 391 SavedState myState = (SavedState) state; 392 super.onRestoreInstanceState(myState.getSuperState()); 393 mProgress = myState.progress; 394 mMax = myState.max; 395 mMin = myState.min; 396 notifyChanged(); 397 } 398 399 /** 400 * SavedState, a subclass of {@link BaseSavedState}, will store the state 401 * of MyPreference, a subclass of Preference. 402 * <p> 403 * It is important to always call through to super methods. 404 */ 405 private static class SavedState extends BaseSavedState { 406 int progress; 407 int max; 408 int min; 409 SavedState(Parcel source)410 public SavedState(Parcel source) { 411 super(source); 412 413 // Restore the click counter 414 progress = source.readInt(); 415 max = source.readInt(); 416 min = source.readInt(); 417 } 418 419 @Override writeToParcel(Parcel dest, int flags)420 public void writeToParcel(Parcel dest, int flags) { 421 super.writeToParcel(dest, flags); 422 423 // Save the click counter 424 dest.writeInt(progress); 425 dest.writeInt(max); 426 dest.writeInt(min); 427 } 428 SavedState(Parcelable superState)429 public SavedState(Parcelable superState) { 430 super(superState); 431 } 432 433 @SuppressWarnings("unused") 434 public static final Parcelable.Creator<SavedState> CREATOR = 435 new Parcelable.Creator<SavedState>() { 436 public SavedState createFromParcel(Parcel in) { 437 return new SavedState(in); 438 } 439 440 public SavedState[] newArray(int size) { 441 return new SavedState[size]; 442 } 443 }; 444 } 445 } 446