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 android.content.Context;
20 import android.content.res.TypedArray;
21 import android.os.Parcel;
22 import android.os.Parcelable;
23 import android.text.TextUtils;
24 import android.util.AttributeSet;
25 import android.view.KeyEvent;
26 import android.view.View;
27 import android.view.accessibility.AccessibilityNodeInfo;
28 import android.widget.SeekBar;
29 import android.widget.SeekBar.OnSeekBarChangeListener;
30 
31 import androidx.core.content.res.TypedArrayUtils;
32 import androidx.preference.PreferenceViewHolder;
33 
34 import com.android.settingslib.RestrictedPreference;
35 
36 /**
37  * Based on android.preference.SeekBarPreference, but uses support preference as base.
38  */
39 public class SeekBarPreference extends RestrictedPreference
40         implements OnSeekBarChangeListener, View.OnKeyListener {
41 
42     private int mProgress;
43     private int mMax;
44     private int mMin;
45     private boolean mTrackingTouch;
46 
47     private boolean mContinuousUpdates;
48     private int mDefaultProgress = -1;
49 
50     private SeekBar mSeekBar;
51     private boolean mShouldBlink;
52     private int mAccessibilityRangeInfoType = AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_INT;
53     private CharSequence mSeekBarContentDescription;
54 
SeekBarPreference( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)55     public SeekBarPreference(
56             Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
57         super(context, attrs, defStyleAttr, defStyleRes);
58 
59         TypedArray a = context.obtainStyledAttributes(
60                 attrs, com.android.internal.R.styleable.ProgressBar, defStyleAttr, defStyleRes);
61         setMax(a.getInt(com.android.internal.R.styleable.ProgressBar_max, mMax));
62         setMin(a.getInt(com.android.internal.R.styleable.ProgressBar_min, mMin));
63         a.recycle();
64 
65         a = context.obtainStyledAttributes(attrs,
66                 com.android.internal.R.styleable.SeekBarPreference, defStyleAttr, defStyleRes);
67         final int layoutResId = a.getResourceId(
68                 com.android.internal.R.styleable.SeekBarPreference_layout,
69                 com.android.internal.R.layout.preference_widget_seekbar);
70         a.recycle();
71 
72         a = context.obtainStyledAttributes(
73                 attrs, com.android.internal.R.styleable.Preference, defStyleAttr, defStyleRes);
74         final boolean isSelectable = a.getBoolean(
75                 com.android.settings.R.styleable.Preference_android_selectable, false);
76         setSelectable(isSelectable);
77         a.recycle();
78 
79         setLayoutResource(layoutResId);
80     }
81 
SeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr)82     public SeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr) {
83         this(context, attrs, defStyleAttr, 0);
84     }
85 
SeekBarPreference(Context context, AttributeSet attrs)86     public SeekBarPreference(Context context, AttributeSet attrs) {
87         this(context, attrs, TypedArrayUtils.getAttr(context,
88                         androidx.preference.R.attr.seekBarPreferenceStyle,
89                         com.android.internal.R.attr.seekBarPreferenceStyle));
90     }
91 
SeekBarPreference(Context context)92     public SeekBarPreference(Context context) {
93         this(context, null);
94     }
95 
setShouldBlink(boolean shouldBlink)96     public void setShouldBlink(boolean shouldBlink) {
97         mShouldBlink = shouldBlink;
98         notifyChanged();
99     }
100 
101     @Override
isSelectable()102     public boolean isSelectable() {
103         if(isDisabledByAdmin()) {
104             return true;
105         } else {
106             return super.isSelectable();
107         }
108     }
109 
110     @Override
onBindViewHolder(PreferenceViewHolder view)111     public void onBindViewHolder(PreferenceViewHolder view) {
112         super.onBindViewHolder(view);
113         view.itemView.setOnKeyListener(this);
114         mSeekBar = (SeekBar) view.findViewById(
115                 com.android.internal.R.id.seekbar);
116         mSeekBar.setOnSeekBarChangeListener(this);
117         mSeekBar.setMax(mMax);
118         mSeekBar.setMin(mMin);
119         mSeekBar.setProgress(mProgress);
120         mSeekBar.setEnabled(isEnabled());
121         final CharSequence title = getTitle();
122         if (!TextUtils.isEmpty(mSeekBarContentDescription)) {
123             mSeekBar.setContentDescription(mSeekBarContentDescription);
124         } else if (!TextUtils.isEmpty(title)) {
125             mSeekBar.setContentDescription(title);
126         }
127         if (mSeekBar instanceof DefaultIndicatorSeekBar) {
128             ((DefaultIndicatorSeekBar) mSeekBar).setDefaultProgress(mDefaultProgress);
129         }
130         if (mShouldBlink) {
131             View v = view.itemView;
132             v.post(() -> {
133                 if (v.getBackground() != null) {
134                     final int centerX = v.getWidth() / 2;
135                     final int centerY = v.getHeight() / 2;
136                     v.getBackground().setHotspot(centerX, centerY);
137                 }
138                 v.setPressed(true);
139                 v.setPressed(false);
140                 mShouldBlink = false;
141             });
142         }
143         mSeekBar.setAccessibilityDelegate(new View.AccessibilityDelegate() {
144             @Override
145             public void onInitializeAccessibilityNodeInfo(View view, AccessibilityNodeInfo info) {
146                 super.onInitializeAccessibilityNodeInfo(view, info);
147                 // Update the range info with the correct type
148                 AccessibilityNodeInfo.RangeInfo rangeInfo = info.getRangeInfo();
149                 if (rangeInfo != null) {
150                     info.setRangeInfo(AccessibilityNodeInfo.RangeInfo.obtain(
151                                     mAccessibilityRangeInfoType, rangeInfo.getMin(),
152                                     rangeInfo.getMax(), rangeInfo.getCurrent()));
153                 }
154             }
155         });
156     }
157 
158     @Override
getSummary()159     public CharSequence getSummary() {
160         return null;
161     }
162 
163     @Override
onSetInitialValue(boolean restoreValue, Object defaultValue)164     protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
165         setProgress(restoreValue ? getPersistedInt(mProgress)
166                 : (Integer) defaultValue);
167     }
168 
169     @Override
onGetDefaultValue(TypedArray a, int index)170     protected Object onGetDefaultValue(TypedArray a, int index) {
171         return a.getInt(index, 0);
172     }
173 
174     @Override
onKey(View v, int keyCode, KeyEvent event)175     public boolean onKey(View v, int keyCode, KeyEvent event) {
176         if (event.getAction() != KeyEvent.ACTION_DOWN) {
177             return false;
178         }
179 
180         SeekBar seekBar = (SeekBar) v.findViewById(com.android.internal.R.id.seekbar);
181         if (seekBar == null) {
182             return false;
183         }
184         return seekBar.onKeyDown(keyCode, event);
185     }
186 
setMax(int max)187     public void setMax(int max) {
188         if (max != mMax) {
189             mMax = max;
190             notifyChanged();
191         }
192     }
193 
setMin(int min)194     public void setMin(int min) {
195         if (min != mMin) {
196             mMin = min;
197             notifyChanged();
198         }
199     }
200 
getMax()201     public int getMax() {
202         return mMax;
203     }
204 
getMin()205     public int getMin() {
206         return mMin;
207     }
208 
setProgress(int progress)209     public void setProgress(int progress) {
210         setProgress(progress, true);
211     }
212 
213     /**
214      * Sets the progress point to draw a single tick mark representing a default value.
215      */
setDefaultProgress(int defaultProgress)216     public void setDefaultProgress(int defaultProgress) {
217         if (mDefaultProgress != defaultProgress) {
218             mDefaultProgress = defaultProgress;
219             if (mSeekBar instanceof DefaultIndicatorSeekBar) {
220                 ((DefaultIndicatorSeekBar) mSeekBar).setDefaultProgress(mDefaultProgress);
221             }
222         }
223     }
224 
225     /**
226      * When {@code continuousUpdates} is true, update the persisted setting immediately as the thumb
227      * is dragged along the SeekBar. Otherwise, only update the value of the setting when the thumb
228      * is dropped.
229      */
setContinuousUpdates(boolean continuousUpdates)230     public void setContinuousUpdates(boolean continuousUpdates) {
231         mContinuousUpdates = continuousUpdates;
232     }
233 
setProgress(int progress, boolean notifyChanged)234     private void setProgress(int progress, boolean notifyChanged) {
235         if (progress > mMax) {
236             progress = mMax;
237         }
238         if (progress < mMin) {
239             progress = mMin;
240         }
241         if (progress != mProgress) {
242             mProgress = progress;
243             persistInt(progress);
244             if (notifyChanged) {
245                 notifyChanged();
246             }
247         }
248     }
249 
getProgress()250     public int getProgress() {
251         return mProgress;
252     }
253 
254     /**
255      * Persist the seekBar's progress value if callChangeListener
256      * returns true, otherwise set the seekBar's progress to the stored value
257      */
syncProgress(SeekBar seekBar)258     void syncProgress(SeekBar seekBar) {
259         int progress = seekBar.getProgress();
260         if (progress != mProgress) {
261             if (callChangeListener(progress)) {
262                 setProgress(progress, false);
263             } else {
264                 seekBar.setProgress(mProgress);
265             }
266         }
267     }
268 
269     @Override
onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)270     public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
271         if (fromUser && (mContinuousUpdates || !mTrackingTouch)) {
272             syncProgress(seekBar);
273         }
274     }
275 
276     @Override
onStartTrackingTouch(SeekBar seekBar)277     public void onStartTrackingTouch(SeekBar seekBar) {
278         mTrackingTouch = true;
279     }
280 
281     @Override
onStopTrackingTouch(SeekBar seekBar)282     public void onStopTrackingTouch(SeekBar seekBar) {
283         mTrackingTouch = false;
284         if (seekBar.getProgress() != mProgress) {
285             syncProgress(seekBar);
286         }
287     }
288 
289     /**
290      * Specify the type of range this seek bar represents.
291      *
292      * @param rangeInfoType The type of range to be shared with accessibility
293      *
294      * @see android.view.accessibility.AccessibilityNodeInfo.RangeInfo
295      */
setAccessibilityRangeInfoType(int rangeInfoType)296     public void setAccessibilityRangeInfoType(int rangeInfoType) {
297         mAccessibilityRangeInfoType = rangeInfoType;
298     }
299 
setSeekBarContentDescription(CharSequence contentDescription)300     public void setSeekBarContentDescription(CharSequence contentDescription) {
301         mSeekBarContentDescription = contentDescription;
302         if (mSeekBar != null) {
303             mSeekBar.setContentDescription(contentDescription);
304         }
305     }
306 
307     @Override
onSaveInstanceState()308     protected Parcelable onSaveInstanceState() {
309         /*
310          * Suppose a client uses this preference type without persisting. We
311          * must save the instance state so it is able to, for example, survive
312          * orientation changes.
313          */
314 
315         final Parcelable superState = super.onSaveInstanceState();
316         if (isPersistent()) {
317             // No need to save instance state since it's persistent
318             return superState;
319         }
320 
321         // Save the instance state
322         final SavedState myState = new SavedState(superState);
323         myState.progress = mProgress;
324         myState.max = mMax;
325         myState.min = mMin;
326         return myState;
327     }
328 
329     @Override
onRestoreInstanceState(Parcelable state)330     protected void onRestoreInstanceState(Parcelable state) {
331         if (!state.getClass().equals(SavedState.class)) {
332             // Didn't save state for us in onSaveInstanceState
333             super.onRestoreInstanceState(state);
334             return;
335         }
336 
337         // Restore the instance state
338         SavedState myState = (SavedState) state;
339         super.onRestoreInstanceState(myState.getSuperState());
340         mProgress = myState.progress;
341         mMax = myState.max;
342         mMin = myState.min;
343         notifyChanged();
344     }
345 
346     /**
347      * SavedState, a subclass of {@link BaseSavedState}, will store the state
348      * of MyPreference, a subclass of Preference.
349      * <p>
350      * It is important to always call through to super methods.
351      */
352     private static class SavedState extends BaseSavedState {
353         int progress;
354         int max;
355         int min;
356 
SavedState(Parcel source)357         public SavedState(Parcel source) {
358             super(source);
359 
360             // Restore the click counter
361             progress = source.readInt();
362             max = source.readInt();
363             min = source.readInt();
364         }
365 
366         @Override
writeToParcel(Parcel dest, int flags)367         public void writeToParcel(Parcel dest, int flags) {
368             super.writeToParcel(dest, flags);
369 
370             // Save the click counter
371             dest.writeInt(progress);
372             dest.writeInt(max);
373             dest.writeInt(min);
374         }
375 
SavedState(Parcelable superState)376         public SavedState(Parcelable superState) {
377             super(superState);
378         }
379 
380         @SuppressWarnings("unused")
381         public static final Parcelable.Creator<SavedState> CREATOR =
382                 new Parcelable.Creator<SavedState>() {
383             public SavedState createFromParcel(Parcel in) {
384                 return new SavedState(in);
385             }
386 
387             public SavedState[] newArray(int size) {
388                 return new SavedState[size];
389             }
390         };
391     }
392 }
393