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