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