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