diff --git a/res/layout/preference_circular_icons.xml b/res/layout/preference_circular_icons.xml index 863d2288513..e1d7cfeb470 100644 --- a/res/layout/preference_circular_icons.xml +++ b/res/layout/preference_circular_icons.xml @@ -58,8 +58,8 @@ android:lineBreakWordStyle="phrase" android:maxLines="10"/> - - + icons, int extraItems) { - static final LoadedIcons EMPTY = new LoadedIcons(ImmutableList.of(), 0); - } - - private Executor mUiExecutor; - - // Chronologically, fields will be set top-to-bottom. - @Nullable private CircularIconSet mIconSet; - @Nullable private ListenableFuture> mPendingLoadIconsFuture; - @Nullable private LoadedIcons mLoadedIcons; + private CircularIconSet mIconSet = CircularIconSet.EMPTY; public CircularIconsPreference(Context context) { super(context); - init(context); - } - - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - public CircularIconsPreference(Context context, Executor uiExecutor) { - this(context); - mUiExecutor = uiExecutor; + init(); } public CircularIconsPreference(Context context, AttributeSet attrs) { super(context, attrs); - init(context); + init(); } public CircularIconsPreference(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - init(context); + init(); } public CircularIconsPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - init(context); + init(); } - private void init(Context context) { - mUiExecutor = context.getMainExecutor(); + private void init() { setLayoutResource(R.layout.preference_circular_icons); } - void displayIcons(CircularIconSet iconSet) { - displayIcons(iconSet, null); + void setIcons(CircularIconSet iconSet) { + setIcons(iconSet, null); } - void displayIcons(CircularIconSet iconSet, @Nullable Equivalence itemEquivalence) { - if (mIconSet != null && mIconSet.hasSameItemsAs(iconSet, itemEquivalence)) { + void setIcons(CircularIconSet iconSet, @Nullable Equivalence itemEquivalence) { + if (mIconSet.hasSameItemsAs(iconSet, itemEquivalence)) { return; } + mIconSet = iconSet; - - mLoadedIcons = null; - if (mPendingLoadIconsFuture != null) { - mPendingLoadIconsFuture.cancel(true); - mPendingLoadIconsFuture = null; - } - notifyChanged(); } @Override public void onBindViewHolder(PreferenceViewHolder holder) { super.onBindViewHolder(holder); + CircularIconsView iconContainer = checkNotNull( + (CircularIconsView) holder.findViewById(R.id.circles_container)); - LinearLayout iconContainer = checkNotNull( - (LinearLayout) holder.findViewById(R.id.circles_container)); - bindIconContainer(iconContainer); - } - - private void bindIconContainer(LinearLayout container) { - if (mLoadedIcons != null) { - // We have the icons ready to display already, show them. - setDrawables(container, mLoadedIcons); - } else if (mIconSet != null) { - // We know what icons we want, but haven't yet loaded them. - if (mIconSet.size() == 0) { - container.setVisibility(View.GONE); - mLoadedIcons = LoadedIcons.EMPTY; - return; - } - container.setVisibility(View.VISIBLE); - if (container.getMeasuredWidth() != 0) { - startLoadingIcons(container, mIconSet); - } else { - container.getViewTreeObserver().addOnGlobalLayoutListener( - new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - container.getViewTreeObserver().removeOnGlobalLayoutListener(this); - notifyChanged(); - } - } - ); - } - } - } - - private void startLoadingIcons(LinearLayout container, CircularIconSet iconSet) { - Resources res = getContext().getResources(); - int availableSpace = container.getMeasuredWidth(); - int iconHorizontalSpace = res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_diameter) - + res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_margin_between); - int numIconsThatFit = availableSpace / iconHorizontalSpace; - - List> iconFutures; - int extraItems; - if (iconSet.size() > numIconsThatFit) { - // Reserve one space for the (+xx) textview. - int numIconsToShow = numIconsThatFit - 1; - if (numIconsToShow < 0) { - numIconsToShow = 0; - } - iconFutures = iconSet.getIcons(numIconsToShow); - extraItems = iconSet.size() - numIconsToShow; - } else { - // Fit exactly or with remaining space. - iconFutures = iconSet.getIcons(); - extraItems = 0; - } - - // Display icons when all are ready (more consistent than randomly loading). - mPendingLoadIconsFuture = Futures.allAsList(iconFutures); - FutureUtil.whenDone( - mPendingLoadIconsFuture, - icons -> { - mLoadedIcons = new LoadedIcons(ImmutableList.copyOf(icons), extraItems); - notifyChanged(); // So that view is rebound and icons actually shown. - }, - mUiExecutor); - } - - private void setDrawables(LinearLayout container, LoadedIcons loadedIcons) { - // Rearrange child views until we have ImageViews... - LayoutInflater inflater = LayoutInflater.from(getContext()); - int numImages = loadedIcons.icons.size(); - int numImageViews = getChildCount(container, ImageView.class); - if (numImages > numImageViews) { - for (int i = 0; i < numImages - numImageViews; i++) { - ImageView imageView = (ImageView) inflater.inflate( - R.layout.preference_circular_icons_item, container, false); - container.addView(imageView, 0); - } - } else if (numImageViews > numImages) { - for (int i = 0; i < numImageViews - numImages; i++) { - container.removeViewAt(0); - } - } - // ... plus 0/1 TextViews at the end. - if (loadedIcons.extraItems > 0 && !(getLastChild(container) instanceof TextView)) { - TextView plusView = (TextView) inflater.inflate( - R.layout.preference_circular_icons_plus_item, container, false); - container.addView(plusView); - } else if (loadedIcons.extraItems == 0 && (getLastChild(container) instanceof TextView)) { - container.removeViewAt(container.getChildCount() - 1); - } - - // Show images (and +n if needed). - for (int i = 0; i < numImages; i++) { - ImageView imageView = (ImageView) container.getChildAt(i); - imageView.setImageDrawable(loadedIcons.icons.get(i)); - } - if (loadedIcons.extraItems > 0) { - TextView textView = (TextView) checkNotNull(getLastChild(container)); - textView.setText(getContext().getString(R.string.zen_mode_plus_n_items, - loadedIcons.extraItems)); - } - - // Apply enabled/disabled style. - for (int i = 0; i < container.getChildCount(); i++) { - View child = container.getChildAt(i); - child.setAlpha(isEnabled() ? 1.0f : DISABLED_ITEM_ALPHA); - } - } - - private static int getChildCount(ViewGroup parent, Class childClass) { - int count = 0; - for (int i = 0; i < parent.getChildCount(); i++) { - if (childClass.isInstance(parent.getChildAt(i))) { - count++; - } - } - return count; - } - - @Nullable - private static View getLastChild(ViewGroup parent) { - if (parent.getChildCount() == 0) { - return null; - } - return parent.getChildAt(parent.getChildCount() - 1); - } - - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - @Nullable - LoadedIcons getLoadedIcons() { - return mLoadedIcons; + iconContainer.setVisibility(mIconSet != null && mIconSet.size() == 0 ? GONE : VISIBLE); + iconContainer.setEnabled(isEnabled()); + iconContainer.setIcons(mIconSet); } } diff --git a/src/com/android/settings/notification/modes/CircularIconsView.java b/src/com/android/settings/notification/modes/CircularIconsView.java new file mode 100644 index 00000000000..b0e4280129b --- /dev/null +++ b/src/com/android/settings/notification/modes/CircularIconsView.java @@ -0,0 +1,232 @@ +/* + * 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.settings.notification.modes; + +import static com.google.common.base.Preconditions.checkNotNull; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.settings.R; + +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.List; +import java.util.concurrent.Executor; + +public class CircularIconsView extends LinearLayout { + + private static final float DISABLED_ITEM_ALPHA = 0.3f; + + record Icons(ImmutableList icons, int extraItems) { } + + private Executor mUiExecutor; + private int mNumberOfCirclesThatFit; + + // Chronologically, fields will be set top-to-bottom. + @Nullable private CircularIconSet mIconSet; + @Nullable private ListenableFuture> mPendingLoadIconsFuture; + @Nullable private Icons mDisplayedIcons; + + public CircularIconsView(Context context) { + super(context); + setUiExecutor(context.getMainExecutor()); + } + + public CircularIconsView(Context context, AttributeSet attrs) { + super(context, attrs); + setUiExecutor(context.getMainExecutor()); + } + + public CircularIconsView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setUiExecutor(context.getMainExecutor()); + } + + public CircularIconsView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + setUiExecutor(context.getMainExecutor()); + } + + @VisibleForTesting + void setUiExecutor(Executor uiExecutor) { + mUiExecutor = uiExecutor; + } + + void setIcons(CircularIconSet iconSet) { + if (mIconSet != null && mIconSet.equals(iconSet)) { + return; + } + + mIconSet = checkNotNull(iconSet); + cancelPendingTasks(); + if (getMeasuredWidth() != 0) { + startLoadingIcons(iconSet); + } + } + + private void cancelPendingTasks() { + mDisplayedIcons = null; + if (mPendingLoadIconsFuture != null) { + mPendingLoadIconsFuture.cancel(true); + mPendingLoadIconsFuture = null; + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + int numFitting = getNumberOfCirclesThatFit(); + if (mNumberOfCirclesThatFit != numFitting) { + // View has been measured for the first time OR its dimensions have changed since then. + // Keep track, because we want to reload stuff if more (or less) items fit. + mNumberOfCirclesThatFit = numFitting; + + if (mIconSet != null) { + cancelPendingTasks(); + startLoadingIcons(mIconSet); + } + } + } + + private int getNumberOfCirclesThatFit() { + Resources res = getContext().getResources(); + int availableSpace = getMeasuredWidth(); + int iconHorizontalSpace = res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_diameter) + + res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_margin_between); + return availableSpace / iconHorizontalSpace; + } + + private void startLoadingIcons(CircularIconSet iconSet) { + int numCirclesThatFit = getNumberOfCirclesThatFit(); + + List> iconFutures; + int extraItems; + if (iconSet.size() > numCirclesThatFit) { + // Reserve one space for the (+xx) textview. + int numIconsToShow = numCirclesThatFit - 1; + if (numIconsToShow < 0) { + numIconsToShow = 0; + } + iconFutures = iconSet.getIcons(numIconsToShow); + extraItems = iconSet.size() - numIconsToShow; + } else { + // Fit exactly or with remaining space. + iconFutures = iconSet.getIcons(); + extraItems = 0; + } + + // Display icons when all are ready (more consistent than randomly loading). + mPendingLoadIconsFuture = Futures.allAsList(iconFutures); + FutureUtil.whenDone( + mPendingLoadIconsFuture, + icons -> setDrawables(new Icons(ImmutableList.copyOf(icons), extraItems)), + mUiExecutor); + } + + private void setDrawables(Icons icons) { + mDisplayedIcons = icons; + + // Rearrange child views until we have ImageViews... + LayoutInflater inflater = LayoutInflater.from(getContext()); + int numImages = icons.icons.size(); + int numImageViews = getChildCount(ImageView.class); + if (numImages > numImageViews) { + for (int i = 0; i < numImages - numImageViews; i++) { + ImageView imageView = (ImageView) inflater.inflate( + R.layout.preference_circular_icons_item, this, false); + addView(imageView, 0); + } + } else if (numImageViews > numImages) { + for (int i = 0; i < numImageViews - numImages; i++) { + removeViewAt(0); + } + } + // ... plus 0/1 TextViews at the end. + if (icons.extraItems > 0 && !(getLastChild() instanceof TextView)) { + TextView plusView = (TextView) inflater.inflate( + R.layout.preference_circular_icons_plus_item, this, false); + this.addView(plusView); + } else if (icons.extraItems == 0 && (getLastChild() instanceof TextView)) { + removeViewAt(getChildCount() - 1); + } + + // Show images (and +n if needed). + for (int i = 0; i < numImages; i++) { + ImageView imageView = (ImageView) getChildAt(i); + imageView.setImageDrawable(icons.icons.get(i)); + } + if (icons.extraItems > 0) { + TextView textView = (TextView) checkNotNull(getLastChild()); + textView.setText(getContext().getString(R.string.zen_mode_plus_n_items, + icons.extraItems)); + } + + applyEnabledDisabledAppearance(isEnabled()); + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + applyEnabledDisabledAppearance(isEnabled()); + } + + private void applyEnabledDisabledAppearance(boolean enabled) { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + child.setAlpha(enabled ? 1.0f : DISABLED_ITEM_ALPHA); + } + } + + private int getChildCount(Class childClass) { + int count = 0; + for (int i = 0; i < getChildCount(); i++) { + if (childClass.isInstance(getChildAt(i))) { + count++; + } + } + return count; + } + + @Nullable + private View getLastChild() { + if (getChildCount() == 0) { + return null; + } + return getChildAt(getChildCount() - 1); + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + @Nullable + Icons getDisplayedIcons() { + return mDisplayedIcons; + } +} diff --git a/src/com/android/settings/notification/modes/IconUtil.java b/src/com/android/settings/notification/modes/IconUtil.java index dc4d875869e..33d0d961698 100644 --- a/src/com/android/settings/notification/modes/IconUtil.java +++ b/src/com/android/settings/notification/modes/IconUtil.java @@ -79,6 +79,7 @@ class IconUtil { @Px int innerSizePx = res.getDimensionPixelSize(R.dimen.zen_mode_header_inner_icon_size); Drawable base = composeIcons( + context.getResources(), background, Utils.getColorAttr(context, com.android.internal.R.attr.materialColorSecondaryContainer), @@ -89,6 +90,7 @@ class IconUtil { innerSizePx); Drawable selected = composeIcons( + context.getResources(), background, Utils.getColorAttr(context, com.android.internal.R.attr.materialColorPrimary), outerSizePx, @@ -111,6 +113,7 @@ class IconUtil { */ static Drawable makeIconPickerHeader(@NonNull Context context, Drawable icon) { return composeIconCircle( + context.getResources(), Utils.getColorAttr(context, com.android.internal.R.attr.materialColorSecondaryContainer), context.getResources().getDimensionPixelSize( @@ -129,6 +132,7 @@ class IconUtil { */ static Drawable makeIconPickerItem(@NonNull Context context, @DrawableRes int iconResId) { return composeIconCircle( + context.getResources(), context.getColorStateList(R.color.modes_icon_selectable_background), context.getResources().getDimensionPixelSize( R.dimen.zen_mode_icon_list_item_circle_diameter), @@ -146,6 +150,7 @@ class IconUtil { static Drawable makeCircularIconPreferenceItem(@NonNull Context context, @DrawableRes int iconResId) { return composeIconCircle( + context.getResources(), Utils.getColorAttr(context, com.android.internal.R.attr.materialColorSecondaryContainer), context.getResources().getDimensionPixelSize( @@ -166,6 +171,7 @@ class IconUtil { Resources res = context.getResources(); if (Strings.isNullOrEmpty(displayName)) { return composeIconCircle( + context.getResources(), Utils.getColorAttr(context, com.android.internal.R.attr.materialColorTertiaryContainer), res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_diameter), @@ -204,17 +210,17 @@ class IconUtil { return new BitmapDrawable(context.getResources(), bitmap); } - private static Drawable composeIconCircle(ColorStateList circleColor, @Px int circleDiameterPx, - Drawable icon, ColorStateList iconColor, @Px int iconSizePx) { - return composeIcons(new ShapeDrawable(new OvalShape()), circleColor, circleDiameterPx, icon, - iconColor, iconSizePx); + private static Drawable composeIconCircle(Resources res, ColorStateList circleColor, + @Px int circleDiameterPx, Drawable icon, ColorStateList iconColor, @Px int iconSizePx) { + return composeIcons(res, new ShapeDrawable(new OvalShape()), circleColor, circleDiameterPx, + icon, iconColor, iconSizePx); } - private static Drawable composeIcons(Drawable outer, ColorStateList outerColor, + private static Drawable composeIcons(Resources res, Drawable outer, ColorStateList outerColor, @Px int outerSizePx, Drawable icon, ColorStateList iconColor, @Px int iconSizePx) { - Drawable background = checkNotNull(outer.getConstantState()).newDrawable().mutate(); + Drawable background = checkNotNull(outer.getConstantState()).newDrawable(res).mutate(); background.setTintList(outerColor); - Drawable foreground = checkNotNull(icon.getConstantState()).newDrawable().mutate(); + Drawable foreground = checkNotNull(icon.getConstantState()).newDrawable(res).mutate(); foreground.setTintList(iconColor); LayerDrawable layerDrawable = new LayerDrawable(new Drawable[] { background, foreground }); diff --git a/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java index a3cb30d5657..3f95028f0c4 100644 --- a/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceController.java @@ -109,7 +109,7 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr if (zenMode.getPolicy().getAllowedChannels() == ZenPolicy.CHANNEL_POLICY_NONE) { mPreference.setSummary(R.string.zen_mode_apps_none_apps); - mPreference.displayIcons(CircularIconSet.EMPTY); + mPreference.setIcons(CircularIconSet.EMPTY); if (mAppSession != null) { mAppSession.deactivateSession(); } @@ -151,7 +151,7 @@ class ZenModeAppsLinkPreferenceController extends AbstractZenModePreferenceContr ImmutableList apps = getAppsBypassingDndSortedByName(allApps); mPreference.setSummary(mSummaryHelper.getAppsSummary(mZenMode, apps)); - mPreference.displayIcons(new CircularIconSet<>(apps, + mPreference.setIcons(new CircularIconSet<>(apps, app -> mAppIconRetriever.apply(app.info)), APP_ENTRY_EQUIVALENCE); } diff --git a/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java index 15e0edcf1df..939c7a605c9 100644 --- a/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceController.java @@ -72,7 +72,7 @@ class ZenModeOtherLinkPreferenceController extends AbstractZenModePreferenceCont preference.setEnabled(zenMode.isEnabled()); preference.setSummary(mSummaryHelper.getOtherSoundCategoriesSummary(zenMode)); - ((CircularIconsPreference) preference).displayIcons(getSoundIcons(zenMode.getPolicy())); + ((CircularIconsPreference) preference).setIcons(getSoundIcons(zenMode.getPolicy())); } private CircularIconSet getSoundIcons(ZenPolicy policy) { diff --git a/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java index 4610c35ca82..08a551e9bbb 100644 --- a/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java @@ -95,7 +95,7 @@ class ZenModePeopleLinkPreferenceController extends AbstractZenModePreferenceCon preference.setEnabled(zenMode.isEnabled()); preference.setSummary(mSummaryHelper.getPeopleSummary(zenMode.getPolicy())); - ((CircularIconsPreference) preference).displayIcons(getPeopleIcons(zenMode.getPolicy()), + ((CircularIconsPreference) preference).setIcons(getPeopleIcons(zenMode.getPolicy()), PEOPLE_ITEM_EQUIVALENCE); } diff --git a/tests/robotests/src/com/android/settings/notification/modes/CircularIconsPreferenceTest.java b/tests/robotests/src/com/android/settings/notification/modes/CircularIconsPreferenceTest.java index d145f255c5b..55448329d6f 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/CircularIconsPreferenceTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/CircularIconsPreferenceTest.java @@ -62,8 +62,7 @@ public class CircularIconsPreferenceTest { private Context mContext; private CircularIconsPreference mPreference; - private PreferenceViewHolder mViewHolder; - private ViewGroup mContainer; + private CircularIconsView mContainer; private int mOneIconWidth; @@ -73,179 +72,211 @@ public class CircularIconsPreferenceTest { mContext = RuntimeEnvironment.application; CircularIconSet.sExecutorService = MoreExecutors.newDirectExecutorService(); mPreference = new TestableCircularIconsPreference(mContext); - // Tests should call bindAndMeasureViewHolder() so that icons can be added. + // Tests should call bindAndLayoutViewHolder() so that icons can be added. Resources res = mContext.getResources(); mOneIconWidth = res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_diameter) + res.getDimensionPixelSize(R.dimen.zen_mode_circular_icon_margin_between); } - private void bindAndMeasureViewHolder(int viewWidth) { + private void bindAndLayoutViewHolder(int viewWidth) { bindViewHolder(); - measureViewHolder(viewWidth); + layoutViewHolder(viewWidth); } private void bindViewHolder() { View preferenceView = LayoutInflater.from(mContext).inflate(mPreference.getLayoutResource(), null); mContainer = checkNotNull(preferenceView.findViewById(R.id.circles_container)); - mViewHolder = PreferenceViewHolder.createInstanceForTests(preferenceView); - mPreference.onBindViewHolder(mViewHolder); + mContainer.setUiExecutor(MoreExecutors.directExecutor()); + PreferenceViewHolder viewHolder = PreferenceViewHolder.createInstanceForTests( + preferenceView); + mPreference.onBindViewHolder(viewHolder); } - private void measureViewHolder(int viewWidth) { + private void layoutViewHolder(int viewWidth) { checkState(mContainer != null, "Call bindViewHolder() first!"); mContainer.measure(makeMeasureSpec(viewWidth, View.MeasureSpec.EXACTLY), makeMeasureSpec(1000, View.MeasureSpec.EXACTLY)); - mContainer.getViewTreeObserver().dispatchOnGlobalLayout(); + mContainer.layout(0, 0, viewWidth, 1000); } @Test - public void displayIcons_loadsIcons() { + public void setIcons_loadsIcons() { CircularIconSet iconSet = new CircularIconSet<>(ImmutableList.of(1, 2), ColorDrawable::new); - bindAndMeasureViewHolder(VIEW_WIDTH); - mPreference.displayIcons(iconSet); + bindAndLayoutViewHolder(VIEW_WIDTH); + mPreference.setIcons(iconSet); - assertThat(getIcons(mContainer)).hasSize(2); - assertThat(((ColorDrawable) getIcons(mContainer).get(0)).getColor()).isEqualTo(1); - assertThat(((ColorDrawable) getIcons(mContainer).get(1)).getColor()).isEqualTo(2); + assertThat(getDrawables(mContainer)).hasSize(2); + assertThat(((ColorDrawable) getDrawables(mContainer).get(0)).getColor()).isEqualTo(1); + assertThat(((ColorDrawable) getDrawables(mContainer).get(1)).getColor()).isEqualTo(2); assertThat(getPlusText(mContainer)).isNull(); } @Test - public void displayIcons_noIcons_hidesRow() { + public void setIcons_noIcons_hidesRow() { CircularIconSet iconSet = new CircularIconSet<>(ImmutableList.of(), ColorDrawable::new); - bindAndMeasureViewHolder(VIEW_WIDTH); - mPreference.displayIcons(iconSet); + bindAndLayoutViewHolder(VIEW_WIDTH); + mPreference.setIcons(iconSet); assertThat(mContainer.getVisibility()).isEqualTo(View.GONE); } @Test - public void displayIcons_exactlyMaxIcons_loadsAllIcons() throws Exception { + public void setIcons_exactlyMaxIcons_loadsAllIcons() throws Exception { int width = 300; int fittingCircles = width / mOneIconWidth; CircularIconSet iconSet = new CircularIconSet<>( IntStream.range(0, fittingCircles).boxed().toList(), ColorDrawable::new); - bindAndMeasureViewHolder(width); - mPreference.displayIcons(iconSet); + bindAndLayoutViewHolder(width); + mPreference.setIcons(iconSet); - assertThat(getIcons(mContainer)).hasSize(fittingCircles); - assertThat(getIcons(mContainer)).containsExactlyElementsIn( + assertThat(getDrawables(mContainer)).hasSize(fittingCircles); + assertThat(getDrawables(mContainer)).containsExactlyElementsIn( Futures.allAsList(iconSet.getIcons()).get()).inOrder(); assertThat(getPlusText(mContainer)).isNull(); - } @Test - public void displayIcons_tooManyIcons_loadsFirstNAndPlusIcon() throws Exception { + public void setIcons_tooManyIcons_loadsFirstNAndPlusIcon() throws Exception { int width = 300; int fittingCircles = width / mOneIconWidth; CircularIconSet iconSet = new CircularIconSet<>( IntStream.range(0, fittingCircles + 5).boxed().toList(), ColorDrawable::new); - bindAndMeasureViewHolder(width); - mPreference.displayIcons(iconSet); + bindAndLayoutViewHolder(width); + mPreference.setIcons(iconSet); // N-1 icons, plus (+6) text. - assertThat(getIcons(mContainer)).hasSize(fittingCircles - 1); - assertThat(getIcons(mContainer)).containsExactlyElementsIn( + assertThat(getDrawables(mContainer)).hasSize(fittingCircles - 1); + assertThat(getDrawables(mContainer)).containsExactlyElementsIn( Futures.allAsList(iconSet.getIcons(fittingCircles - 1)).get()) .inOrder(); assertThat(getPlusText(mContainer)).isEqualTo("+6"); } @Test - public void displayIcons_teenyTinySpace_showsPlusIcon_noCrash() { + public void setIcons_teenyTinySpace_showsPlusIcon_noCrash() { CircularIconSet iconSet = new CircularIconSet<>(ImmutableList.of(1, 2), ColorDrawable::new); - bindAndMeasureViewHolder(1); - mPreference.displayIcons(iconSet); + bindAndLayoutViewHolder(1); + mPreference.setIcons(iconSet); - assertThat(getIcons(mContainer)).isEmpty(); + assertThat(getDrawables(mContainer)).isEmpty(); assertThat(getPlusText(mContainer)).isEqualTo("+2"); } @Test - public void displayIcons_beforeBind_loadsIconsOnBindAndMeasure() { + public void setIcons_beforeBind_loadsIconsOnBindAndMeasure() { CircularIconSet iconSet = new CircularIconSet<>(ImmutableList.of(1, 2, 3), ColorDrawable::new); - mPreference.displayIcons(iconSet); - assertThat(mPreference.getLoadedIcons()).isNull(); // Hold... + mPreference.setIcons(iconSet); + assertThat(mContainer).isNull(); // Hold... bindViewHolder(); - assertThat(mPreference.getLoadedIcons()).isNull(); // Hooooold... + assertThat(getDrawables(mContainer)).hasSize(0); // Hooooold... - measureViewHolder(VIEW_WIDTH); - assertThat(mPreference.getLoadedIcons().icons()).hasSize(3); - assertThat(getIcons(mContainer)).hasSize(3); + layoutViewHolder(VIEW_WIDTH); + assertThat(getDrawables(mContainer)).hasSize(3); } @Test - public void displayIcons_beforeMeasure_loadsIconsOnMeasure() { + public void setIcons_beforeMeasure_loadsIconsOnMeasure() { CircularIconSet iconSet = new CircularIconSet<>(ImmutableList.of(1, 2, 3), ColorDrawable::new); bindViewHolder(); - mPreference.displayIcons(iconSet); - assertThat(mPreference.getLoadedIcons()).isNull(); + mPreference.setIcons(iconSet); + assertThat(getDrawables(mContainer)).hasSize(0); - measureViewHolder(VIEW_WIDTH); - assertThat(getIcons(mContainer)).hasSize(3); + layoutViewHolder(VIEW_WIDTH); + assertThat(getDrawables(mContainer)).hasSize(3); } @Test - public void displayIcons_calledAgain_reloadsIcons() { + public void setIcons_calledAgain_reloadsIcons() { CircularIconSet threeIcons = new CircularIconSet<>(ImmutableList.of(1, 2, 3), ColorDrawable::new); CircularIconSet twoIcons = new CircularIconSet<>(ImmutableList.of(1, 2), ColorDrawable::new); CircularIconSet fourIcons = new CircularIconSet<>(ImmutableList.of(1, 2, 3, 4), ColorDrawable::new); - bindAndMeasureViewHolder(VIEW_WIDTH); + bindAndLayoutViewHolder(VIEW_WIDTH); - mPreference.displayIcons(threeIcons); - assertThat(mPreference.getLoadedIcons()).isNotNull(); - assertThat(getIcons(mContainer)).hasSize(3); + mPreference.setIcons(threeIcons); + assertThat(getDrawables(mContainer)).hasSize(3); - mPreference.displayIcons(twoIcons); - assertThat(mPreference.getLoadedIcons()).isNotNull(); - assertThat(getIcons(mContainer)).hasSize(2); + mPreference.setIcons(twoIcons); + assertThat(getDrawables(mContainer)).hasSize(2); - mPreference.displayIcons(fourIcons); - assertThat(mPreference.getLoadedIcons()).isNotNull(); - assertThat(getIcons(mContainer)).hasSize(4); + mPreference.setIcons(fourIcons); + assertThat(getDrawables(mContainer)).hasSize(4); } @Test - public void displayIcons_sameSet_doesNotReloadIcons() { + public void setIcons_sameSet_doesNotReloadIcons() { CircularIconSet one = new CircularIconSet<>(ImmutableList.of(1, 2, 3), ColorDrawable::new); CircularIconSet same = Mockito.spy(new CircularIconSet<>(ImmutableList.of(1, 2, 3), ColorDrawable::new)); when(same.getIcons()).thenThrow(new RuntimeException("Shouldn't be called!")); - bindAndMeasureViewHolder(VIEW_WIDTH); + bindAndLayoutViewHolder(VIEW_WIDTH); - mPreference.displayIcons(one); + mPreference.setIcons(one); - mPreference.displayIcons(same); // if no exception, wasn't called. + mPreference.setIcons(same); // if no exception, wasn't called. } + @Test + public void sizeChanged_reloadsIconsIfDifferentFit() { + CircularIconSet largeIconSet = new CircularIconSet<>( + IntStream.range(0, 100).boxed().toList(), + ColorDrawable::new); + mPreference.setIcons(largeIconSet); + + // Base space -> some icons + int firstWidth = 600; + int firstFittingCircles = firstWidth / mOneIconWidth; + bindAndLayoutViewHolder(firstWidth); + + assertThat(getDrawables(mContainer)).hasSize(firstFittingCircles - 1); + assertThat(getPlusText(mContainer)).isEqualTo("+" + (100 - (firstFittingCircles - 1))); + + // More space -> more icons + int secondWidth = 1000; + int secondFittingCircles = secondWidth / mOneIconWidth; + assertThat(secondFittingCircles).isGreaterThan(firstFittingCircles); + bindAndLayoutViewHolder(secondWidth); + + assertThat(getDrawables(mContainer)).hasSize(secondFittingCircles - 1); + assertThat(getPlusText(mContainer)).isEqualTo("+" + (100 - (secondFittingCircles - 1))); + + // Less space -> fewer icons + int thirdWidth = 600; + int thirdFittingCircles = thirdWidth / mOneIconWidth; + bindAndLayoutViewHolder(thirdWidth); + + assertThat(getDrawables(mContainer)).hasSize(thirdFittingCircles - 1); + assertThat(getPlusText(mContainer)).isEqualTo("+" + (100 - (thirdFittingCircles - 1))); + } + + @Test public void onBindViewHolder_withDifferentView_reloadsIconsCorrectly() { View preferenceViewOne = LayoutInflater.from(mContext).inflate( mPreference.getLayoutResource(), null); - ViewGroup containerOne = preferenceViewOne.findViewById(R.id.circles_container); + CircularIconsView containerOne = preferenceViewOne.findViewById(R.id.circles_container); + containerOne.setUiExecutor(MoreExecutors.directExecutor()); PreferenceViewHolder viewHolderOne = PreferenceViewHolder.createInstanceForTests( preferenceViewOne); containerOne.measure(makeMeasureSpec(1000, View.MeasureSpec.EXACTLY), @@ -253,7 +284,8 @@ public class CircularIconsPreferenceTest { View preferenceViewTwo = LayoutInflater.from(mContext).inflate( mPreference.getLayoutResource(), null); - ViewGroup containerTwo = preferenceViewTwo.findViewById(R.id.circles_container); + CircularIconsView containerTwo = preferenceViewTwo.findViewById(R.id.circles_container); + containerTwo.setUiExecutor(MoreExecutors.directExecutor()); PreferenceViewHolder viewHolderTwo = PreferenceViewHolder.createInstanceForTests( preferenceViewTwo); containerTwo.measure(makeMeasureSpec(1000, View.MeasureSpec.EXACTLY), @@ -265,25 +297,25 @@ public class CircularIconsPreferenceTest { ColorDrawable::new); mPreference.onBindViewHolder(viewHolderOne); - mPreference.displayIcons(iconSetOne); - assertThat(getIcons(containerOne)).hasSize(3); + mPreference.setIcons(iconSetOne); + assertThat(getDrawables(containerOne)).hasSize(3); mPreference.onBindViewHolder(viewHolderTwo); - assertThat(getIcons(containerTwo)).hasSize(3); + assertThat(getDrawables(containerTwo)).hasSize(3); - mPreference.displayIcons(iconSetTwo); + mPreference.setIcons(iconSetTwo); // The second view is updated and the first view is unaffected. - assertThat(getIcons(containerTwo)).hasSize(2); - assertThat(getIcons(containerOne)).hasSize(3); + assertThat(getDrawables(containerTwo)).hasSize(2); + assertThat(getDrawables(containerOne)).hasSize(3); } @Test - public void setEnabled_afterDisplayIcons_showsEnabledOrDisabledImages() { + public void setEnabled_afterSetIcons_showsEnabledOrDisabledImages() { CircularIconSet iconSet = new CircularIconSet<>(ImmutableList.of(1, 2), ColorDrawable::new); - bindAndMeasureViewHolder(VIEW_WIDTH); - mPreference.displayIcons(iconSet); + bindAndLayoutViewHolder(VIEW_WIDTH); + mPreference.setIcons(iconSet); assertThat(getViews(mContainer)).hasSize(2); mPreference.setEnabled(false); @@ -294,13 +326,13 @@ public class CircularIconsPreferenceTest { } @Test - public void setEnabled_beforeDisplayIcons_showsEnabledOrDisabledImages() { + public void setEnabled_beforeSetIcons_showsEnabledOrDisabledImages() { CircularIconSet iconSet = new CircularIconSet<>(ImmutableList.of(1, 2), ColorDrawable::new); mPreference.setEnabled(false); - bindAndMeasureViewHolder(VIEW_WIDTH); - mPreference.displayIcons(iconSet); + bindAndLayoutViewHolder(VIEW_WIDTH); + mPreference.setIcons(iconSet); assertThat(getViews(mContainer)).hasSize(2); assertThat(getViews(mContainer).get(0).getAlpha()).isLessThan(1f); @@ -314,7 +346,7 @@ public class CircularIconsPreferenceTest { return views; } - private static List getIcons(ViewGroup container) { + private static List getDrawables(ViewGroup container) { ArrayList drawables = new ArrayList<>(); for (int i = 0; i < container.getChildCount(); i++) { if (container.getChildAt(i) instanceof ImageView imageView) { diff --git a/tests/robotests/src/com/android/settings/notification/modes/TestableCircularIconsPreference.java b/tests/robotests/src/com/android/settings/notification/modes/TestableCircularIconsPreference.java index 6fefcacad21..6c1b0597779 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/TestableCircularIconsPreference.java +++ b/tests/robotests/src/com/android/settings/notification/modes/TestableCircularIconsPreference.java @@ -20,14 +20,12 @@ import android.content.Context; import androidx.preference.PreferenceViewHolder; -import com.google.common.util.concurrent.MoreExecutors; - class TestableCircularIconsPreference extends CircularIconsPreference { private PreferenceViewHolder mLastViewHolder; TestableCircularIconsPreference(Context context) { - super(context, MoreExecutors.directExecutor()); + super(context); } @Override diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java index f656bad0f01..f77dc3ebfa0 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java @@ -19,6 +19,7 @@ package com.android.settings.notification.modes; import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID; +import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -51,6 +52,7 @@ import android.view.View; import androidx.fragment.app.Fragment; import androidx.preference.PreferenceViewHolder; +import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.applications.ApplicationsState.AppEntry; @@ -81,6 +83,7 @@ public final class ZenModeAppsLinkPreferenceControllerTest { private ZenModeAppsLinkPreferenceController mController; private CircularIconsPreference mPreference; + private CircularIconsView mIconsView; private Context mContext; @Mock @@ -113,6 +116,8 @@ public final class ZenModeAppsLinkPreferenceControllerTest { // Ensure the preference view is bound & measured (needed to add child ImageViews). View preferenceView = LayoutInflater.from(mContext).inflate(mPreference.getLayoutResource(), null); + mIconsView = checkNotNull(preferenceView.findViewById(R.id.circles_container)); + mIconsView.setUiExecutor(MoreExecutors.directExecutor()); preferenceView.measure(View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY)); PreferenceViewHolder holder = PreferenceViewHolder.createInstanceForTests(preferenceView); @@ -272,7 +277,7 @@ public final class ZenModeAppsLinkPreferenceControllerTest { appEntries.add(createAppEntry("test2", mContext.getUserId())); mController.mAppSessionCallbacks.onRebuildComplete(appEntries); - assertThat(mPreference.getLoadedIcons().icons()).hasSize(2); + assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(2); } @Test @@ -312,7 +317,7 @@ public final class ZenModeAppsLinkPreferenceControllerTest { mController.updateState(mPreference, zenModeWithNone); - assertThat(mPreference.getLoadedIcons().icons()).hasSize(0); + assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(0); verifyNoMoreInteractions(mApplicationsState); verifyNoMoreInteractions(mSession); @@ -321,7 +326,7 @@ public final class ZenModeAppsLinkPreferenceControllerTest { verify(mApplicationsState).newSession(any(), any()); verify(mSession).rebuild(any(), any(), anyBoolean()); mController.mAppSessionCallbacks.onRebuildComplete(appEntries); - assertThat(mPreference.getLoadedIcons().icons()).hasSize(1); + assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(1); } @Test @@ -342,11 +347,11 @@ public final class ZenModeAppsLinkPreferenceControllerTest { verify(mApplicationsState).newSession(any(), any()); verify(mSession).rebuild(any(), any(), anyBoolean()); mController.mAppSessionCallbacks.onRebuildComplete(appEntries); - assertThat(mPreference.getLoadedIcons().icons()).hasSize(1); + assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(1); mController.updateState(mPreference, zenModeWithNone); - assertThat(mPreference.getLoadedIcons().icons()).hasSize(0); + assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(0); verify(mSession).deactivateSession(); verifyNoMoreInteractions(mSession); verifyNoMoreInteractions(mApplicationsState); @@ -355,7 +360,7 @@ public final class ZenModeAppsLinkPreferenceControllerTest { // updateState()) is ignored. mController.mAppSessionCallbacks.onRebuildComplete(appEntries); - assertThat(mPreference.getLoadedIcons().icons()).hasSize(0); + assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(0); } @Test diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java index 8aa87e6c903..3db70fa67bc 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeOtherLinkPreferenceControllerTest.java @@ -95,7 +95,7 @@ public final class ZenModeOtherLinkPreferenceControllerTest { mController.updateState(pref, mode); - verify(pref).displayIcons(argThat(iconSet -> iconSet.size() == 3)); + verify(pref).setIcons(argThat(iconSet -> iconSet.size() == 3)); } @Test @@ -107,7 +107,7 @@ public final class ZenModeOtherLinkPreferenceControllerTest { mController.updateState(pref, mode); - verify(pref).displayIcons(argThat(iconSet -> + verify(pref).setIcons(argThat(iconSet -> iconSet.size() == ZenModeSummaryHelper.OTHER_SOUND_CATEGORIES.size())); } } \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java index a4d141e9ca3..8555d710fa0 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java @@ -22,6 +22,7 @@ import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS; import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE; import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED; +import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -50,6 +51,7 @@ import android.view.View; import androidx.preference.PreferenceViewHolder; +import com.android.settings.R; import com.android.settings.notification.modes.ZenHelperBackend.Contact; import com.android.settingslib.notification.ConversationIconFactory; import com.android.settingslib.notification.modes.TestModeBuilder; @@ -76,6 +78,7 @@ public final class ZenModePeopleLinkPreferenceControllerTest { private ZenModePeopleLinkPreferenceController mController; private CircularIconsPreference mPreference; + private CircularIconsView mIconsView; @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -94,6 +97,8 @@ public final class ZenModePeopleLinkPreferenceControllerTest { // Ensure the preference view is bound & measured (needed to add icons). View preferenceView = LayoutInflater.from(mContext).inflate(mPreference.getLayoutResource(), null); + mIconsView = checkNotNull(preferenceView.findViewById(R.id.circles_container)); + mIconsView.setUiExecutor(MoreExecutors.directExecutor()); preferenceView.measure(View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY)); PreferenceViewHolder holder = PreferenceViewHolder.createInstanceForTests(preferenceView); @@ -142,9 +147,9 @@ public final class ZenModePeopleLinkPreferenceControllerTest { mController.updateState(mPreference, mode); - assertThat(mPreference.getLoadedIcons()).isNotNull(); - assertThat(mPreference.getLoadedIcons().icons()).hasSize(2); - assertThat(mPreference.getLoadedIcons().icons().stream() + assertThat(mIconsView.getDisplayedIcons()).isNotNull(); + assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(2); + assertThat(mIconsView.getDisplayedIcons().icons().stream() .map(ColorDrawable.class::cast) .map(d -> d.getColor()).toList()) .containsExactly(2, 3).inOrder(); @@ -162,9 +167,9 @@ public final class ZenModePeopleLinkPreferenceControllerTest { mController.updateState(mPreference, mode); - assertThat(mPreference.getLoadedIcons()).isNotNull(); - assertThat(mPreference.getLoadedIcons().icons()).hasSize(4); - assertThat(mPreference.getLoadedIcons().icons().stream() + assertThat(mIconsView.getDisplayedIcons()).isNotNull(); + assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(4); + assertThat(mIconsView.getDisplayedIcons().icons().stream() .map(ColorDrawable.class::cast) .map(d -> d.getColor()).toList()) .containsExactly(1, 2, 3, 4).inOrder(); @@ -182,8 +187,8 @@ public final class ZenModePeopleLinkPreferenceControllerTest { mController.updateState(mPreference, mode); - assertThat(mPreference.getLoadedIcons()).isNotNull(); - assertThat(mPreference.getLoadedIcons().icons()).hasSize(1); + assertThat(mIconsView.getDisplayedIcons()).isNotNull(); + assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(1); verify(mHelperBackend, never()).getContactPhoto(any()); } @@ -201,8 +206,8 @@ public final class ZenModePeopleLinkPreferenceControllerTest { mController.updateState(mPreference, mode); - assertThat(mPreference.getLoadedIcons()).isNotNull(); - assertThat(mPreference.getLoadedIcons().icons()).hasSize(3); + assertThat(mIconsView.getDisplayedIcons()).isNotNull(); + assertThat(mIconsView.getDisplayedIcons().icons()).hasSize(3); verify(mConversationIconFactory, times(3)).getConversationDrawable((ShortcutInfo) any(), any(), anyInt(), anyBoolean()); }