• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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