Merge "Fix several issues related to CircularIconsPreference" into main

This commit is contained in:
Matías Hernández
2024-08-16 16:44:24 +00:00
committed by Android (Google) Code Review
13 changed files with 414 additions and 299 deletions

View File

@@ -58,8 +58,8 @@
android:lineBreakWordStyle="phrase"
android:maxLines="10"/>
<!-- Circular icons (32dp) will be ImageViews under this LinearLayout -->
<LinearLayout
<!-- Circular icons (32dp) will be ImageViews under this container -->
<com.android.settings.notification.modes.CircularIconsView
android:id="@+id/circles_container"
android:importantForAccessibility="noHideDescendants"
android:orientation="horizontal"

View File

@@ -61,13 +61,6 @@ abstract class AbstractZenModeHeaderController extends AbstractZenModePreference
LayoutPreference preference = checkNotNull(screen.findPreference(getPreferenceKey()));
preference.setSelectable(false);
if (mHeaderController == null) {
mHeaderController = EntityHeaderController.newInstance(
mFragment.getActivity(),
mFragment,
preference.findViewById(R.id.entity_header));
}
ImageView iconView = checkNotNull(preference.findViewById(R.id.entity_header_icon));
ViewGroup.LayoutParams layoutParams = iconView.getLayoutParams();
if (layoutParams.width != iconSizePx || layoutParams.height != iconSizePx) {
@@ -75,6 +68,14 @@ abstract class AbstractZenModeHeaderController extends AbstractZenModePreference
layoutParams.height = iconSizePx;
iconView.setLayoutParams(layoutParams);
}
if (mHeaderController == null) {
mHeaderController = EntityHeaderController.newInstance(
mFragment.getActivity(),
mFragment,
preference.findViewById(R.id.entity_header));
mHeaderController.done(false); // Make the space for the (unused) name go away.
}
}
protected void updateIcon(Preference preference, @NonNull ZenMode zenMode,

View File

@@ -16,236 +16,72 @@
package com.android.settings.notification.modes;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
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.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
import com.android.settingslib.RestrictedPreference;
import com.google.common.base.Equivalence;
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 CircularIconsPreference extends RestrictedPreference {
private static final float DISABLED_ITEM_ALPHA = 0.3f;
record LoadedIcons(ImmutableList<Drawable> 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<List<Drawable>> 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);
}
<T> void displayIcons(CircularIconSet<T> iconSet) {
displayIcons(iconSet, null);
<T> void setIcons(CircularIconSet<T> iconSet) {
setIcons(iconSet, null);
}
<T> void displayIcons(CircularIconSet<T> iconSet, @Nullable Equivalence<T> itemEquivalence) {
if (mIconSet != null && mIconSet.hasSameItemsAs(iconSet, itemEquivalence)) {
<T> void setIcons(CircularIconSet<T> iconSet, @Nullable Equivalence<T> 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<ListenableFuture<Drawable>> 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 <numImages> 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<? extends View> 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);
}
}

View File

@@ -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<Drawable> icons, int extraItems) { }
private Executor mUiExecutor;
private int mNumberOfCirclesThatFit;
// Chronologically, fields will be set top-to-bottom.
@Nullable private CircularIconSet<?> mIconSet;
@Nullable private ListenableFuture<List<Drawable>> 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;
}
<T> void setIcons(CircularIconSet<T> 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<ListenableFuture<Drawable>> 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 <numImages> 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<? extends View> 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;
}
}

View File

@@ -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 });

View File

@@ -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<AppEntry> 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);
}

View File

@@ -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<Integer> getSoundIcons(ZenPolicy policy) {

View File

@@ -96,7 +96,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);
}

View File

@@ -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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> threeIcons = new CircularIconSet<>(ImmutableList.of(1, 2, 3),
ColorDrawable::new);
CircularIconSet<Integer> twoIcons = new CircularIconSet<>(ImmutableList.of(1, 2),
ColorDrawable::new);
CircularIconSet<Integer> 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<Integer> one = new CircularIconSet<>(ImmutableList.of(1, 2, 3),
ColorDrawable::new);
CircularIconSet<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Drawable> getIcons(ViewGroup container) {
private static List<Drawable> getDrawables(ViewGroup container) {
ArrayList<Drawable> drawables = new ArrayList<>();
for (int i = 0; i < container.getChildCount(); i++) {
if (container.getChildAt(i) instanceof ImageView imageView) {

View File

@@ -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

View File

@@ -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;
@@ -52,6 +53,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;
@@ -82,6 +84,7 @@ public final class ZenModeAppsLinkPreferenceControllerTest {
private ZenModeAppsLinkPreferenceController mController;
private CircularIconsPreference mPreference;
private CircularIconsView mIconsView;
private Context mContext;
@Mock
@@ -114,6 +117,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);
@@ -273,7 +278,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
@@ -313,7 +318,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);
@@ -322,7 +327,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
@@ -343,11 +348,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);
@@ -356,7 +361,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

View File

@@ -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()));
}
}

View File

@@ -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());
}