diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp index a65bf41210a1b..6c691f81d9a67 100644 --- a/packages/SettingsLib/Android.bp +++ b/packages/SettingsLib/Android.bp @@ -73,6 +73,7 @@ java_defaults { "SettingsLibCollapsingToolbarBaseActivity", "SettingsLibTwoTargetPreference", "SettingsLibSettingsTransition", + "SettingsLibSeekBarPreference", ], } diff --git a/packages/SettingsLib/SeekBarPreference/Android.bp b/packages/SettingsLib/SeekBarPreference/Android.bp new file mode 100644 index 0000000000000..98f5d2aaf6320 --- /dev/null +++ b/packages/SettingsLib/SeekBarPreference/Android.bp @@ -0,0 +1,14 @@ +android_library { + name: "SettingsLibSeekBarPreference", + + srcs: ["src/**/*.java"], + resource_dirs: ["res"], + + static_libs: [ + "androidx.annotation_annotation", + "androidx.preference_preference", + "SettingsLibSettingsTheme", + ], + + min_sdk_version: "28", +} diff --git a/packages/SettingsLib/SeekBarPreference/AndroidManifest.xml b/packages/SettingsLib/SeekBarPreference/AndroidManifest.xml new file mode 100644 index 0000000000000..73163fca53628 --- /dev/null +++ b/packages/SettingsLib/SeekBarPreference/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/packages/SettingsLib/SeekBarPreference/lint-baseline.xml b/packages/SettingsLib/SeekBarPreference/lint-baseline.xml new file mode 100644 index 0000000000000..934b27b72a42d --- /dev/null +++ b/packages/SettingsLib/SeekBarPreference/lint-baseline.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/packages/SettingsLib/SeekBarPreference/res/layout/preference_labeled_slider.xml b/packages/SettingsLib/SeekBarPreference/res/layout/preference_labeled_slider.xml new file mode 100644 index 0000000000000..d9d42bf9ce4e7 --- /dev/null +++ b/packages/SettingsLib/SeekBarPreference/res/layout/preference_labeled_slider.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SettingsLib/SeekBarPreference/res/layout/preference_widget_slider.xml b/packages/SettingsLib/SeekBarPreference/res/layout/preference_widget_slider.xml new file mode 100644 index 0000000000000..f0510a2be29de --- /dev/null +++ b/packages/SettingsLib/SeekBarPreference/res/layout/preference_widget_slider.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SettingsLib/SeekBarPreference/res/values/attrs.xml b/packages/SettingsLib/SeekBarPreference/res/values/attrs.xml new file mode 100644 index 0000000000000..1a24a4e84aa2f --- /dev/null +++ b/packages/SettingsLib/SeekBarPreference/res/values/attrs.xml @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/packages/SettingsLib/SeekBarPreference/res/values/strings.xml b/packages/SettingsLib/SeekBarPreference/res/values/strings.xml new file mode 100644 index 0000000000000..8c398446c37b7 --- /dev/null +++ b/packages/SettingsLib/SeekBarPreference/res/values/strings.xml @@ -0,0 +1,21 @@ + + + + + +   + \ No newline at end of file diff --git a/packages/SettingsLib/SeekBarPreference/src/com/android/settingslib/widget/LabeledSeekBarPreference.java b/packages/SettingsLib/SeekBarPreference/src/com/android/settingslib/widget/LabeledSeekBarPreference.java new file mode 100644 index 0000000000000..5d5ebe8ea167f --- /dev/null +++ b/packages/SettingsLib/SeekBarPreference/src/com/android/settingslib/widget/LabeledSeekBarPreference.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.widget.SeekBar; +import android.widget.TextView; + +import androidx.preference.PreferenceViewHolder; + +/** A slider preference with left and right labels **/ +public class LabeledSeekBarPreference extends SeekBarPreference { + + private final int mTextStartId; + private final int mTextEndId; + private final int mTickMarkId; + private OnPreferenceChangeListener mStopListener; + + public LabeledSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + + super(context, attrs, defStyleAttr, defStyleRes); + setLayoutResource(R.layout.preference_labeled_slider); + + final TypedArray styledAttrs = context.obtainStyledAttributes(attrs, + R.styleable.LabeledSeekBarPreference); + mTextStartId = styledAttrs.getResourceId( + R.styleable.LabeledSeekBarPreference_textStart, + R.string.summary_placeholder); + mTextEndId = styledAttrs.getResourceId( + R.styleable.LabeledSeekBarPreference_textEnd, + R.string.summary_placeholder); + mTickMarkId = styledAttrs.getResourceId( + R.styleable.LabeledSeekBarPreference_tickMark, /* defValue= */ 0); + styledAttrs.recycle(); + } + + public LabeledSeekBarPreference(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.seekBarPreferenceStyle, 0); + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + + final TextView startText = (TextView) holder.findViewById(android.R.id.text1); + final TextView endText = (TextView) holder.findViewById(android.R.id.text2); + startText.setText(mTextStartId); + endText.setText(mTextEndId); + + if (mTickMarkId != 0) { + final Drawable tickMark = getContext().getDrawable(mTickMarkId); + final SeekBar seekBar = (SeekBar) holder.findViewById( + com.android.internal.R.id.seekbar); + seekBar.setTickMark(tickMark); + } + } + + public void setOnPreferenceChangeStopListener(OnPreferenceChangeListener listener) { + mStopListener = listener; + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + super.onStopTrackingTouch(seekBar); + + if (mStopListener != null) { + mStopListener.onPreferenceChange(this, seekBar.getProgress()); + } + } +} + diff --git a/packages/SettingsLib/SeekBarPreference/src/com/android/settingslib/widget/SeekBarPreference.java b/packages/SettingsLib/SeekBarPreference/src/com/android/settingslib/widget/SeekBarPreference.java new file mode 100644 index 0000000000000..6f32618d5266a --- /dev/null +++ b/packages/SettingsLib/SeekBarPreference/src/com/android/settingslib/widget/SeekBarPreference.java @@ -0,0 +1,428 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.widget; + +import static android.view.HapticFeedbackConstants.CLOCK_TICK; + +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.View; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; + +import androidx.annotation.RequiresApi; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + +/** + * Based on android.preference.SeekBarPreference, but uses support preference as base. + */ +public class SeekBarPreference extends Preference + implements OnSeekBarChangeListener, View.OnKeyListener { + + public static final int HAPTIC_FEEDBACK_MODE_NONE = 0; + public static final int HAPTIC_FEEDBACK_MODE_ON_TICKS = 1; + public static final int HAPTIC_FEEDBACK_MODE_ON_ENDS = 2; + protected int mDefaultProgress = -1; + protected SeekBar mSeekBar; + private int mProgress; + private int mMax; + private int mMin; + private boolean mTrackingTouch; + private boolean mContinuousUpdates; + private int mHapticFeedbackMode = HAPTIC_FEEDBACK_MODE_NONE; + private boolean mShouldBlink; + private int mAccessibilityRangeInfoType = AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_INT; + private CharSequence mSeekBarContentDescription; + private CharSequence mSeekBarStateDescription; + + public SeekBarPreference( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + TypedArray a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.ProgressBar, defStyleAttr, defStyleRes); + setMax(a.getInt(com.android.internal.R.styleable.ProgressBar_max, mMax)); + setMin(a.getInt(com.android.internal.R.styleable.ProgressBar_min, mMin)); + a.recycle(); + + a = context.obtainStyledAttributes( + attrs, com.android.internal.R.styleable.Preference, defStyleAttr, defStyleRes); + final boolean isSelectable = a.getBoolean( + R.styleable.Preference_android_selectable, false); + setSelectable(isSelectable); + a.recycle(); + + setLayoutResource(R.layout.preference_widget_slider); + } + + public SeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public SeekBarPreference(Context context, AttributeSet attrs) { + this(context, attrs, androidx.preference.R.attr.seekBarPreferenceStyle); + } + + public SeekBarPreference(Context context) { + this(context, null); + } + + @Override + public void onBindViewHolder(PreferenceViewHolder view) { + super.onBindViewHolder(view); + view.itemView.setOnKeyListener(this); + mSeekBar = (SeekBar) view.findViewById(com.android.internal.R.id.seekbar); + + if (mSeekBar == null) { + return; + } + + mSeekBar.setOnSeekBarChangeListener(this); + mSeekBar.setMax(mMax); + mSeekBar.setMin(mMin); + mSeekBar.setProgress(mProgress); + mSeekBar.setEnabled(isEnabled()); + final CharSequence title = getTitle(); + if (!TextUtils.isEmpty(mSeekBarContentDescription)) { + mSeekBar.setContentDescription(mSeekBarContentDescription); + } else if (!TextUtils.isEmpty(title)) { + mSeekBar.setContentDescription(title); + } + if (!TextUtils.isEmpty(mSeekBarStateDescription) && (Build.VERSION.SDK_INT + >= Build.VERSION_CODES.R)) { + mSeekBar.setStateDescription(mSeekBarStateDescription); + } + if (mShouldBlink) { + View v = view.itemView; + v.post(() -> { + if (v.getBackground() != null) { + final int centerX = v.getWidth() / 2; + final int centerY = v.getHeight() / 2; + v.getBackground().setHotspot(centerX, centerY); + } + v.setPressed(true); + v.setPressed(false); + mShouldBlink = false; + }); + } + mSeekBar.setAccessibilityDelegate(new View.AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo(View view, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(view, info); + // Update the range info with the correct type + final AccessibilityNodeInfo.RangeInfo rangeInfo = info.getRangeInfo(); + if (rangeInfo != null) { + info.setRangeInfo(AccessibilityNodeInfo.RangeInfo.obtain( + mAccessibilityRangeInfoType, rangeInfo.getMin(), + rangeInfo.getMax(), rangeInfo.getCurrent())); + } + } + }); + } + + @Override + public CharSequence getSummary() { + return null; + } + + @Override + protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { + setProgress(restoreValue ? getPersistedInt(mProgress) + : (Integer) defaultValue); + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getInt(index, 0); + } + + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (event.getAction() != KeyEvent.ACTION_DOWN) { + return false; + } + + final SeekBar seekBar = v.findViewById(com.android.internal.R.id.seekbar); + if (seekBar == null) { + return false; + } + return seekBar.onKeyDown(keyCode, event); + } + + public int getMax() { + return mMax; + } + + /** + * Sets the upper bound on the {@link SeekBar}. + * + * @param max The upper bound to set + */ + public void setMax(int max) { + if (max != mMax) { + mMax = max; + notifyChanged(); + } + } + + public int getMin() { + return mMin; + } + + /** + * Sets the lower bound on the {@link SeekBar}. + * + * @param min The lower bound to set + */ + public void setMin(int min) { + if (min != mMin) { + mMin = min; + notifyChanged(); + } + } + + /** + * Sets the progress point to draw a single tick mark representing a default value. + */ + public void setDefaultProgress(int defaultProgress) { + if (mDefaultProgress != defaultProgress) { + mDefaultProgress = defaultProgress; + } + } + + /** + * When {@code continuousUpdates} is true, update the persisted setting immediately as the thumb + * is dragged along the SeekBar. Otherwise, only update the value of the setting when the thumb + * is dropped. + */ + public void setContinuousUpdates(boolean continuousUpdates) { + mContinuousUpdates = continuousUpdates; + } + + /** + * Sets the haptic feedback mode. HAPTIC_FEEDBACK_MODE_ON_TICKS means to perform haptic feedback + * as the SeekBar's progress is updated; HAPTIC_FEEDBACK_MODE_ON_ENDS means to perform haptic + * feedback as the SeekBar's progress value is equal to the min/max value. + * + * @param hapticFeedbackMode the haptic feedback mode. + */ + public void setHapticFeedbackMode(int hapticFeedbackMode) { + mHapticFeedbackMode = hapticFeedbackMode; + } + + private void setProgress(int progress, boolean notifyChanged) { + if (progress > mMax) { + progress = mMax; + } + if (progress < mMin) { + progress = mMin; + } + if (progress != mProgress) { + mProgress = progress; + persistInt(progress); + if (notifyChanged) { + notifyChanged(); + } + } + } + + public int getProgress() { + return mProgress; + } + + /** + * Sets the current progress of the {@link SeekBar}. + * + * @param progress The current progress of the {@link SeekBar} + */ + public void setProgress(int progress) { + setProgress(progress, true); + } + + /** + * Persist the seekBar's progress value if callChangeListener + * returns true, otherwise set the seekBar's progress to the stored value + */ + void syncProgress(SeekBar seekBar) { + int progress = seekBar.getProgress(); + if (progress != mProgress) { + if (callChangeListener(progress)) { + setProgress(progress, false); + switch (mHapticFeedbackMode) { + case HAPTIC_FEEDBACK_MODE_ON_TICKS: + seekBar.performHapticFeedback(CLOCK_TICK); + break; + case HAPTIC_FEEDBACK_MODE_ON_ENDS: + if (progress == mMax || progress == mMin) { + seekBar.performHapticFeedback(CLOCK_TICK); + } + break; + } + } else { + seekBar.setProgress(mProgress); + } + } + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser && (mContinuousUpdates || !mTrackingTouch)) { + syncProgress(seekBar); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + mTrackingTouch = true; + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + mTrackingTouch = false; + if (seekBar.getProgress() != mProgress) { + syncProgress(seekBar); + } + } + + /** + * Specify the type of range this seek bar represents. + * + * @param rangeInfoType The type of range to be shared with accessibility + * @see android.view.accessibility.AccessibilityNodeInfo.RangeInfo + */ + public void setAccessibilityRangeInfoType(int rangeInfoType) { + mAccessibilityRangeInfoType = rangeInfoType; + } + + /** + * Specify the content description for this seek bar represents. + * + * @param contentDescription the content description of seek bar + */ + public void setSeekBarContentDescription(CharSequence contentDescription) { + mSeekBarContentDescription = contentDescription; + if (mSeekBar != null) { + mSeekBar.setContentDescription(contentDescription); + } + } + + /** + * Specify the state description for this seek bar represents. + * + * @param stateDescription the state description of seek bar + */ + @RequiresApi(Build.VERSION_CODES.R) + public void setSeekBarStateDescription(CharSequence stateDescription) { + mSeekBarStateDescription = stateDescription; + if (mSeekBar != null) { + mSeekBar.setStateDescription(stateDescription); + } + } + + @Override + protected Parcelable onSaveInstanceState() { + /* + * Suppose a client uses this preference type without persisting. We + * must save the instance state so it is able to, for example, survive + * orientation changes. + */ + + final Parcelable superState = super.onSaveInstanceState(); + if (isPersistent()) { + // No need to save instance state since it's persistent + return superState; + } + + // Save the instance state + final SavedState myState = new SavedState(superState); + myState.mProgress = mProgress; + myState.mMax = mMax; + myState.mMin = mMin; + return myState; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (!state.getClass().equals(SavedState.class)) { + // Didn't save state for us in onSaveInstanceState + super.onRestoreInstanceState(state); + return; + } + + // Restore the instance state + SavedState myState = (SavedState) state; + super.onRestoreInstanceState(myState.getSuperState()); + mProgress = myState.mProgress; + mMax = myState.mMax; + mMin = myState.mMin; + notifyChanged(); + } + + /** + * SavedState, a subclass of {@link BaseSavedState}, will store the state + * of MyPreference, a subclass of Preference. + *

+ * It is important to always call through to super methods. + */ + private static class SavedState extends BaseSavedState { + @SuppressWarnings("unused") + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + int mProgress; + int mMax; + int mMin; + + SavedState(Parcel source) { + super(source); + + // Restore the click counter + mProgress = source.readInt(); + mMax = source.readInt(); + mMin = source.readInt(); + } + + SavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + + // Save the click counter + dest.writeInt(mProgress); + dest.writeInt(mMax); + dest.writeInt(mMin); + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/Restrictable.java b/packages/SettingsLib/src/com/android/settingslib/Restrictable.java new file mode 100644 index 0000000000000..dfe84233fe839 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/Restrictable.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settingslib; +import android.os.UserHandle; +/** + * A collection of API making a Preference "restrictable" + */ +public interface Restrictable { + /** + * Returns RestrictedPreferenceHelper + * @return + */ + RestrictedPreferenceHelper getHelper(); + /** + * call preference notifyChanged() + */ + void notifyPreferenceChanged(); + /** + * Set if show restriction message in Preference summary or not. + * @param useSummary. + */ + default void useAdminDisabledSummary(boolean useSummary) { + getHelper().useAdminDisabledSummary(useSummary); + } + /** + * Set the user restriction and disable this preference. + * + * @param userRestriction constant from {@link android.os.UserManager} + */ + default void checkRestrictionAndSetDisabled(String userRestriction) { + getHelper().checkRestrictionAndSetDisabled(userRestriction, UserHandle.myUserId()); + } + /** + * Set the user restriction and disable this preference for the given user. + * + * @param userRestriction constant from {@link android.os.UserManager} + * @param userId user to check the restriction for. + */ + default void checkRestrictionAndSetDisabled(String userRestriction, int userId) { + getHelper().checkRestrictionAndSetDisabled(userRestriction, userId); + } + /** + * Disable preference based on the enforce admin. + * + * @param admin details of the admin who enforced the restriction. If it is {@code null}, then + * this preference will be enabled. Otherwise, it will be disabled. + */ + default void setDisabledByAdmin(RestrictedLockUtils.EnforcedAdmin admin) { + if (getHelper().setDisabledByAdmin(admin)) { + notifyPreferenceChanged(); + } + } + /** + * Check whether this preference is disabled by admin. + * + * @return true if this preference is disabled by admin. + */ + default boolean isDisabledByAdmin() { + return getHelper().isDisabledByAdmin(); + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedPreference.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedPreference.java index fc8b5879c5fa1..bd0f33bcb56af 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedPreference.java @@ -16,10 +16,7 @@ package com.android.settingslib; -import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; - import android.content.Context; -import android.os.UserHandle; import android.util.AttributeSet; import android.view.View; @@ -33,8 +30,8 @@ import com.android.settingslib.widget.TwoTargetPreference; * Preference class that supports being disabled by a user restriction * set by a device admin. */ -public class RestrictedPreference extends TwoTargetPreference { - RestrictedPreferenceHelper mHelper; +public class RestrictedPreference extends TwoTargetPreference implements Restrictable { + private RestrictedPreferenceHelper mHelper; public RestrictedPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { @@ -82,8 +79,14 @@ public class RestrictedPreference extends TwoTargetPreference { } } - public void useAdminDisabledSummary(boolean useSummary) { - mHelper.useAdminDisabledSummary(useSummary); + @Override + public RestrictedPreferenceHelper getHelper() { + return mHelper; + } + + @Override + public void notifyPreferenceChanged() { + notifyChanged(); } @Override @@ -92,14 +95,6 @@ public class RestrictedPreference extends TwoTargetPreference { super.onAttachedToHierarchy(preferenceManager); } - public void checkRestrictionAndSetDisabled(String userRestriction) { - mHelper.checkRestrictionAndSetDisabled(userRestriction, UserHandle.myUserId()); - } - - public void checkRestrictionAndSetDisabled(String userRestriction, int userId) { - mHelper.checkRestrictionAndSetDisabled(userRestriction, userId); - } - @Override public void setEnabled(boolean enabled) { if (enabled && isDisabledByAdmin()) { @@ -108,14 +103,4 @@ public class RestrictedPreference extends TwoTargetPreference { } super.setEnabled(enabled); } - - public void setDisabledByAdmin(EnforcedAdmin admin) { - if (mHelper.setDisabledByAdmin(admin)) { - notifyChanged(); - } - } - - public boolean isDisabledByAdmin() { - return mHelper.isDisabledByAdmin(); - } } diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedSeekBarPreference.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedSeekBarPreference.java new file mode 100644 index 0000000000000..486dfef9f7381 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedSeekBarPreference.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; + +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceViewHolder; + +import com.android.settingslib.widget.DefaultIndicatorSeekBar; +import com.android.settingslib.widget.SeekBarPreference; + +/** + * Based on android.preference.SeekBarPreference, but uses support preference as base. + */ +public class RestrictedSeekBarPreference extends SeekBarPreference implements Restrictable { + + private CharSequence mSeekBarStateDescription; + private RestrictedPreferenceHelper mHelper; + + public RestrictedSeekBarPreference( + Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + mHelper = new RestrictedPreferenceHelper(context, this, attrs); + + final int secondTargetResId = getSecondTargetResId(); + if (secondTargetResId != 0) { + setWidgetLayoutResource(secondTargetResId); + } + } + + public RestrictedSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public RestrictedSeekBarPreference(Context context, AttributeSet attrs) { + this(context, attrs, androidx.preference.R.attr.seekBarPreferenceStyle); + } + + public RestrictedSeekBarPreference(Context context) { + this(context, null); + } + + @Override + public boolean isSelectable() { + if (isDisabledByAdmin()) { + return true; + } else { + return super.isSelectable(); + } + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + if (mSeekBar instanceof DefaultIndicatorSeekBar) { + ((DefaultIndicatorSeekBar) mSeekBar).setDefaultProgress(mDefaultProgress); + } + + mHelper.onBindViewHolder(holder); + final View restrictedIcon = holder.findViewById(R.id.restricted_icon); + if (restrictedIcon != null) { + restrictedIcon.setVisibility(isDisabledByAdmin() ? View.VISIBLE : View.GONE); + } + + final View widgetFrame = holder.findViewById(android.R.id.widget_frame); + if (widgetFrame != null) { + widgetFrame.setVisibility(shouldHideSecondTarget() ? View.GONE : View.VISIBLE); + } + } + + @Override + public void performClick() { + if (!mHelper.performClick()) { + super.performClick(); + } + } + + @Override + public void setEnabled(boolean enabled) { + if (enabled && isDisabledByAdmin()) { + mHelper.setDisabledByAdmin(null); + return; + } + super.setEnabled(enabled); + } + + /** + * Sets the progress point to draw a single tick mark representing a default value. + */ + public void setDefaultProgress(int defaultProgress) { + if (mDefaultProgress != defaultProgress) { + mDefaultProgress = defaultProgress; + if (mSeekBar instanceof DefaultIndicatorSeekBar) { + ((DefaultIndicatorSeekBar) mSeekBar).setDefaultProgress(mDefaultProgress); + } + } + } + + @Override + public RestrictedPreferenceHelper getHelper() { + return mHelper; + } + + @Override + public void notifyPreferenceChanged() { + notifyChanged(); + } + + @Override + protected void onAttachedToHierarchy(PreferenceManager preferenceManager) { + mHelper.onAttachedToHierarchy(); + super.onAttachedToHierarchy(preferenceManager); + } + + protected int getSecondTargetResId() { + return R.layout.restricted_icon; + } + + protected boolean shouldHideSecondTarget() { + return !isDisabledByAdmin(); + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java index 5c05a1bd67226..cdbeda8125914 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java @@ -16,11 +16,8 @@ package com.android.settingslib; -import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; - import android.content.Context; import android.content.res.TypedArray; -import android.os.UserHandle; import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; @@ -37,10 +34,10 @@ import androidx.preference.SwitchPreference; * Version of SwitchPreference that can be disabled by a device admin * using a user restriction. */ -public class RestrictedSwitchPreference extends SwitchPreference { - RestrictedPreferenceHelper mHelper; - boolean mUseAdditionalSummary = false; - CharSequence mRestrictedSwitchSummary; +public class RestrictedSwitchPreference extends SwitchPreference implements Restrictable { + private RestrictedPreferenceHelper mHelper; + private boolean mUseAdditionalSummary = false; + private CharSequence mRestrictedSwitchSummary; private int mIconSize; public RestrictedSwitchPreference(Context context, AttributeSet attrs, @@ -153,8 +150,14 @@ public class RestrictedSwitchPreference extends SwitchPreference { } } - public void useAdminDisabledSummary(boolean useSummary) { - mHelper.useAdminDisabledSummary(useSummary); + @Override + public RestrictedPreferenceHelper getHelper() { + return mHelper; + } + + @Override + public void notifyPreferenceChanged() { + notifyChanged(); } @Override @@ -163,14 +166,6 @@ public class RestrictedSwitchPreference extends SwitchPreference { super.onAttachedToHierarchy(preferenceManager); } - public void checkRestrictionAndSetDisabled(String userRestriction) { - mHelper.checkRestrictionAndSetDisabled(userRestriction, UserHandle.myUserId()); - } - - public void checkRestrictionAndSetDisabled(String userRestriction, int userId) { - mHelper.checkRestrictionAndSetDisabled(userRestriction, userId); - } - @Override public void setEnabled(boolean enabled) { if (enabled && isDisabledByAdmin()) { @@ -179,14 +174,4 @@ public class RestrictedSwitchPreference extends SwitchPreference { } super.setEnabled(enabled); } - - public void setDisabledByAdmin(EnforcedAdmin admin) { - if (mHelper.setDisabledByAdmin(admin)) { - notifyChanged(); - } - } - - public boolean isDisabledByAdmin() { - return mHelper.isDisabledByAdmin(); - } } diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedTopLevelPreference.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedTopLevelPreference.java index 0096015aa875f..4da0e5e020d13 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedTopLevelPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedTopLevelPreference.java @@ -16,10 +16,7 @@ package com.android.settingslib; -import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; - import android.content.Context; -import android.os.UserHandle; import android.util.AttributeSet; import androidx.core.content.res.TypedArrayUtils; @@ -28,7 +25,7 @@ import androidx.preference.PreferenceManager; import androidx.preference.PreferenceViewHolder; /** Top level preference that can be disabled by a device admin using a user restriction. */ -public class RestrictedTopLevelPreference extends Preference { +public class RestrictedTopLevelPreference extends Preference implements Restrictable { private RestrictedPreferenceHelper mHelper; public RestrictedTopLevelPreference(Context context, AttributeSet attrs, @@ -69,23 +66,14 @@ public class RestrictedTopLevelPreference extends Preference { super.onAttachedToHierarchy(preferenceManager); } - /** - * Set the user restriction and disable this preference. - * - * @param userRestriction constant from {@link android.os.UserManager} - */ - public void checkRestrictionAndSetDisabled(String userRestriction) { - mHelper.checkRestrictionAndSetDisabled(userRestriction, UserHandle.myUserId()); + @Override + public RestrictedPreferenceHelper getHelper() { + return mHelper; } - /** - * Set the user restriction and disable this preference for the given user. - * - * @param userRestriction constant from {@link android.os.UserManager} - * @param userId user to check the restriction for. - */ - public void checkRestrictionAndSetDisabled(String userRestriction, int userId) { - mHelper.checkRestrictionAndSetDisabled(userRestriction, userId); + @Override + public void notifyPreferenceChanged() { + notifyChanged(); } @Override @@ -96,25 +84,4 @@ public class RestrictedTopLevelPreference extends Preference { } super.setEnabled(enabled); } - - /** - * Check whether this preference is disabled by admin. - * - * @return true if this preference is disabled by admin. - */ - public boolean isDisabledByAdmin() { - return mHelper.isDisabledByAdmin(); - } - - /** - * Disable preference based on the enforce admin. - * - * @param admin details of the admin who enforced the restriction. If it is {@code null}, then - * this preference will be enabled. Otherwise, it will be disabled. - */ - public void setDisabledByAdmin(EnforcedAdmin admin) { - if (mHelper.setDisabledByAdmin(admin)) { - notifyChanged(); - } - } } diff --git a/packages/SettingsLib/src/com/android/settingslib/widget/DefaultIndicatorSeekBar.java b/packages/SettingsLib/src/com/android/settingslib/widget/DefaultIndicatorSeekBar.java new file mode 100644 index 0000000000000..7a639784de2d2 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/widget/DefaultIndicatorSeekBar.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.widget; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.widget.SeekBar; + +/** + * Customized default indicator seekbar + */ +public class DefaultIndicatorSeekBar extends SeekBar { + + private int mDefaultProgress = -1; + + public DefaultIndicatorSeekBar(Context context) { + super(context); + } + + public DefaultIndicatorSeekBar(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public DefaultIndicatorSeekBar(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public DefaultIndicatorSeekBar(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + /** + * N.B. Only draws the default indicator tick mark, NOT equally spaced tick marks. + */ + @Override + protected void drawTickMarks(Canvas canvas) { + if (isEnabled() && mDefaultProgress <= getMax() && mDefaultProgress >= getMin()) { + final Drawable defaultIndicator = getTickMark(); + + // Adjust the drawable's bounds to center it at the point where it's drawn. + final int w = defaultIndicator.getIntrinsicWidth(); + final int h = defaultIndicator.getIntrinsicHeight(); + final int halfW = w >= 0 ? w / 2 : 1; + final int halfH = h >= 0 ? h / 2 : 1; + defaultIndicator.setBounds(-halfW, -halfH, halfW, halfH); + + // This mimics the computation of the thumb position, to get the true "default." + final int availableWidth = getWidth() - mPaddingLeft - mPaddingRight; + final int range = getMax() - getMin(); + final float scale = range > 0f ? mDefaultProgress / (float) range : 0f; + final int offset = (int) ((scale * availableWidth) + 0.5f); + final int indicatorPosition = isLayoutRtl() && getMirrorForRtl() + ? availableWidth - offset + mPaddingRight : offset + mPaddingLeft; + + final int saveCount = canvas.save(); + canvas.translate(indicatorPosition, getHeight() / 2); + defaultIndicator.draw(canvas); + canvas.restoreToCount(saveCount); + } + } + + /** + * N.B. This sets the default *unadjusted* progress, i.e. in the SeekBar's [0 - max] terms. + */ + public void setDefaultProgress(int defaultProgress) { + if (mDefaultProgress != defaultProgress) { + mDefaultProgress = defaultProgress; + invalidate(); + } + } + + public int getDefaultProgress() { + return mDefaultProgress; + } +} diff --git a/packages/SettingsLib/tests/robotests/res/xml-mcc998/seekbar_preference.xml b/packages/SettingsLib/tests/robotests/res/xml-mcc998/seekbar_preference.xml new file mode 100644 index 0000000000000..ad5775536bf4e --- /dev/null +++ b/packages/SettingsLib/tests/robotests/res/xml-mcc998/seekbar_preference.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/packages/SettingsLib/tests/robotests/res/xml-mcc999/seekbar_preference.xml b/packages/SettingsLib/tests/robotests/res/xml-mcc999/seekbar_preference.xml new file mode 100644 index 0000000000000..28527fc90c856 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/res/xml-mcc999/seekbar_preference.xml @@ -0,0 +1,24 @@ + + + + + + diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/DefaultIndicatorSeekBarTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/DefaultIndicatorSeekBarTest.java new file mode 100644 index 0000000000000..86625481b1889 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/DefaultIndicatorSeekBarTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.widget; + +import static org.junit.Assert.assertEquals; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class DefaultIndicatorSeekBarTest { + + private DefaultIndicatorSeekBar mDefaultIndicatorSeekBar; + + @Before + public void setUp() { + mDefaultIndicatorSeekBar = new DefaultIndicatorSeekBar(RuntimeEnvironment.application); + mDefaultIndicatorSeekBar.setMax(100); + } + + @After + public void tearDown() { + mDefaultIndicatorSeekBar = null; + } + + @Test + public void defaultProgress_setSucceeds() { + mDefaultIndicatorSeekBar.setDefaultProgress(40); + assertEquals(40, mDefaultIndicatorSeekBar.getDefaultProgress()); + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/LabeledSeekBarPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/LabeledSeekBarPreferenceTest.java new file mode 100644 index 0000000000000..6dd72b460f6dd --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/LabeledSeekBarPreferenceTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.widget; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.SeekBar; + +import androidx.preference.Preference; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class LabeledSeekBarPreferenceTest { + + private Context mContext; + private SeekBar mSeekBar; + private LabeledSeekBarPreference mSeekBarPreference; + + @Mock + private Preference.OnPreferenceChangeListener mListener; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application; + mSeekBarPreference = new LabeledSeekBarPreference(mContext, null); + LayoutInflater inflater = LayoutInflater.from(mContext); + final View view = + inflater.inflate(mSeekBarPreference.getLayoutResource(), + new LinearLayout(mContext), false); + mSeekBar = view.findViewById(com.android.internal.R.id.seekbar); + } + + @Test + public void seekBarPreferenceOnStopTrackingTouch_callsListener() { + mSeekBar.setProgress(2); + + mSeekBarPreference.setOnPreferenceChangeStopListener(mListener); + mSeekBarPreference.onStopTrackingTouch(mSeekBar); + + verify(mListener, times(1)).onPreferenceChange(mSeekBarPreference, 2); + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/SeekBarPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/SeekBarPreferenceTest.java new file mode 100644 index 0000000000000..19ee3f940aa2b --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/SeekBarPreferenceTest.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.widget; + +import static android.view.HapticFeedbackConstants.CLOCK_TICK; +import static android.view.HapticFeedbackConstants.CONTEXT_CLICK; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; + +import android.content.Context; +import android.os.Bundle; +import android.os.Parcelable; +import android.widget.SeekBar; + +import androidx.preference.PreferenceFragmentCompat; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.androidx.fragment.FragmentController; + +@RunWith(RobolectricTestRunner.class) +public class SeekBarPreferenceTest { + + private static final int MAX = 75; + private static final int MIN = 5; + private static final int PROGRESS = 16; + private static final int NEW_PROGRESS = 17; + + private Context mContext; + private SeekBarPreference mSeekBarPreference; + private SeekBar mSeekBar; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + + mSeekBarPreference = spy(new SeekBarPreference(mContext)); + mSeekBarPreference.setMax(MAX); + mSeekBarPreference.setMin(MIN); + mSeekBarPreference.setProgress(PROGRESS); + mSeekBarPreference.setPersistent(false); + mSeekBarPreference.setHapticFeedbackMode(SeekBarPreference.HAPTIC_FEEDBACK_MODE_NONE); + + mSeekBar = new SeekBar(mContext); + mSeekBar.setMax(MAX); + mSeekBar.setMin(MIN); + } + + @Test + public void testSaveAndRestoreInstanceState() { + final Parcelable parcelable = mSeekBarPreference.onSaveInstanceState(); + + final SeekBarPreference preference = new SeekBarPreference(mContext); + preference.onRestoreInstanceState(parcelable); + + assertThat(preference.getMax()).isEqualTo(MAX); + assertThat(preference.getMin()).isEqualTo(MIN); + assertThat(preference.getProgress()).isEqualTo(PROGRESS); + } + + @Test + @Config(qualifiers = "mcc998") + @Ignore("b/188888268") + public void isSelectable_default_returnFalse() { + final PreferenceFragmentCompat fragment = FragmentController.of(new TestFragment(), + new Bundle()) + .create() + .start() + .resume() + .get(); + + final SeekBarPreference seekBarPreference = fragment.findPreference("seek_bar"); + + assertThat(seekBarPreference.isSelectable()).isFalse(); + } + + @Test + @Config(qualifiers = "mcc999") + @Ignore("b/188888268") + public void isSelectable_selectableInXml_returnTrue() { + final PreferenceFragmentCompat fragment = FragmentController.of(new TestFragment(), + new Bundle()) + .create() + .start() + .resume() + .get(); + + final SeekBarPreference seekBarPreference = fragment.findPreference("seek_bar"); + + assertThat(seekBarPreference.isSelectable()).isTrue(); + } + + @Test + public void onProgressChanged_hapticFeedbackModeNone_clockTickFeedbackNotPerformed() { + mSeekBar.setProgress(NEW_PROGRESS); + when(mSeekBarPreference.callChangeListener(anyInt())).thenReturn(true); + mSeekBar.performHapticFeedback(CONTEXT_CLICK); + + mSeekBarPreference.onProgressChanged(mSeekBar, NEW_PROGRESS, true); + + assertThat(shadowOf(mSeekBar).lastHapticFeedbackPerformed()).isNotEqualTo(CLOCK_TICK); + } + + @Test + public void onProgressChanged_hapticFeedbackModeOnTicks_clockTickFeedbackPerformed() { + mSeekBarPreference.setHapticFeedbackMode(SeekBarPreference.HAPTIC_FEEDBACK_MODE_ON_TICKS); + mSeekBar.setProgress(NEW_PROGRESS); + when(mSeekBarPreference.callChangeListener(anyInt())).thenReturn(true); + mSeekBar.performHapticFeedback(CONTEXT_CLICK); + + mSeekBarPreference.onProgressChanged(mSeekBar, NEW_PROGRESS, true); + + assertThat(shadowOf(mSeekBar).lastHapticFeedbackPerformed()).isEqualTo(CLOCK_TICK); + } + + @Test + public void onProgressChanged_hapticFeedbackModeOnEnds_clockTickFeedbackNotPerformed() { + mSeekBarPreference.setHapticFeedbackMode(SeekBarPreference.HAPTIC_FEEDBACK_MODE_ON_ENDS); + mSeekBar.setProgress(NEW_PROGRESS); + when(mSeekBarPreference.callChangeListener(anyInt())).thenReturn(true); + mSeekBar.performHapticFeedback(CONTEXT_CLICK); + + mSeekBarPreference.onProgressChanged(mSeekBar, NEW_PROGRESS, true); + + assertThat(shadowOf(mSeekBar).lastHapticFeedbackPerformed()).isNotEqualTo(CLOCK_TICK); + } + + @Test + public void onProgressChanged_hapticFeedbackModeOnEndsAndMinValue_clockTickFeedbackPerformed() { + mSeekBarPreference.setHapticFeedbackMode(SeekBarPreference.HAPTIC_FEEDBACK_MODE_ON_ENDS); + mSeekBar.setProgress(MIN); + when(mSeekBarPreference.callChangeListener(anyInt())).thenReturn(true); + mSeekBar.performHapticFeedback(CONTEXT_CLICK); + + mSeekBarPreference.onProgressChanged(mSeekBar, MIN, true); + + assertThat(shadowOf(mSeekBar).lastHapticFeedbackPerformed()).isEqualTo(CLOCK_TICK); + } + + public static class TestFragment extends PreferenceFragmentCompat { + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.seekbar_preference); + } + } +}