• 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.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