1 /*
2  * Copyright (C) 2018 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.settings.common;
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.util.AttributeSet;
24 import android.util.Log;
25 import android.view.KeyEvent;
26 import android.view.View;
27 import android.widget.SeekBar;
28 import android.widget.TextView;
29 
30 import androidx.preference.PreferenceViewHolder;
31 
32 import com.android.car.settings.R;
33 import com.android.car.ui.preference.CarUiPreference;
34 
35 /**
36  * Car Setting's own version of SeekBarPreference.
37  *
38  * The code is directly taken from androidx.preference.SeekBarPreference. However it has 1 main
39  * functionality difference. There is a new field which can enable continuous updates while the
40  * seek bar value is changing. This can be set programmatically by using the {@link
41  * #setContinuousUpdate() setContinuousUpdate} method.
42  */
43 public class SeekBarPreference extends CarUiPreference {
44 
45     private int mSeekBarValue;
46     private int mMin;
47     private int mMax;
48     private int mSeekBarIncrement;
49     private boolean mTrackingTouch;
50     private SeekBar mSeekBar;
51     private TextView mSeekBarValueTextView;
52     private boolean mAdjustable; // whether the seekbar should respond to the left/right keys
53     private boolean mShowSeekBarValue; // whether to show the seekbar value TextView next to the bar
54     private boolean mContinuousUpdate; // whether scrolling provides continuous calls to listener
55 
56     private static final String TAG = "SeekBarPreference";
57 
58     /**
59      * Listener reacting to the SeekBar changing value by the user
60      */
61     private SeekBar.OnSeekBarChangeListener mSeekBarChangeListener =
62             new SeekBar.OnSeekBarChangeListener() {
63                 @Override
64                 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
65                     if (fromUser && (mContinuousUpdate || !mTrackingTouch)) {
66                         syncValueInternal(seekBar);
67                     }
68                 }
69 
70                 @Override
71                 public void onStartTrackingTouch(SeekBar seekBar) {
72                     mTrackingTouch = true;
73                 }
74 
75                 @Override
76                 public void onStopTrackingTouch(SeekBar seekBar) {
77                     mTrackingTouch = false;
78                     if (seekBar.getProgress() + mMin != mSeekBarValue) {
79                         syncValueInternal(seekBar);
80                     }
81                 }
82             };
83 
84     /**
85      * Listener reacting to the user pressing DPAD left/right keys if {@code
86      * adjustable} attribute is set to true; it transfers the key presses to the SeekBar
87      * to be handled accordingly.
88      */
89     private View.OnKeyListener mSeekBarKeyListener = new View.OnKeyListener() {
90         @Override
91         public boolean onKey(View v, int keyCode, KeyEvent event) {
92             if (event.getAction() != KeyEvent.ACTION_DOWN) {
93                 return false;
94             }
95 
96             if (!mAdjustable && (keyCode == KeyEvent.KEYCODE_DPAD_LEFT
97                     || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT)) {
98                 // Right or left keys are pressed when in non-adjustable mode; Skip the keys.
99                 return false;
100             }
101 
102             // We don't want to propagate the click keys down to the seekbar view since it will
103             // create the ripple effect for the thumb.
104             if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) {
105                 return false;
106             }
107 
108             if (mSeekBar == null) {
109                 Log.e(TAG, "SeekBar view is null and hence cannot be adjusted.");
110                 return false;
111             }
112             return mSeekBar.onKeyDown(keyCode, event);
113         }
114     };
115 
SeekBarPreference( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)116     public SeekBarPreference(
117             Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
118         super(context, attrs, defStyleAttr, defStyleRes);
119 
120         TypedArray a = context.obtainStyledAttributes(
121                 attrs, R.styleable.SeekBarPreference, defStyleAttr, defStyleRes);
122 
123         /**
124          * The ordering of these two statements are important. If we want to set max first, we need
125          * to perform the same steps by changing min/max to max/min as following:
126          * mMax = a.getInt(...) and setMin(...).
127          */
128         mMin = a.getInt(R.styleable.SeekBarPreference_min, 0);
129         setMax(a.getInt(R.styleable.SeekBarPreference_android_max, 100));
130         setSeekBarIncrement(a.getInt(R.styleable.SeekBarPreference_seekBarIncrement, 0));
131         mAdjustable = a.getBoolean(R.styleable.SeekBarPreference_adjustable, true);
132         mShowSeekBarValue = a.getBoolean(R.styleable.SeekBarPreference_showSeekBarValue, true);
133         a.recycle();
134     }
135 
SeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr)136     public SeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr) {
137         this(context, attrs, defStyleAttr, 0);
138     }
139 
SeekBarPreference(Context context, AttributeSet attrs)140     public SeekBarPreference(Context context, AttributeSet attrs) {
141         this(context, attrs, R.attr.seekBarPreferenceStyle);
142     }
143 
SeekBarPreference(Context context)144     public SeekBarPreference(Context context) {
145         this(context, null);
146     }
147 
148     @Override
onBindViewHolder(PreferenceViewHolder view)149     public void onBindViewHolder(PreferenceViewHolder view) {
150         super.onBindViewHolder(view);
151         view.itemView.setOnKeyListener(mSeekBarKeyListener);
152         mSeekBar = (SeekBar) view.findViewById(R.id.seekbar);
153         mSeekBarValueTextView = (TextView) view.findViewById(R.id.seekbar_value);
154         if (mShowSeekBarValue) {
155             mSeekBarValueTextView.setVisibility(View.VISIBLE);
156         } else {
157             mSeekBarValueTextView.setVisibility(View.GONE);
158             mSeekBarValueTextView = null;
159         }
160 
161         if (mSeekBar == null) {
162             Log.e(TAG, "SeekBar view is null in onBindViewHolder.");
163             return;
164         }
165         mSeekBar.setOnSeekBarChangeListener(mSeekBarChangeListener);
166         mSeekBar.setMax(mMax - mMin);
167         // If the increment is not zero, use that. Otherwise, use the default mKeyProgressIncrement
168         // in AbsSeekBar when it's zero. This default increment value is set by AbsSeekBar
169         // after calling setMax. That's why it's important to call setKeyProgressIncrement after
170         // calling setMax() since setMax() can change the increment value.
171         if (mSeekBarIncrement != 0) {
172             mSeekBar.setKeyProgressIncrement(mSeekBarIncrement);
173         } else {
174             mSeekBarIncrement = mSeekBar.getKeyProgressIncrement();
175         }
176 
177         mSeekBar.setProgress(mSeekBarValue - mMin);
178         if (mSeekBarValueTextView != null) {
179             mSeekBarValueTextView.setText(String.valueOf(mSeekBarValue));
180         }
181         mSeekBar.setEnabled(isEnabled());
182     }
183 
184     @Override
onSetInitialValue(boolean restoreValue, Object defaultValue)185     protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
186         setValue(restoreValue ? getPersistedInt(mSeekBarValue)
187                 : (Integer) defaultValue);
188     }
189 
190     @Override
onGetDefaultValue(TypedArray a, int index)191     protected Object onGetDefaultValue(TypedArray a, int index) {
192         return a.getInt(index, 0);
193     }
194 
195     /** Setter for the minimum value allowed on seek bar. */
setMin(int min)196     public void setMin(int min) {
197         if (min > mMax) {
198             min = mMax;
199         }
200         if (min != mMin) {
201             mMin = min;
202             notifyChanged();
203         }
204     }
205 
206     /** Getter for the minimum value allowed on seek bar. */
getMin()207     public int getMin() {
208         return mMin;
209     }
210 
211     /** Setter for the maximum value allowed on seek bar. */
setMax(int max)212     public final void setMax(int max) {
213         if (max < mMin) {
214             max = mMin;
215         }
216         if (max != mMax) {
217             mMax = max;
218             notifyChanged();
219         }
220     }
221 
222     /**
223      * Returns the amount of increment change via each arrow key click. This value is derived
224      * from
225      * user's specified increment value if it's not zero. Otherwise, the default value is picked
226      * from the default mKeyProgressIncrement value in {@link android.widget.AbsSeekBar}.
227      *
228      * @return The amount of increment on the SeekBar performed after each user's arrow key press.
229      */
getSeekBarIncrement()230     public final int getSeekBarIncrement() {
231         return mSeekBarIncrement;
232     }
233 
234     /**
235      * Sets the increment amount on the SeekBar for each arrow key press.
236      *
237      * @param seekBarIncrement The amount to increment or decrement when the user presses an
238      *                         arrow key.
239      */
setSeekBarIncrement(int seekBarIncrement)240     public final void setSeekBarIncrement(int seekBarIncrement) {
241         if (seekBarIncrement != mSeekBarIncrement) {
242             mSeekBarIncrement = Math.min(mMax - mMin, Math.abs(seekBarIncrement));
243             notifyChanged();
244         }
245     }
246 
247     /** Getter for the maximum value allowed on seek bar. */
getMax()248     public int getMax() {
249         return mMax;
250     }
251 
252     /** Setter for the functionality which allows for changing the values via keyboard arrows. */
setAdjustable(boolean adjustable)253     public void setAdjustable(boolean adjustable) {
254         mAdjustable = adjustable;
255     }
256 
257     /** Getter for the functionality which allows for changing the values via keyboard arrows. */
isAdjustable()258     public boolean isAdjustable() {
259         return mAdjustable;
260     }
261 
262     /** Setter for the functionality which allows for continuous triggering of listener code. */
setContinuousUpdate(boolean continuousUpdate)263     public void setContinuousUpdate(boolean continuousUpdate) {
264         mContinuousUpdate = continuousUpdate;
265     }
266 
267     /** Setter for the whether the text should be visible. */
setShowSeekBarValue(boolean showSeekBarValue)268     public void setShowSeekBarValue(boolean showSeekBarValue) {
269         mShowSeekBarValue = showSeekBarValue;
270     }
271 
272     /** Setter for the current value of the seek bar. */
setValue(int seekBarValue)273     public void setValue(int seekBarValue) {
274         setValueInternal(seekBarValue, true);
275     }
276 
setValueInternal(int seekBarValue, boolean notifyChanged)277     private void setValueInternal(int seekBarValue, boolean notifyChanged) {
278         if (seekBarValue < mMin) {
279             seekBarValue = mMin;
280         }
281         if (seekBarValue > mMax) {
282             seekBarValue = mMax;
283         }
284 
285         if (seekBarValue != mSeekBarValue) {
286             mSeekBarValue = seekBarValue;
287             if (mSeekBarValueTextView != null) {
288                 mSeekBarValueTextView.setText(String.valueOf(mSeekBarValue));
289             }
290             persistInt(seekBarValue);
291             if (notifyChanged) {
292                 notifyChanged();
293             }
294         }
295     }
296 
297     /** Getter for the current value of the seek bar. */
getValue()298     public int getValue() {
299         return mSeekBarValue;
300     }
301 
302     /**
303      * Persist the seekBar's seekbar value if callChangeListener
304      * returns true, otherwise set the seekBar's value to the stored value
305      */
syncValueInternal(SeekBar seekBar)306     private void syncValueInternal(SeekBar seekBar) {
307         int seekBarValue = mMin + seekBar.getProgress();
308         if (seekBarValue != mSeekBarValue) {
309             if (callChangeListener(seekBarValue)) {
310                 setValueInternal(seekBarValue, false);
311             } else {
312                 seekBar.setProgress(mSeekBarValue - mMin);
313             }
314         }
315     }
316 
317     @Override
onSaveInstanceState()318     protected Parcelable onSaveInstanceState() {
319         final Parcelable superState = super.onSaveInstanceState();
320         if (isPersistent()) {
321             // No need to save instance state since it's persistent
322             return superState;
323         }
324 
325         // Save the instance state
326         final SeekBarPreference.SavedState myState = new SeekBarPreference.SavedState(superState);
327         myState.mSeekBarValue = mSeekBarValue;
328         myState.mMin = mMin;
329         myState.mMax = mMax;
330         return myState;
331     }
332 
333     @Override
onRestoreInstanceState(Parcelable state)334     protected void onRestoreInstanceState(Parcelable state) {
335         if (!state.getClass().equals(SeekBarPreference.SavedState.class)) {
336             // Didn't save state for us in onSaveInstanceState
337             super.onRestoreInstanceState(state);
338             return;
339         }
340 
341         // Restore the instance state
342         SeekBarPreference.SavedState myState = (SeekBarPreference.SavedState) state;
343         super.onRestoreInstanceState(myState.getSuperState());
344         mSeekBarValue = myState.mSeekBarValue;
345         mMin = myState.mMin;
346         mMax = myState.mMax;
347         notifyChanged();
348     }
349 
350     /**
351      * SavedState, a subclass of {@link BaseSavedState}, will store the state
352      * of MyPreference, a subclass of Preference.
353      * <p>
354      * It is important to always call through to super methods.
355      */
356     private static class SavedState extends BaseSavedState {
357         int mSeekBarValue;
358         int mMin;
359         int mMax;
360 
SavedState(Parcel source)361         SavedState(Parcel source) {
362             super(source);
363 
364             // Restore the click counter
365             mSeekBarValue = source.readInt();
366             mMin = source.readInt();
367             mMax = source.readInt();
368         }
369 
370         @Override
writeToParcel(Parcel dest, int flags)371         public void writeToParcel(Parcel dest, int flags) {
372             super.writeToParcel(dest, flags);
373 
374             // Save the click counter
375             dest.writeInt(mSeekBarValue);
376             dest.writeInt(mMin);
377             dest.writeInt(mMax);
378         }
379 
SavedState(Parcelable superState)380         SavedState(Parcelable superState) {
381             super(superState);
382         }
383 
384         @SuppressWarnings("unused")
385         public static final Parcelable.Creator<SeekBarPreference.SavedState> CREATOR =
386                 new Parcelable.Creator<SeekBarPreference.SavedState>() {
387                     @Override
388                     public SeekBarPreference.SavedState createFromParcel(Parcel in) {
389                         return new SeekBarPreference.SavedState(in);
390                     }
391 
392                     @Override
393                     public SeekBarPreference.SavedState[] newArray(int size) {
394                         return new SeekBarPreference
395                                 .SavedState[size];
396                     }
397                 };
398     }
399 }
400