From 1516c28737ef99f526cfa846837305fbd0d1ea24 Mon Sep 17 00:00:00 2001 From: Yuri Lin Date: Thu, 15 Jul 2021 12:06:07 -0400 Subject: [PATCH] Rename RadioButtonPreference to SelectorWithWidgetPreference, and add a checkbox styling option. This rename makes it slightly less confusing to have both radio and checkbox display options. Functionally, this preference was already equivalent to a checkbox (with checking/unchecking functionality managed by the caller); the reason to use this class for settings checkboxes rather than the base CheckBoxPreference is for the added widget functionality. This change leaves RadioButtonPreference as-is to make it easier to bring google3 uses in without having to be in lockstep, but RadioButtonPreference will be deleted in a future CL. Test: SelectorWithWidgetPreferenceTest, RunSettingsLibRoboTests Bug: 190180868 Change-Id: I0811997b4625f1292b1842ea353a90221159bb50 --- packages/SettingsLib/Android.bp | 1 + .../RadioButtonPreference/Android.bp | 1 + .../widget/RadioButtonPreference.java | 6 + .../SelectorWithWidgetPreference/Android.bp | 27 +++ .../AndroidManifest.xml | 23 ++ .../res/drawable/ic_settings_accent.xml | 29 +++ .../preference_selector_with_widget.xml | 123 +++++++++++ .../res/layout/preference_widget_checkbox.xml | 25 +++ .../layout/preference_widget_radiobutton.xml | 25 +++ .../res/values/strings.xml | 23 ++ .../widget/SelectorWithWidgetPreference.java | 203 ++++++++++++++++++ .../SelectorWithWidgetPreferenceTest.java | 166 ++++++++++++++ 12 files changed, 652 insertions(+) create mode 100644 packages/SettingsLib/SelectorWithWidgetPreference/Android.bp create mode 100644 packages/SettingsLib/SelectorWithWidgetPreference/AndroidManifest.xml create mode 100644 packages/SettingsLib/SelectorWithWidgetPreference/res/drawable/ic_settings_accent.xml create mode 100644 packages/SettingsLib/SelectorWithWidgetPreference/res/layout/preference_selector_with_widget.xml create mode 100644 packages/SettingsLib/SelectorWithWidgetPreference/res/layout/preference_widget_checkbox.xml create mode 100644 packages/SettingsLib/SelectorWithWidgetPreference/res/layout/preference_widget_radiobutton.xml create mode 100644 packages/SettingsLib/SelectorWithWidgetPreference/res/values/strings.xml create mode 100644 packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java create mode 100644 packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/SelectorWithWidgetPreferenceTest.java diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp index 2b8f049a36fc7..e8ed88fc02222 100644 --- a/packages/SettingsLib/Android.bp +++ b/packages/SettingsLib/Android.bp @@ -37,6 +37,7 @@ android_library { "SettingsLibProgressBar", "SettingsLibAdaptiveIcon", "SettingsLibRadioButtonPreference", + "SettingsLibSelectorWithWidgetPreference", "SettingsLibDisplayDensityUtils", "SettingsLibUtils", "SettingsLibEmergencyNumber", diff --git a/packages/SettingsLib/RadioButtonPreference/Android.bp b/packages/SettingsLib/RadioButtonPreference/Android.bp index 28ff71f228402..1387daa959be9 100644 --- a/packages/SettingsLib/RadioButtonPreference/Android.bp +++ b/packages/SettingsLib/RadioButtonPreference/Android.bp @@ -15,6 +15,7 @@ android_library { static_libs: [ "androidx.preference_preference", + "SettingsLibSelectorWithWidgetPreference", "SettingsLibSettingsTheme", ], diff --git a/packages/SettingsLib/RadioButtonPreference/src/com/android/settingslib/widget/RadioButtonPreference.java b/packages/SettingsLib/RadioButtonPreference/src/com/android/settingslib/widget/RadioButtonPreference.java index f50127f6e21ff..02d3c06dcea25 100644 --- a/packages/SettingsLib/RadioButtonPreference/src/com/android/settingslib/widget/RadioButtonPreference.java +++ b/packages/SettingsLib/RadioButtonPreference/src/com/android/settingslib/widget/RadioButtonPreference.java @@ -26,6 +26,10 @@ import androidx.preference.CheckBoxPreference; import androidx.preference.PreferenceViewHolder; /** + * DEPRECATED. Please use SelectorWithWidgetPreference instead. + * + * This file has been moved there and will be removed once all callers are updated. + * * Check box preference with check box replaced by radio button. * * Functionally speaking, it's actually a CheckBoxPreference. We only modified @@ -37,6 +41,8 @@ import androidx.preference.PreferenceViewHolder; * * RadioButtonPreference can assign a extraWidgetListener to show a gear icon * on the right side that can open another page. + * + * @Deprecated */ public class RadioButtonPreference extends CheckBoxPreference { diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp b/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp new file mode 100644 index 0000000000000..bcc64d3cd2346 --- /dev/null +++ b/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp @@ -0,0 +1,27 @@ +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: "SettingsLibSelectorWithWidgetPreference", + + srcs: ["src/**/*.java"], + resource_dirs: ["res"], + + static_libs: [ + "androidx.preference_preference", + "SettingsLibSettingsTheme", + ], + + sdk_version: "system_current", + min_sdk_version: "21", + apex_available: [ + "//apex_available:platform", + "com.android.permission", + ], +} diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/AndroidManifest.xml b/packages/SettingsLib/SelectorWithWidgetPreference/AndroidManifest.xml new file mode 100644 index 0000000000000..51fc7ed646602 --- /dev/null +++ b/packages/SettingsLib/SelectorWithWidgetPreference/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/res/drawable/ic_settings_accent.xml b/packages/SettingsLib/SelectorWithWidgetPreference/res/drawable/ic_settings_accent.xml new file mode 100644 index 0000000000000..6521bc9e0fb9d --- /dev/null +++ b/packages/SettingsLib/SelectorWithWidgetPreference/res/drawable/ic_settings_accent.xml @@ -0,0 +1,29 @@ + + + + + + \ No newline at end of file diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/res/layout/preference_selector_with_widget.xml b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout/preference_selector_with_widget.xml new file mode 100644 index 0000000000000..8bb56ff0a07df --- /dev/null +++ b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout/preference_selector_with_widget.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/res/layout/preference_widget_checkbox.xml b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout/preference_widget_checkbox.xml new file mode 100644 index 0000000000000..6dd16709b7b59 --- /dev/null +++ b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout/preference_widget_checkbox.xml @@ -0,0 +1,25 @@ + + + + \ No newline at end of file diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/res/layout/preference_widget_radiobutton.xml b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout/preference_widget_radiobutton.xml new file mode 100644 index 0000000000000..cf6371d04f789 --- /dev/null +++ b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout/preference_widget_radiobutton.xml @@ -0,0 +1,25 @@ + + + + diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/res/values/strings.xml b/packages/SettingsLib/SelectorWithWidgetPreference/res/values/strings.xml new file mode 100644 index 0000000000000..ff3f90cfffd3e --- /dev/null +++ b/packages/SettingsLib/SelectorWithWidgetPreference/res/values/strings.xml @@ -0,0 +1,23 @@ + + + + + + + Settings + + \ No newline at end of file diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java b/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java new file mode 100644 index 0000000000000..1ecc42235c3bb --- /dev/null +++ b/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java @@ -0,0 +1,203 @@ +/* + * 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.text.TextUtils; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; + +import androidx.preference.CheckBoxPreference; +import androidx.preference.PreferenceViewHolder; + +/** + * Selector preference (checkbox or radio button) with an optional additional widget. + * + * Functionally speaking, it's a CheckBoxPreference. When styled like a radio button, + * it only "looks like" a RadioButtonPreference. + * + * In other words, there's no "RadioButtonPreferenceGroup" in this + * implementation. When you check one preference, if you want to + * uncheck all the other preferences, you should do that by code yourself. + * + * SelectorWithWidgetPreference can assign a extraWidgetListener to show a gear icon + * on the right side that can open another page. + */ +public class SelectorWithWidgetPreference extends CheckBoxPreference { + + /** + * Interface definition for a callback to be invoked when the preference is clicked. + */ + public interface OnClickListener { + /** + * Called when a preference has been clicked. + * + * @param emiter The clicked preference + */ + void onRadioButtonClicked(SelectorWithWidgetPreference emiter); + } + + private OnClickListener mListener = null; + private View mAppendix; + private int mAppendixVisibility = -1; + + private View mExtraWidgetContainer; + private ImageView mExtraWidget; + private boolean mIsCheckBox = false; // whether to display this button as a checkbox + + private View.OnClickListener mExtraWidgetOnClickListener; + + /** + * Perform inflation from XML and apply a class-specific base style. + * + * @param context The {@link Context} this is associated with, through which it can + * access the current theme, resources, {@link SharedPreferences}, etc. + * @param attrs The attributes of the XML tag that is inflating the preference + * @param defStyle An attribute in the current theme that contains a reference to a style + * resource that supplies default values for the view. Can be 0 to not + * look for defaults. + */ + public SelectorWithWidgetPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + /** + * Perform inflation from XML and apply a class-specific base style. + * + * @param context The {@link Context} this is associated with, through which it can + * access the current theme, resources, {@link SharedPreferences}, etc. + * @param attrs The attributes of the XML tag that is inflating the preference + */ + public SelectorWithWidgetPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + /** + * Constructor to create a preference, which will display with a checkbox style. + * + * @param context The {@link Context} this is associated with. + * @param isCheckbox Whether this preference should display as a checkbox. + */ + public SelectorWithWidgetPreference(Context context, boolean isCheckbox) { + super(context, null); + mIsCheckBox = isCheckbox; + init(); + } + + /** + * Constructor to create a preference. + * + * @param context The Context this is associated with. + */ + public SelectorWithWidgetPreference(Context context) { + this(context, null); + } + + /** + * Sets the callback to be invoked when this preference is clicked by the user. + * + * @param listener The callback to be invoked + */ + public void setOnClickListener(OnClickListener listener) { + mListener = listener; + } + + /** + * Processes a click on the preference. + */ + @Override + public void onClick() { + if (mListener != null) { + mListener.onRadioButtonClicked(this); + } + } + + /** + * Binds the created View to the data for this preference. + * + *

This is a good place to grab references to custom Views in the layout and set + * properties on them. + * + *

Make sure to call through to the superclass's implementation. + * + * @param holder The ViewHolder that provides references to the views to fill in. These views + * will be recycled, so you should not hold a reference to them after this method + * returns. + */ + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + + View summaryContainer = holder.findViewById(R.id.summary_container); + if (summaryContainer != null) { + summaryContainer.setVisibility( + TextUtils.isEmpty(getSummary()) ? View.GONE : View.VISIBLE); + mAppendix = holder.findViewById(R.id.appendix); + if (mAppendix != null && mAppendixVisibility != -1) { + mAppendix.setVisibility(mAppendixVisibility); + } + } + + mExtraWidget = (ImageView) holder.findViewById(R.id.selector_extra_widget); + mExtraWidgetContainer = holder.findViewById(R.id.selector_extra_widget_container); + + setExtraWidgetOnClickListener(mExtraWidgetOnClickListener); + } + + /** + * Set the visibility state of appendix view. + * + * @param visibility One of {@link View#VISIBLE}, {@link View#INVISIBLE}, or {@link View#GONE}. + */ + public void setAppendixVisibility(int visibility) { + if (mAppendix != null) { + mAppendix.setVisibility(visibility); + } + mAppendixVisibility = visibility; + } + + /** + * Sets the callback to be invoked when extra widget is clicked by the user. + * + * @param listener The callback to be invoked + */ + public void setExtraWidgetOnClickListener(View.OnClickListener listener) { + mExtraWidgetOnClickListener = listener; + + if (mExtraWidget == null || mExtraWidgetContainer == null) { + return; + } + + mExtraWidget.setOnClickListener(mExtraWidgetOnClickListener); + + mExtraWidgetContainer.setVisibility((mExtraWidgetOnClickListener != null) + ? View.VISIBLE : View.GONE); + } + + private void init() { + if (mIsCheckBox) { + setWidgetLayoutResource(R.layout.preference_widget_checkbox); + } else { + setWidgetLayoutResource(R.layout.preference_widget_radiobutton); + } + setLayoutResource(R.layout.preference_selector_with_widget); + setIconSpaceReserved(false); + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/SelectorWithWidgetPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/SelectorWithWidgetPreferenceTest.java new file mode 100644 index 0000000000000..34efe82f25271 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/SelectorWithWidgetPreferenceTest.java @@ -0,0 +1,166 @@ +/* + * 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 com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.app.Application; +import android.view.LayoutInflater; +import android.view.View; + +import androidx.preference.PreferenceViewHolder; + +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 SelectorWithWidgetPreferenceTest { + + private Application mContext; + private SelectorWithWidgetPreference mPreference; + + private View mExtraWidgetContainer; + private View mExtraWidget; + + private boolean mIsClickListenerCalled; + private View.OnClickListener mClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + mIsClickListenerCalled = true; + } + }; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + mPreference = new SelectorWithWidgetPreference(mContext); + + View view = LayoutInflater.from(mContext) + .inflate(R.layout.preference_selector_with_widget, null /* root */); + PreferenceViewHolder preferenceViewHolder = + PreferenceViewHolder.createInstanceForTests(view); + mPreference.onBindViewHolder(preferenceViewHolder); + + mExtraWidgetContainer = view.findViewById(R.id.selector_extra_widget_container); + mExtraWidget = view.findViewById(R.id.selector_extra_widget); + } + + @Test + public void shouldHaveRadioPreferenceLayout() { + assertThat(mPreference.getLayoutResource()).isEqualTo( + R.layout.preference_selector_with_widget); + } + + @Test + public void shouldHaveRadioButtonWidgetLayoutByDefault() { + assertThat(mPreference.getWidgetLayoutResource()) + .isEqualTo(R.layout.preference_widget_radiobutton); + } + + @Test + public void shouldHaveCheckBoxWidgetLayoutIfSet() { + mPreference = new SelectorWithWidgetPreference(mContext, true); + assertThat(mPreference.getWidgetLayoutResource()) + .isEqualTo(R.layout.preference_widget_checkbox); + } + + @Test + public void iconSpaceReservedShouldBeFalse() { + assertThat(mPreference.isIconSpaceReserved()).isFalse(); + } + + @Test + public void onBindViewHolder_withSummary_containerShouldBeVisible() { + mPreference.setSummary("some summary"); + View summaryContainer = new View(mContext); + View view = mock(View.class); + when(view.findViewById(R.id.summary_container)).thenReturn(summaryContainer); + PreferenceViewHolder preferenceViewHolder = + PreferenceViewHolder.createInstanceForTests(view); + + mPreference.onBindViewHolder(preferenceViewHolder); + + assertEquals(View.VISIBLE, summaryContainer.getVisibility()); + } + + @Test + public void onBindViewHolder_emptySummary_containerShouldBeGone() { + mPreference.setSummary(""); + View summaryContainer = new View(mContext); + View view = mock(View.class); + when(view.findViewById(R.id.summary_container)).thenReturn(summaryContainer); + PreferenceViewHolder preferenceViewHolder = + PreferenceViewHolder.createInstanceForTests(view); + + mPreference.onBindViewHolder(preferenceViewHolder); + + assertEquals(View.GONE, summaryContainer.getVisibility()); + } + + @Test + public void nullSummary_containerShouldBeGone() { + mPreference.setSummary(null); + View summaryContainer = new View(mContext); + View view = mock(View.class); + when(view.findViewById(R.id.summary_container)).thenReturn(summaryContainer); + PreferenceViewHolder preferenceViewHolder = + PreferenceViewHolder.createInstanceForTests(view); + mPreference.onBindViewHolder(preferenceViewHolder); + assertEquals(View.GONE, summaryContainer.getVisibility()); + } + + @Test + public void setAppendixVisibility_setGone_shouldBeGone() { + mPreference.setAppendixVisibility(View.GONE); + + View view = LayoutInflater.from(mContext) + .inflate(R.layout.preference_selector_with_widget, null /* root */); + PreferenceViewHolder holder = PreferenceViewHolder.createInstanceForTests(view); + mPreference.onBindViewHolder(holder); + assertThat(holder.findViewById(R.id.appendix).getVisibility()).isEqualTo(View.GONE); + } + + @Test + public void setExtraWidgetListener_setNull_extraWidgetShouldInvisible() { + mPreference.setExtraWidgetOnClickListener(null); + + assertEquals(View.GONE, mExtraWidgetContainer.getVisibility()); + } + + @Test + public void setExtraWidgetListener_extraWidgetShouldVisible() { + mPreference.setExtraWidgetOnClickListener(mClickListener); + + assertEquals(View.VISIBLE, mExtraWidgetContainer.getVisibility()); + } + + @Test + public void onClickListener_setExtraWidgetOnClickListener_ShouldCalled() { + mPreference.setExtraWidgetOnClickListener(mClickListener); + + assertThat(mIsClickListenerCalled).isFalse(); + mExtraWidget.callOnClick(); + assertThat(mIsClickListenerCalled).isTrue(); + } +}