From c2d2de085d2282da6f3ab9d062c426139a397489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=ADas=20Hern=C3=A1ndez?= Date: Tue, 25 Jun 2024 20:29:14 +0200 Subject: [PATCH] Changes to icon picker for reusability in "add mode" flow * Don't finish the fragment from the controller (ugh!) instead just report the selected icon via a listener. * Highlight the selected icon in the list. * Cache the icon drawables (since we're using selectors for the colors, we don't need to swap them, one per icon resource id is enough). * Improved the tests a bit too. Bug: 333901673 Bug: 326442408 Test: ates Flag: android.app.modes_ui Change-Id: Ib2ec7a7e3ed99b13f9264aa6f7c209ee3f6967a0 --- .../modes_icon_picker_item_background.xml | 25 +++++++ res/color/modes_icon_picker_item_icon.xml | 25 +++++++ .../settings/notification/modes/IconUtil.java | 12 +-- .../notification/modes/ZenModeFragment.java | 11 ++- .../modes/ZenModeFragmentBase.java | 21 +++--- .../modes/ZenModeIconPickerFragment.java | 13 +++- ...odeIconPickerListPreferenceController.java | 75 ++++++++++++++----- .../notification/modes/TestModeBuilder.java | 14 +++- ...conPickerListPreferenceControllerTest.java | 61 +++++++++------ 9 files changed, 190 insertions(+), 67 deletions(-) create mode 100644 res/color/modes_icon_picker_item_background.xml create mode 100644 res/color/modes_icon_picker_item_icon.xml diff --git a/res/color/modes_icon_picker_item_background.xml b/res/color/modes_icon_picker_item_background.xml new file mode 100644 index 00000000000..f9280c60d6c --- /dev/null +++ b/res/color/modes_icon_picker_item_background.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/res/color/modes_icon_picker_item_icon.xml b/res/color/modes_icon_picker_item_icon.xml new file mode 100644 index 00000000000..8a517d5f474 --- /dev/null +++ b/res/color/modes_icon_picker_item_icon.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/src/com/android/settings/notification/modes/IconUtil.java b/src/com/android/settings/notification/modes/IconUtil.java index 1e653bf03fe..56967c89d00 100644 --- a/src/com/android/settings/notification/modes/IconUtil.java +++ b/src/com/android/settings/notification/modes/IconUtil.java @@ -50,14 +50,16 @@ class IconUtil { /** * Returns a variant of the supplied {@code icon} to be used in the icon picker. The inner icon - * is 36x36dp and it's contained into a circle of diameter 54dp. + * is 36x36dp and it's contained into a circle of diameter 54dp. It's also set up so that + * selection and pressed states are represented in the color. */ static Drawable makeIconCircle(@NonNull Context context, @NonNull Drawable icon) { ShapeDrawable background = new ShapeDrawable(new OvalShape()); - background.getPaint().setColor(Utils.getColorAttrDefaultColor(context, - com.android.internal.R.attr.materialColorSecondaryContainer)); - icon.setTint(Utils.getColorAttrDefaultColor(context, - com.android.internal.R.attr.materialColorOnSecondaryContainer)); + background.setTintList( + context.getColorStateList(R.color.modes_icon_picker_item_background)); + icon = icon.mutate(); + icon.setTintList( + context.getColorStateList(R.color.modes_icon_picker_item_icon)); LayerDrawable layerDrawable = new LayerDrawable(new Drawable[] { background, icon }); diff --git a/src/com/android/settings/notification/modes/ZenModeFragment.java b/src/com/android/settings/notification/modes/ZenModeFragment.java index 5897c4dd680..63ed8395006 100644 --- a/src/com/android/settings/notification/modes/ZenModeFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeFragment.java @@ -18,13 +18,14 @@ package com.android.settings.notification.modes; import android.app.AlertDialog; import android.app.Application; -import android.app.AutomaticZenRule; import android.app.settings.SettingsEnums; import android.content.Context; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import androidx.annotation.NonNull; + import com.android.settings.R; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.core.AbstractPreferenceController; @@ -72,11 +73,9 @@ public class ZenModeFragment extends ZenModeFragmentBase { // Set title for the entire screen ZenMode mode = getMode(); - AutomaticZenRule azr = getAZR(); - if (mode == null || azr == null) { - return; + if (mode != null) { + requireActivity().setTitle(mode.getName()); } - getActivity().setTitle(azr.getName()); } @Override @@ -92,7 +91,7 @@ public class ZenModeFragment extends ZenModeFragmentBase { } @Override - protected boolean onOptionsItemSelected(MenuItem item, ZenMode zenMode) { + protected boolean onOptionsItemSelected(MenuItem item, @NonNull ZenMode zenMode) { switch (item.getItemId()) { case DELETE_MODE: new AlertDialog.Builder(mContext) diff --git a/src/com/android/settings/notification/modes/ZenModeFragmentBase.java b/src/com/android/settings/notification/modes/ZenModeFragmentBase.java index d08f7ea0229..b0ad7956a84 100644 --- a/src/com/android/settings/notification/modes/ZenModeFragmentBase.java +++ b/src/com/android/settings/notification/modes/ZenModeFragmentBase.java @@ -18,7 +18,6 @@ package com.android.settings.notification.modes; import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID; -import android.app.AutomaticZenRule; import android.content.Context; import android.os.Bundle; import android.util.Log; @@ -34,7 +33,10 @@ import com.android.settings.R; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.notification.modes.ZenMode; +import com.google.common.base.Preconditions; + import java.util.List; +import java.util.function.Consumer; /** * Base class for Settings pages used to configure individual modes. @@ -175,14 +177,15 @@ abstract class ZenModeFragmentBase extends ZenModesFragmentBase { return mZenMode; } - /** - * Get AutomaticZenRule associated with current mode data, or null if it doesn't exist. - */ - @Nullable - public AutomaticZenRule getAZR() { - if (mZenMode == null) { - return null; + protected final boolean saveMode(Consumer updater) { + Preconditions.checkState(mBackend != null); + ZenMode mode = mZenMode; + if (mode == null) { + Log.wtf(TAG, "Cannot save mode, it hasn't been loaded (" + getClass() + ")"); + return false; } - return mZenMode.getRule(); + updater.accept(mode); + mBackend.updateMode(mode); + return true; } } diff --git a/src/com/android/settings/notification/modes/ZenModeIconPickerFragment.java b/src/com/android/settings/notification/modes/ZenModeIconPickerFragment.java index 760b1835f70..43d9dba1b54 100644 --- a/src/com/android/settings/notification/modes/ZenModeIconPickerFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeIconPickerFragment.java @@ -43,7 +43,16 @@ public class ZenModeIconPickerFragment extends ZenModeFragmentBase { return ImmutableList.of( new ZenModeIconPickerIconPreferenceController(context, "current_icon", this, mBackend), - new ZenModeIconPickerListPreferenceController(context, "icon_list", this, - new IconOptionsProviderImpl(mContext), mBackend)); + new ZenModeIconPickerListPreferenceController(context, "icon_list", + mIconPickerListener, new IconOptionsProviderImpl(mContext), mBackend)); } + + private final ZenModeIconPickerListPreferenceController.IconPickerListener mIconPickerListener = + new ZenModeIconPickerListPreferenceController.IconPickerListener() { + @Override + public void onIconSelected(int iconResId) { + saveMode(mode -> mode.getRule().setIconResId(iconResId)); + finish(); + } + }; } diff --git a/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceController.java index 85ceafe0870..e663354231e 100644 --- a/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceController.java @@ -17,6 +17,7 @@ package com.android.settings.notification.modes; import android.content.Context; +import android.graphics.drawable.Drawable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -25,31 +26,35 @@ import android.widget.ImageView; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.SimpleItemAnimator; import com.android.settings.R; -import com.android.settings.dashboard.DashboardFragment; import com.android.settingslib.notification.modes.ZenMode; import com.android.settingslib.notification.modes.ZenModesBackend; import com.android.settingslib.widget.LayoutPreference; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +import java.util.HashMap; +import java.util.Map; class ZenModeIconPickerListPreferenceController extends AbstractZenModePreferenceController { - private final DashboardFragment mFragment; private final IconOptionsProvider mIconOptionsProvider; + private final IconPickerListener mListener; @Nullable private IconAdapter mAdapter; + private @DrawableRes int mCurrentIconResId; ZenModeIconPickerListPreferenceController(@NonNull Context context, @NonNull String key, - @NonNull DashboardFragment fragment, @NonNull IconOptionsProvider iconOptionsProvider, + @NonNull IconPickerListener listener, @NonNull IconOptionsProvider iconOptionsProvider, @Nullable ZenModesBackend backend) { super(context, key, backend); - mFragment = fragment; + mListener = listener; mIconOptionsProvider = iconOptionsProvider; } @@ -68,20 +73,34 @@ class ZenModeIconPickerListPreferenceController extends AbstractZenModePreferenc recyclerView.setLayoutManager(new AutoFitGridLayoutManager(mContext)); recyclerView.setAdapter(mAdapter); recyclerView.setHasFixedSize(true); - } - - @VisibleForTesting - void onIconSelected(@DrawableRes int resId) { - saveMode(mode -> { - mode.getRule().setIconResId(resId); - return mode; - }); - mFragment.finish(); + if (recyclerView.getItemAnimator() instanceof SimpleItemAnimator animator) { + animator.setSupportsChangeAnimations(true); + } } @Override void updateState(Preference preference, @NonNull ZenMode zenMode) { - // Nothing to do, the current icon is shown in a different preference. + updateIconSelection(zenMode.getRule().getIconResId()); + } + + private void updateIconSelection(@DrawableRes int iconResId) { + if (iconResId != mCurrentIconResId) { + int oldIconResId = mCurrentIconResId; + mCurrentIconResId = iconResId; + if (mAdapter != null) { + mAdapter.notifyIconChanged(oldIconResId); + mAdapter.notifyIconChanged(mCurrentIconResId); + } + } + } + + private void onIconSelected(@DrawableRes int iconResId) { + updateIconSelection(iconResId); + mListener.onIconSelected(iconResId); + } + + interface IconPickerListener { + void onIconSelected(@DrawableRes int iconResId); } private class IconHolder extends RecyclerView.ViewHolder { @@ -93,20 +112,25 @@ class ZenModeIconPickerListPreferenceController extends AbstractZenModePreferenc mImageView = itemView.findViewById(R.id.icon_image_view); } - void bindIcon(IconOptionsProvider.IconInfo icon) { - mImageView.setImageDrawable( - IconUtil.makeIconCircle(itemView.getContext(), icon.resId())); + void bindIcon(IconOptionsProvider.IconInfo icon, Drawable iconDrawable) { + mImageView.setImageDrawable(iconDrawable); itemView.setContentDescription(icon.description()); - itemView.setOnClickListener(v -> onIconSelected(icon.resId())); + itemView.setOnClickListener(v -> { + itemView.setSelected(true); // Immediately, to avoid flicker until we rebind. + onIconSelected(icon.resId()); + }); + itemView.setSelected(icon.resId() == mCurrentIconResId); } } private class IconAdapter extends RecyclerView.Adapter { private final ImmutableList mIconResources; + private final Map mIconCache; private IconAdapter(IconOptionsProvider iconOptionsProvider) { mIconResources = iconOptionsProvider.getIcons(); + mIconCache = new HashMap<>(); } @NonNull @@ -119,13 +143,24 @@ class ZenModeIconPickerListPreferenceController extends AbstractZenModePreferenc @Override public void onBindViewHolder(@NonNull IconHolder holder, int position) { - holder.bindIcon(mIconResources.get(position)); + IconOptionsProvider.IconInfo iconInfo = mIconResources.get(position); + Drawable iconDrawable = mIconCache.computeIfAbsent(iconInfo, + info -> IconUtil.makeIconCircle(mContext, info.resId())); + holder.bindIcon(iconInfo, iconDrawable); } @Override public int getItemCount() { return mIconResources.size(); } + + private void notifyIconChanged(@DrawableRes int iconResId) { + int position = Iterables.indexOf(mIconResources, + iconInfo -> iconInfo.resId() == iconResId); + if (position != -1) { + notifyItemChanged(position); + } + } } private static class AutoFitGridLayoutManager extends GridLayoutManager { diff --git a/tests/robotests/src/com/android/settings/notification/modes/TestModeBuilder.java b/tests/robotests/src/com/android/settings/notification/modes/TestModeBuilder.java index fa12b30590c..26c7fe170c0 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/TestModeBuilder.java +++ b/tests/robotests/src/com/android/settings/notification/modes/TestModeBuilder.java @@ -24,6 +24,7 @@ import android.service.notification.ZenDeviceEffects; import android.service.notification.ZenModeConfig; import android.service.notification.ZenPolicy; +import androidx.annotation.DrawableRes; import androidx.annotation.Nullable; import com.android.settingslib.notification.modes.ZenMode; @@ -70,13 +71,13 @@ class TestModeBuilder { return this; } - public TestModeBuilder setName(String name) { + TestModeBuilder setName(String name) { mRule.setName(name); mConfigZenRule.name = name; return this; } - public TestModeBuilder setPackage(String pkg) { + TestModeBuilder setPackage(String pkg) { mRule.setPackageName(pkg); mConfigZenRule.pkg = pkg; return this; @@ -114,7 +115,7 @@ class TestModeBuilder { return this; } - public TestModeBuilder setEnabled(boolean enabled) { + TestModeBuilder setEnabled(boolean enabled) { mRule.setEnabled(enabled); mConfigZenRule.enabled = enabled; return this; @@ -126,12 +127,17 @@ class TestModeBuilder { return this; } - public TestModeBuilder setTriggerDescription(@Nullable String triggerDescription) { + TestModeBuilder setTriggerDescription(@Nullable String triggerDescription) { mRule.setTriggerDescription(triggerDescription); mConfigZenRule.triggerDescription = triggerDescription; return this; } + TestModeBuilder setIconResId(@DrawableRes int iconResId) { + mRule.setIconResId(iconResId); + return this; + } + TestModeBuilder setActive(boolean active) { if (active) { mConfigZenRule.enabled = true; diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceControllerTest.java index 5db7e925eef..e0ca306c71c 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeIconPickerListPreferenceControllerTest.java @@ -24,13 +24,15 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.preference.PreferenceScreen; import androidx.recyclerview.widget.RecyclerView; import com.android.settings.R; -import com.android.settings.dashboard.DashboardFragment; import com.android.settingslib.notification.modes.ZenMode; import com.android.settingslib.notification.modes.ZenModesBackend; import com.android.settingslib.widget.LayoutPreference; @@ -40,35 +42,34 @@ import com.google.common.collect.ImmutableList; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; @RunWith(RobolectricTestRunner.class) public class ZenModeIconPickerListPreferenceControllerTest { - private static final ZenMode ZEN_MODE = TestModeBuilder.EXAMPLE; - - private ZenModesBackend mBackend; + private Context mContext; private ZenModeIconPickerListPreferenceController mController; - private PreferenceScreen mPreferenceScreen; + @Mock private PreferenceScreen mPreferenceScreen; + private LayoutPreference mLayoutPreference; private RecyclerView mRecyclerView; + @Mock private ZenModeIconPickerListPreferenceController.IconPickerListener mListener; @Before public void setUp() { - Context context = RuntimeEnvironment.getApplication(); - mBackend = mock(ZenModesBackend.class); + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.getApplication(); - DashboardFragment fragment = mock(DashboardFragment.class); mController = new ZenModeIconPickerListPreferenceController( - RuntimeEnvironment.getApplication(), "icon_list", fragment, - new TestIconOptionsProvider(), mBackend); + RuntimeEnvironment.getApplication(), "icon_list", mListener, + new TestIconOptionsProvider(), mock(ZenModesBackend.class)); - mRecyclerView = new RecyclerView(context); + mRecyclerView = new RecyclerView(mContext); mRecyclerView.setId(R.id.icon_list); - LayoutPreference layoutPreference = new LayoutPreference(context, mRecyclerView); - mPreferenceScreen = mock(PreferenceScreen.class); - when(mPreferenceScreen.findPreference(eq("icon_list"))).thenReturn(layoutPreference); + mLayoutPreference = new LayoutPreference(mContext, mRecyclerView); + when(mPreferenceScreen.findPreference(eq("icon_list"))).thenReturn(mLayoutPreference); } @Test @@ -80,14 +81,32 @@ public class ZenModeIconPickerListPreferenceControllerTest { } @Test - public void selectIcon_updatesMode() { - mController.setZenMode(ZEN_MODE); + public void updateState_highlightsCurrentIcon() { + ZenMode mode = new TestModeBuilder().setIconResId(R.drawable.ic_hearing).build(); + mController.displayPreference(mPreferenceScreen); - mController.onIconSelected(R.drawable.ic_android); + mController.updateZenMode(mLayoutPreference, mode); - ArgumentCaptor captor = ArgumentCaptor.forClass(ZenMode.class); - verify(mBackend).updateMode(captor.capture()); - assertThat(captor.getValue().getRule().getIconResId()).isEqualTo(R.drawable.ic_android); + assertThat(getItemViewAt(0).isSelected()).isFalse(); + assertThat(getItemViewAt(1).isSelected()).isFalse(); + assertThat(getItemViewAt(2).isSelected()).isTrue(); + } + + @Test + public void performClick_onIconItem_notifiesListener() { + mController.displayPreference(mPreferenceScreen); + + getItemViewAt(1).performClick(); + + verify(mListener).onIconSelected(R.drawable.ic_info); + } + + private View getItemViewAt(int position) { + ViewGroup fakeParent = new FrameLayout(mContext); + RecyclerView.ViewHolder viewHolder = mRecyclerView.getAdapter().onCreateViewHolder( + fakeParent, 0); + mRecyclerView.getAdapter().bindViewHolder(viewHolder, position); + return viewHolder.itemView; } private static class TestIconOptionsProvider implements IconOptionsProvider {