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