diff options
author | 2024-09-16 11:41:36 +0000 | |
---|---|---|
committer | 2024-09-30 19:22:23 +0000 | |
commit | 2d38fe44a9b1dc9b4c8aa790b45019f941d8c55f (patch) | |
tree | 17ad268dc258ac47398467ca2e16fb2bac608d26 | |
parent | a823dd5482b7c839a8c941c81e46240dd0d37ea3 (diff) |
[Expressive design] create SliderPreference
- use material slider
- support Continuous/Discrete slider
- support adding left/right icon
- support adding left/right text
Bug: 367183958
Test: manual
Flag: EXEMPT resource only update
Change-Id: Id3c5a4a924c533546fa2a32d4eeaca8a59d6b606
11 files changed, 1170 insertions, 0 deletions
diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp index e77c2a01b996..cee7d8f9a428 100644 --- a/packages/SettingsLib/Android.bp +++ b/packages/SettingsLib/Android.bp @@ -53,6 +53,7 @@ android_library { "SettingsLibSelectorWithWidgetPreference", "SettingsLibSettingsSpinner", "SettingsLibSettingsTransition", + "SettingsLibSliderPreference", "SettingsLibTopIntroPreference", "SettingsLibTwoTargetPreference", "SettingsLibUsageProgressBarPreference", diff --git a/packages/SettingsLib/SliderPreference/Android.bp b/packages/SettingsLib/SliderPreference/Android.bp new file mode 100644 index 000000000000..46528c1a584f --- /dev/null +++ b/packages/SettingsLib/SliderPreference/Android.bp @@ -0,0 +1,31 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_library { + name: "SettingsLibSliderPreference", + use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], + + srcs: ["src/**/*.java"], + resource_dirs: ["res"], + + static_libs: [ + "androidx.annotation_annotation", + "androidx.preference_preference", + "SettingsLibSettingsTheme", + ], + + sdk_version: "system_current", + min_sdk_version: "23", + apex_available: [ + "//apex_available:platform", + ], +} diff --git a/packages/SettingsLib/SliderPreference/AndroidManifest.xml b/packages/SettingsLib/SliderPreference/AndroidManifest.xml new file mode 100644 index 000000000000..04a67524124d --- /dev/null +++ b/packages/SettingsLib/SliderPreference/AndroidManifest.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.settingslib.widget.preference.slider"> + + <uses-sdk android:minSdkVersion="23" /> + +</manifest> diff --git a/packages/SettingsLib/SliderPreference/res/layout/settingslib_expressive_layout_slider.xml b/packages/SettingsLib/SliderPreference/res/layout/settingslib_expressive_layout_slider.xml new file mode 100644 index 000000000000..a9cc2c405f8d --- /dev/null +++ b/packages/SettingsLib/SliderPreference/res/layout/settingslib_expressive_layout_slider.xml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clipChildren="false" + android:gravity="center_vertical"> + + <FrameLayout + android:id="@+id/icon_start_frame" + android:layout_width="@dimen/settingslib_expressive_space_medium4" + android:layout_height="@dimen/settingslib_expressive_space_medium4" + android:clipChildren="false" + android:focusable="true" + android:visibility="gone"> + + <ImageView + android:id="@+id/icon_start" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:background="?android:attr/selectableItemBackgroundBorderless" + android:adjustViewBounds="true" + android:focusable="false" + android:tint="?android:attr/textColorPrimary" + android:tintMode="src_in" /> + </FrameLayout> + + <com.google.android.material.slider.Slider + android:id="@+id/slider" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:theme="@style/Theme.MaterialComponents.DayNight" /> + + <FrameLayout + android:id="@+id/icon_end_frame" + android:layout_width="@dimen/settingslib_expressive_space_medium4" + android:layout_height="@dimen/settingslib_expressive_space_medium4" + android:clipChildren="false" + android:focusable="true" + android:visibility="gone"> + + <ImageView + android:id="@+id/icon_end" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:background="?android:attr/selectableItemBackgroundBorderless" + android:adjustViewBounds="true" + android:focusable="false" + android:tint="?android:attr/textColorPrimary" + android:tintMode="src_in" /> + </FrameLayout> +</LinearLayout> diff --git a/packages/SettingsLib/SliderPreference/res/layout/settingslib_expressive_preference_slider.xml b/packages/SettingsLib/SliderPreference/res/layout/settingslib_expressive_preference_slider.xml new file mode 100644 index 000000000000..27720919b5e6 --- /dev/null +++ b/packages/SettingsLib/SliderPreference/res/layout/settingslib_expressive_preference_slider.xml @@ -0,0 +1,99 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 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. + --> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:orientation="horizontal" + android:background="?android:attr/selectableItemBackground" + android:clipToPadding="false" + android:baselineAligned="false"> + + <include layout="@layout/settingslib_icon_frame"/> + + <RelativeLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:paddingVertical="@dimen/settingslib_expressive_space_small1"> + + <TextView + android:id="@android:id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="start" + android:textAlignment="viewStart" + android:textAppearance="?android:attr/textAppearanceListItem" + android:maxLines="2" + android:ellipsize="marquee"/> + + <TextView + android:id="@android:id/summary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@android:id/title" + android:layout_alignLeft="@android:id/title" + android:layout_alignStart="@android:id/title" + android:layout_gravity="start" + android:textAlignment="viewStart" + android:textAppearance="?android:attr/textAppearanceListItemSecondary" + android:textColor="?android:attr/textColorSecondary" + android:maxLines="10"/> + + <include + layout="@layout/settingslib_expressive_layout_slider" + android:id="@+id/slider_frame" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_alignStart="@android:id/summary" + android:layout_below="@android:id/summary" /> + + <LinearLayout + android:id="@+id/label_frame" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/slider_frame" + android:orientation="horizontal" + android:visibility="gone"> + + <TextView + android:id="@android:id/text1" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_gravity="start|top" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="?android:attr/textColorSecondary" + android:gravity="start" + android:layout_weight="1"/> + + <TextView + android:id="@android:id/text2" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_gravity="end|top" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="?android:attr/textColorSecondary" + android:gravity="end" + android:layout_weight="1"/> + </LinearLayout> + + </RelativeLayout> +</LinearLayout>
\ No newline at end of file diff --git a/packages/SettingsLib/SliderPreference/res/values/attrs_expressive.xml b/packages/SettingsLib/SliderPreference/res/values/attrs_expressive.xml new file mode 100644 index 000000000000..59464099e228 --- /dev/null +++ b/packages/SettingsLib/SliderPreference/res/values/attrs_expressive.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 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. +--> + +<resources> + <!-- For SliderPreference --> + <declare-styleable name="SliderPreference"> + <attr name="textStart" format="reference" /> + <attr name="textEnd" format="reference" /> + <attr name="iconStart" format="reference" /> + <attr name="iconEnd" format="reference" /> + <attr name="iconStartContentDescription" format="reference" /> + <attr name="iconEndContentDescription" format="reference" /> + </declare-styleable> +</resources>
\ No newline at end of file diff --git a/packages/SettingsLib/SliderPreference/res/values/colors_expressive.xml b/packages/SettingsLib/SliderPreference/res/values/colors_expressive.xml new file mode 100644 index 000000000000..803b7a41cffe --- /dev/null +++ b/packages/SettingsLib/SliderPreference/res/values/colors_expressive.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 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. + --> + +<resources> + <color name="settingslib_expressive_color_slider_track_active">@color/settingslib_materialColorPrimary</color> + <color name="settingslib_expressive_color_slider_track_inactive">@color/settingslib_materialColorSurfaceContainerHighest</color> + <color name="settingslib_expressive_color_slider_thumb">@color/settingslib_materialColorPrimary</color> + <color name="settingslib_expressive_color_slider_halo">@android:color/transparent</color> +</resources>
\ No newline at end of file diff --git a/packages/SettingsLib/SliderPreference/res/values/dimens_expressive.xml b/packages/SettingsLib/SliderPreference/res/values/dimens_expressive.xml new file mode 100644 index 000000000000..faae8bcf7e59 --- /dev/null +++ b/packages/SettingsLib/SliderPreference/res/values/dimens_expressive.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 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. + --> +<resources> + <dimen name="settingslib_expressive_slider_track_height">@dimen/settingslib_expressive_space_small1</dimen> + <dimen name="settingslib_expressive_slider_track_inside_corner_size">@dimen/settingslib_expressive_space_extrasmall1</dimen> + <dimen name="settingslib_expressive_slider_track_stop_indicator_size">@dimen/settingslib_expressive_space_extrasmall2</dimen> + <dimen name="settingslib_expressive_slider_thumb_width">@dimen/settingslib_expressive_space_extrasmall2</dimen> + <dimen name="settingslib_expressive_slider_thumb_height">44dp</dimen> + <dimen name="settingslib_expressive_slider_thumb_elevation">@dimen/settingslib_expressive_space_none</dimen> + <dimen name="settingslib_expressive_slider_thumb_stroke_width">@dimen/settingslib_expressive_space_extrasmall2</dimen> + <dimen name="settingslib_expressive_slider_thumb_track_gap_size">@dimen/settingslib_expressive_space_extrasmall3</dimen> + <dimen name="settingslib_expressive_slider_tick_radius">@dimen/settingslib_expressive_radius_extrasmall1</dimen> + +</resources>
\ No newline at end of file diff --git a/packages/SettingsLib/SliderPreference/src/com/android/settingslib/widget/SliderPreference.java b/packages/SettingsLib/SliderPreference/src/com/android/settingslib/widget/SliderPreference.java new file mode 100644 index 000000000000..1815d040bb18 --- /dev/null +++ b/packages/SettingsLib/SliderPreference/src/com/android/settingslib/widget/SliderPreference.java @@ -0,0 +1,668 @@ +/* + * Copyright (C) 2024 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.ColorStateList; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + +import com.android.settingslib.widget.preference.slider.R; + +import com.google.android.material.slider.LabelFormatter; +import com.google.android.material.slider.Slider; + +/** + * A {@link Preference} that displays a {@link Slider}. + */ +public class SliderPreference extends Preference { + private static final String TAG = "SliderPreference"; + + private final int mTextStartId; + private final int mTextEndId; + private final int mIconStartId; + private final int mIconEndId; + private final int mIconStartContentDescriptionId; + private final int mIconEndContentDescriptionId; + private final ColorStateList mTrackActiveColor; + private final ColorStateList mTrackInactiveColor; + private final ColorStateList mThumbColor; + private final ColorStateList mHaloColor; + private final int mTrackHeight; + private final int mTrackInsideCornerSize; + private final int mTrackStopIndicatorSize; + private final int mThumbWidth; + private final int mThumbHeight; + private final int mThumbElevation; + private final int mThumbStrokeWidth; + private final int mThumbTrackGapSize; + private final int mTickRadius; + @Nullable private Slider mSlider; + private int mSliderValue; + private int mMin; + private int mMax; + private int mSliderIncrement; + private boolean mAdjustable; + private boolean mTrackingTouch; + + /** + * Listener reacting to the user pressing DPAD left/right keys if {@code + * adjustable} attribute is set to true; it transfers the key presses to the {@link Slider} + * to be handled accordingly. + */ + private final View.OnKeyListener mSliderKeyListener = new View.OnKeyListener() { + @Override + public boolean onKey(@NonNull View v, int keyCode, @NonNull KeyEvent event) { + if (event.getAction() != KeyEvent.ACTION_DOWN) { + return false; + } + + if (!mAdjustable && (keyCode == KeyEvent.KEYCODE_DPAD_LEFT + || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT)) { + // Right or left keys are pressed when in non-adjustable mode; Skip the keys. + return false; + } + + // We don't want to propagate the click keys down to the Slider since it will + // create the ripple effect for the thumb. + if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) { + return false; + } + + if (mSlider == null) { + Log.e(TAG, "Slider view is null and hence cannot be adjusted."); + return false; + } + return mSlider.onKeyDown(keyCode, event); + } + }; + /** + * Listener reacting to the {@link Slider} touch event by the user + */ + private final Slider.OnSliderTouchListener mTouchListener = new Slider.OnSliderTouchListener() { + @Override + public void onStopTrackingTouch(@NonNull Slider slider) { + mTrackingTouch = false; + if ((int) slider.getValue() != mSliderValue) { + syncValueInternal(slider); + } + } + + @Override + public void onStartTrackingTouch(@NonNull Slider slider) { + mTrackingTouch = true; + } + }; + private LabelFormatter mLabelFormater; + // Whether the SliderPreference should continuously save the Slider value while it is being + // dragged. + private boolean mUpdatesContinuously; + /** + * Listener reacting to the {@link Slider} changing value by the user + */ + private final Slider.OnChangeListener mChangeListener = new Slider.OnChangeListener() { + @Override + public void onValueChange(@NonNull Slider slider, float value, boolean fromUser) { + if (fromUser && (mUpdatesContinuously || !mTrackingTouch)) { + syncValueInternal(slider); + } + } + }; + // Whether to show the Slider value TextView next to the bar + private boolean mShowSliderValue; + + public SliderPreference(@NonNull Context context, + @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setLayoutResource(R.layout.settingslib_expressive_preference_slider); + + TypedArray a = context.obtainStyledAttributes( + attrs, androidx.preference.R.styleable.SeekBarPreference, defStyleAttr, + 0 /*defStyleRes*/); + + // The ordering of these two statements are important. If we want to set max first, we need + // to perform the same steps by changing min/max to max/min as following: + // mMax = a.getInt(...) and setMin(...). + mMin = a.getInt(androidx.preference.R.styleable.SeekBarPreference_min, 0); + setMax(a.getInt(androidx.preference.R.styleable.SeekBarPreference_android_max, 100)); + setSliderIncrement( + a.getInt(androidx.preference.R.styleable.SeekBarPreference_seekBarIncrement, 0)); + mAdjustable = a.getBoolean(androidx.preference.R.styleable.SeekBarPreference_adjustable, + true); + mShowSliderValue = a.getBoolean( + androidx.preference.R.styleable.SeekBarPreference_showSeekBarValue, false); + mUpdatesContinuously = a.getBoolean( + androidx.preference.R.styleable.SeekBarPreference_updatesContinuously, + false); + a.recycle(); + + a = context.obtainStyledAttributes(attrs, + R.styleable.SliderPreference); + mTextStartId = a.getResourceId( + R.styleable.SliderPreference_textStart, /* defValue= */ 0); + mTextEndId = a.getResourceId( + R.styleable.SliderPreference_textEnd, /* defValue= */ 0); + mIconStartId = a.getResourceId( + R.styleable.SliderPreference_iconStart, /* defValue= */ 0); + mIconEndId = a.getResourceId( + R.styleable.SliderPreference_iconEnd, /* defValue= */ 0); + + mIconStartContentDescriptionId = a.getResourceId( + R.styleable.SliderPreference_iconStartContentDescription, + /* defValue= */ 0); + + mIconEndContentDescriptionId = a.getResourceId( + R.styleable.SliderPreference_iconEndContentDescription, + /* defValue= */ 0); + a.recycle(); + + mTrackActiveColor = context.getColorStateList( + R.color.settingslib_expressive_color_slider_track_active); + mTrackInactiveColor = context.getColorStateList( + R.color.settingslib_expressive_color_slider_track_inactive); + mThumbColor = context.getColorStateList( + R.color.settingslib_expressive_color_slider_thumb); + mHaloColor = context.getColorStateList(R.color.settingslib_expressive_color_slider_halo); + + Resources res = context.getResources(); + mTrackHeight = res.getDimensionPixelSize( + R.dimen.settingslib_expressive_slider_track_height); + mTrackInsideCornerSize = res.getDimensionPixelSize( + R.dimen.settingslib_expressive_slider_track_inside_corner_size); + mTrackStopIndicatorSize = res.getDimensionPixelSize( + R.dimen.settingslib_expressive_slider_track_stop_indicator_size); + mThumbWidth = res.getDimensionPixelSize(R.dimen.settingslib_expressive_slider_thumb_width); + mThumbHeight = res.getDimensionPixelSize( + R.dimen.settingslib_expressive_slider_thumb_height); + mThumbElevation = res.getDimensionPixelSize( + R.dimen.settingslib_expressive_slider_thumb_elevation); + mThumbStrokeWidth = res.getDimensionPixelSize( + R.dimen.settingslib_expressive_slider_thumb_stroke_width); + mThumbTrackGapSize = res.getDimensionPixelSize( + R.dimen.settingslib_expressive_slider_thumb_track_gap_size); + mTickRadius = res.getDimensionPixelSize(R.dimen.settingslib_expressive_slider_tick_radius); + } + + /** + * Constructor that is called when inflating a preference from XML. This is called when a + * preference is being constructed from an XML file, supplying attributes that were specified + * in the XML file. This version uses a default style of 0, so the only attribute values + * applied are those in the Context's Theme and the given AttributeSet. + * + * @param context The Context this is associated with, through which it can access the + * current theme, resources, etc. + * @param attrs The attributes of the XML tag that is inflating the preference + */ + public SliderPreference(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0 /* defStyleAttr */); + } + + /** + * Constructor to create a slider preference. + * + * @param context The Context this is associated with, through which it can access the + * current theme, resources, etc. + */ + public SliderPreference(@NonNull Context context) { + this(context, null); + } + + private static void setIconViewAndFrameEnabled(View iconView, ViewGroup iconFrame, + boolean enabled) { + iconView.setEnabled(enabled); + iconFrame.setEnabled(enabled); + } + + @Override + public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + holder.itemView.setOnKeyListener(mSliderKeyListener); + mSlider = (Slider) holder.findViewById(R.id.slider); + + if (mSlider == null) { + Log.e(TAG, "Slider is null in onBindViewHolder."); + return; + } + + if (mShowSliderValue) { + mSlider.setLabelBehavior(LabelFormatter.LABEL_FLOATING); + } else { + mSlider.setLabelBehavior(LabelFormatter.LABEL_GONE); + } + if (mLabelFormater != null) { + mSlider.setLabelFormatter(mLabelFormater); + } + if (mSliderIncrement != 0) { + mSlider.setStepSize(mSliderIncrement); + } else { + mSliderIncrement = (int) (mSlider.getStepSize()); + } + mSlider.setValueFrom(mMin); + mSlider.setValueTo(mMax); + mSlider.setValue(mSliderValue); + mSlider.addOnSliderTouchListener(mTouchListener); + mSlider.addOnChangeListener(mChangeListener); + mSlider.setEnabled(isEnabled()); + + // Set up slider color + mSlider.setTrackActiveTintList(mTrackActiveColor); + mSlider.setTrackInactiveTintList(mTrackInactiveColor); + mSlider.setThumbTintList(mThumbColor); + mSlider.setHaloTintList(mHaloColor); + mSlider.setTickActiveTintList(mTrackInactiveColor); + mSlider.setTickInactiveTintList(mTrackActiveColor); + + // Set up slider size + if (SettingsThemeHelper.isExpressiveTheme(getContext())) { + mSlider.setTrackHeight(mTrackHeight); + // need to drop 1.12.0 to Android + mSlider.setTrackInsideCornerSize(mTrackInsideCornerSize); + mSlider.setTrackStopIndicatorSize(mTrackStopIndicatorSize); + mSlider.setThumbWidth(mThumbWidth); + mSlider.setThumbHeight(mThumbHeight); + mSlider.setThumbElevation(mThumbElevation); + mSlider.setThumbStrokeWidth(mThumbStrokeWidth); + mSlider.setThumbTrackGapSize(mThumbTrackGapSize); + mSlider.setTickActiveRadius(mTickRadius); + mSlider.setTickInactiveRadius(mTickRadius); + } + + TextView startText = (TextView) holder.findViewById(android.R.id.text1); + if (mTextStartId > 0 && startText != null) { + startText.setText(mTextStartId); + } + + TextView endText = (TextView) holder.findViewById(android.R.id.text2); + if (mTextEndId > 0 && endText != null) { + endText.setText(mTextEndId); + } + + View labelFrame = holder.findViewById(R.id.label_frame); + if (labelFrame != null) { + boolean isValidTextResIdExist = mTextStartId > 0 || mTextEndId > 0; + labelFrame.setVisibility(isValidTextResIdExist ? View.VISIBLE : View.GONE); + } + + ImageView iconStartView = (ImageView) holder.findViewById(R.id.icon_start); + updateIconStartIfNeeded(iconStartView); + + ImageView iconEndView = (ImageView) holder.findViewById(R.id.icon_end); + updateIconEndIfNeeded(iconEndView); + } + + /** + * Gets the lower bound set on the {@link Slider}. + * + * @return The lower bound set + */ + public int getMin() { + return mMin; + } + + /** + * Sets the lower bound on the {@link Slider}. + * + * @param min The lower bound to set + */ + public void setMin(int min) { + if (min > mMax) { + min = mMax; + } + if (min != mMin) { + mMin = min; + notifyChanged(); + } + } + + /** + * Gets the upper bound set on the {@link Slider}. + * + * @return The upper bound set + */ + public int getMax() { + return mMax; + } + + /** + * Sets the upper bound on the {@link Slider}. + * + * @param max The upper bound to set + */ + public final void setMax(int max) { + if (max < mMin) { + max = mMin; + } + if (max != mMax) { + mMax = max; + notifyChanged(); + } + } + + public final int getSliderIncrement() { + return mSliderIncrement; + } + + /** + * Sets the increment amount on the {@link Slider} for each arrow key press. + * + * @param sliderIncrement The amount to increment or decrement when the user presses an + * arrow key. + */ + public final void setSliderIncrement(int sliderIncrement) { + if (sliderIncrement != mSliderIncrement) { + mSliderIncrement = Math.min(mMax - mMin, Math.abs(sliderIncrement)); + notifyChanged(); + } + } + + /** + * Gets whether the {@link Slider} should respond to the left/right keys. + * + * @return Whether the {@link Slider} should respond to the left/right keys + */ + public boolean isAdjustable() { + return mAdjustable; + } + + /** + * Sets whether the {@link Slider} should respond to the left/right keys. + * + * @param adjustable Whether the {@link Slider} should respond to the left/right keys + */ + public void setAdjustable(boolean adjustable) { + mAdjustable = adjustable; + } + + /** + * Gets whether the {@link SliderPreference} should continuously save the {@link Slider} value + * while it is being dragged. Note that when the value is true, + * {@link Preference.OnPreferenceChangeListener} will be called continuously as well. + * + * @return Whether the {@link SliderPreference} should continuously save the {@link Slider} + * value while it is being dragged + * @see #setUpdatesContinuously(boolean) + */ + public boolean getUpdatesContinuously() { + return mUpdatesContinuously; + } + + /** + * Sets whether the {@link SliderPreference} should continuously save the {@link Slider} value + * while it is being dragged. + * + * @param updatesContinuously Whether the {@link SliderPreference} should continuously save + * the {@link Slider} value while it is being dragged + * @see #getUpdatesContinuously() + */ + public void setUpdatesContinuously(boolean updatesContinuously) { + mUpdatesContinuously = updatesContinuously; + } + + /** + * Gets whether the current {@link Slider} value is displayed to the user. + * + * @return Whether the current {@link Slider} value is displayed to the user + * @see #setShowSliderValue(boolean) + */ + public boolean getShowSliderValue() { + return mShowSliderValue; + } + + /** + * Sets whether the current {@link Slider} value is displayed to the user. + * + * @param showSliderValue Whether the current {@link Slider} value is displayed to the user + * @see #getShowSliderValue() + */ + public void setShowSliderValue(boolean showSliderValue) { + mShowSliderValue = showSliderValue; + notifyChanged(); + } + + public void setLabelFormater(@Nullable LabelFormatter formater) { + mLabelFormater = formater; + } + + /** + * Gets the current progress of the {@link Slider}. + * + * @return The current progress of the {@link Slider} + */ + public int getValue() { + return mSliderValue; + } + + /** + * Sets the current progress of the {@link Slider}. + * + * @param sliderValue The current progress of the {@link Slider} + */ + public void setValue(int sliderValue) { + setValueInternal(sliderValue, true); + } + + @Override + protected void onSetInitialValue(@Nullable Object defaultValue) { + if (defaultValue == null) { + defaultValue = 0; + } + setValue(getPersistedInt((Integer) defaultValue)); + } + + @Override + protected @Nullable Object onGetDefaultValue(@NonNull TypedArray a, int index) { + return a.getInt(index, 0); + } + + /** + * Persist the {@link Slider}'s Slider value if callChangeListener returns true, otherwise + * set the {@link Slider}'s value to the stored value. + */ + void syncValueInternal(@NonNull Slider slider) { + int sliderValue = mMin + (int) slider.getValue(); + if (sliderValue != mSliderValue) { + if (callChangeListener(sliderValue)) { + setValueInternal(sliderValue, false); + // TODO: mHapticFeedbackMode + } else { + slider.setValue(mSliderValue); + } + } + } + + private void setValueInternal(int sliderValue, boolean notifyChanged) { + if (sliderValue < mMin) { + sliderValue = mMin; + } + if (sliderValue > mMax) { + sliderValue = mMax; + } + + if (sliderValue != mSliderValue) { + mSliderValue = sliderValue; + persistInt(sliderValue); + if (notifyChanged) { + notifyChanged(); + } + } + } + + @Nullable + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + if (isPersistent()) { + // No need to save instance state since it's persistent + return superState; + } + + // Save the instance state + SavedState myState = new SavedState(superState); + myState.mSliderValue = mSliderValue; + myState.mMin = mMin; + myState.mMax = mMax; + return myState; + } + + @Override + protected void onRestoreInstanceState(@Nullable Parcelable state) { + if (state == null || !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()); + mSliderValue = myState.mSliderValue; + mMin = myState.mMin; + mMax = myState.mMax; + notifyChanged(); + } + + private void updateIconStartIfNeeded(ImageView icon) { + if (icon == null) { + return; + } + ViewGroup iconFrame = (ViewGroup) icon.getParent(); + if (iconFrame == null) { + return; + } + + if (mIconStartId == 0 || mSliderIncrement == 0) { + iconFrame.setVisibility(View.GONE); + return; + } + + if (icon.getDrawable() == null) { + icon.setImageResource(mIconStartId); + } + + if (mIconStartContentDescriptionId != 0) { + String contentDescription = + iconFrame.getContext().getString(mIconStartContentDescriptionId); + iconFrame.setContentDescription(contentDescription); + } + + iconFrame.setOnClickListener((view) -> { + if (mSliderValue > 0) { + setValue(mSliderValue - mSliderIncrement); + } + }); + + iconFrame.setVisibility(View.VISIBLE); + setIconViewAndFrameEnabled(icon, iconFrame, mSliderValue > mMin); + } + + private void updateIconEndIfNeeded(ImageView icon) { + if (icon == null) { + return; + } + ViewGroup iconFrame = (ViewGroup) icon.getParent(); + if (iconFrame == null) { + return; + } + + if (mIconEndId == 0 || mSliderIncrement == 0) { + iconFrame.setVisibility(View.GONE); + return; + } + + if (icon.getDrawable() == null) { + icon.setImageResource(mIconEndId); + } + + if (mIconEndContentDescriptionId != 0) { + String contentDescription = + iconFrame.getContext().getString(mIconEndContentDescriptionId); + iconFrame.setContentDescription(contentDescription); + } + + iconFrame.setOnClickListener((view) -> { + if (mSliderValue < mMax) { + setValue(mSliderValue + mSliderIncrement); + } + }); + + iconFrame.setVisibility(View.VISIBLE); + setIconViewAndFrameEnabled(icon, iconFrame, mSliderValue < mMax); + } + + /** + * SavedState, a subclass of {@link BaseSavedState}, will store the state of this preference. + * + * <p>It is important to always call through to super methods. + */ + private static class SavedState extends BaseSavedState { + public static final Parcelable.Creator<SavedState> CREATOR = + new Parcelable.Creator<>() { + @Override + @NonNull + public SavedState createFromParcel(@NonNull Parcel in) { + return new SavedState(in); + } + + @Override + @NonNull + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + + int mSliderValue; + int mMin; + int mMax; + + SavedState(Parcel source) { + super(source); + + // Restore the click counter + mSliderValue = source.readInt(); + mMin = source.readInt(); + mMax = source.readInt(); + } + + SavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + super.writeToParcel(dest, flags); + + // Save the click counter + dest.writeInt(mSliderValue); + dest.writeInt(mMin); + dest.writeInt(mMax); + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedInterface.kt b/packages/SettingsLib/src/com/android/settingslib/RestrictedInterface.kt new file mode 100644 index 000000000000..14f9a19ee753 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedInterface.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 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 com.android.settingslib.RestrictedLockUtils.EnforcedAdmin + +interface RestrictedInterface { + fun useAdminDisabledSummary(useSummary: Boolean) + + fun checkRestrictionAndSetDisabled(userRestriction: String) + + fun checkRestrictionAndSetDisabled(userRestriction: String, userId: Int) + + /** + * Checks if the given setting is subject to Enhanced Confirmation Mode restrictions for this + * package. Marks the preference as disabled if so. + * + * @param settingIdentifier The key identifying the setting + * @param packageName the package to check the settingIdentifier for + */ + fun checkEcmRestrictionAndSetDisabled( + settingIdentifier: String, + packageName: String + ) + + val isDisabledByAdmin: Boolean + + fun setDisabledByAdmin(admin: EnforcedAdmin?) + + val isDisabledByEcm: Boolean + + val uid: Int + + val packageName: String? +} diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedSliderPreference.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedSliderPreference.java new file mode 100644 index 000000000000..1dc5281c266c --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedSliderPreference.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2024 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.annotation.SuppressLint; +import android.content.Context; +import android.os.Process; +import android.os.UserHandle; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.res.TypedArrayUtils; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceViewHolder; + +import com.android.settingslib.widget.SliderPreference; + +/** + * Slide Preference that supports being disabled by a user restriction + * set by a device admin. + */ +public class RestrictedSliderPreference extends SliderPreference implements RestrictedInterface { + RestrictedPreferenceHelper mHelper; + + public RestrictedSliderPreference(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr, @Nullable String packageName, int uid) { + super(context, attrs, defStyleAttr); + mHelper = new RestrictedPreferenceHelper(context, this, attrs, packageName, uid); + } + + public RestrictedSliderPreference(@NonNull Context context, + @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, null, Process.INVALID_UID); + } + + @SuppressLint("RestrictedApi") + public RestrictedSliderPreference(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, TypedArrayUtils.getAttr(context, R.attr.preferenceStyle, + android.R.attr.preferenceStyle)); + } + + public RestrictedSliderPreference(@NonNull Context context) { + this(context, null); + } + + @SuppressLint("RestrictedApi") + public RestrictedSliderPreference(@NonNull Context context, @Nullable String packageName, + int uid) { + this(context, null, TypedArrayUtils.getAttr(context, R.attr.preferenceStyle, + android.R.attr.preferenceStyle), packageName, uid); + } + + @Override + public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + mHelper.onBindViewHolder(holder); + } + + @SuppressLint("RestrictedApi") + @Override + public void performClick() { + if (!mHelper.performClick()) { + super.performClick(); + } + } + + @Override + public void setEnabled(boolean enabled) { + if (enabled && isDisabledByAdmin()) { + mHelper.setDisabledByAdmin(null); + return; + } + + if (enabled && isDisabledByEcm()) { + mHelper.setDisabledByEcm(null); + return; + } + + super.setEnabled(enabled); + } + + @Override + public void useAdminDisabledSummary(boolean useSummary) { + mHelper.useAdminDisabledSummary(useSummary); + } + + @Override + public void checkRestrictionAndSetDisabled(@NonNull String userRestriction) { + mHelper.checkRestrictionAndSetDisabled(userRestriction, UserHandle.myUserId()); + } + + @Override + public void checkRestrictionAndSetDisabled(@NonNull String userRestriction, int userId) { + mHelper.checkRestrictionAndSetDisabled(userRestriction, userId); + } + + @Override + public void checkEcmRestrictionAndSetDisabled(@NonNull String settingIdentifier, + @NonNull String packageName) { + mHelper.checkEcmRestrictionAndSetDisabled(settingIdentifier, packageName); + } + + @Override + public void setDisabledByAdmin(@Nullable RestrictedLockUtils.EnforcedAdmin admin) { + if (mHelper.setDisabledByAdmin(admin)) { + notifyChanged(); + } + } + + @Override + public boolean isDisabledByAdmin() { + return mHelper.isDisabledByAdmin(); + } + + @Override + public boolean isDisabledByEcm() { + return mHelper.isDisabledByEcm(); + } + + @Override + public int getUid() { + return mHelper != null ? mHelper.uid : Process.INVALID_UID; + } + + @Override + @Nullable + public String getPackageName() { + return mHelper != null ? mHelper.packageName : null; + } + + @Override + protected void onAttachedToHierarchy(@NonNull PreferenceManager preferenceManager) { + mHelper.onAttachedToHierarchy(); + super.onAttachedToHierarchy(preferenceManager); + } +} |