diff --git a/src/com/android/settings/notification/modes/CircularIconsPreference.java b/src/com/android/settings/notification/modes/CircularIconsPreference.java index e3cd94848b0..0766ccd5623 100644 --- a/src/com/android/settings/notification/modes/CircularIconsPreference.java +++ b/src/com/android/settings/notification/modes/CircularIconsPreference.java @@ -17,13 +17,10 @@ package com.android.settings.notification.modes; import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; import android.content.Context; import android.content.res.Resources; import android.graphics.drawable.Drawable; -import android.graphics.drawable.ShapeDrawable; -import android.graphics.drawable.shapes.OvalShape; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -39,13 +36,12 @@ import androidx.preference.PreferenceViewHolder; import com.android.settings.R; import com.android.settingslib.RestrictedPreference; -import com.android.settingslib.Utils; 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.ArrayList; import java.util.List; import java.util.concurrent.Executor; @@ -53,12 +49,14 @@ public class CircularIconsPreference extends RestrictedPreference { private static final float DISABLED_ITEM_ALPHA = 0.3f; - private Executor mUiExecutor; - @Nullable private LinearLayout mIconContainer; + record LoadedIcons(ImmutableList icons, int extraItems) { } + private Executor mUiExecutor; + + // Chronologically, fields will be set top-to-bottom. @Nullable private CircularIconSet mIconSet; - @Nullable private CircularIconSet mPendingDisplayIconSet; @Nullable private ListenableFuture> mPendingLoadIconsFuture; + @Nullable private LoadedIcons mLoadedIcons; public CircularIconsPreference(Context context) { super(context); @@ -92,30 +90,6 @@ public class CircularIconsPreference extends RestrictedPreference { setLayoutResource(R.layout.preference_circular_icons); } - @Override - public void onBindViewHolder(PreferenceViewHolder holder) { - super.onBindViewHolder(holder); - - mIconContainer = checkNotNull((LinearLayout) holder.findViewById(R.id.circles_container)); - displayIconsIfPending(); - } - - @Override - public void setEnabled(boolean enabled) { - super.setEnabled(enabled); - if (mIconContainer != null) { - applyEnabledToIcons(mIconContainer, enabled); - } - } - - private void displayIconsIfPending() { - CircularIconSet pendingIconSet = mPendingDisplayIconSet; - if (pendingIconSet != null) { - mPendingDisplayIconSet = null; - displayIconsInternal(pendingIconSet); - } - } - void displayIcons(CircularIconSet iconSet) { displayIcons(iconSet, null); } @@ -125,38 +99,55 @@ public class CircularIconsPreference extends RestrictedPreference { return; } mIconSet = iconSet; - displayIconsInternal(iconSet); + + mLoadedIcons = null; + if (mPendingLoadIconsFuture != null) { + mPendingLoadIconsFuture.cancel(true); + mPendingLoadIconsFuture = null; + } + + notifyChanged(); } - void displayIconsInternal(CircularIconSet iconSet) { - if (mIconContainer == null) { - // Too soon, wait for bind. - mPendingDisplayIconSet = iconSet; - return; - } - mIconContainer.setVisibility(iconSet.size() != 0 ? View.VISIBLE : View.GONE); - if (iconSet.size() == 0) { - return; - } - if (mIconContainer.getMeasuredWidth() == 0) { - // Too soon, wait for first measure to know width. - mPendingDisplayIconSet = iconSet; - mIconContainer.getViewTreeObserver().addOnGlobalLayoutListener( - new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - checkNotNull(mIconContainer).getViewTreeObserver() - .removeOnGlobalLayoutListener(this); - displayIconsIfPending(); - } - } - ); - return; - } + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); - mIconContainer.setVisibility(View.VISIBLE); + 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); + 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); + startLoadingIcons(container, mIconSet); + } + } + ); + } + } + } + + private void startLoadingIcons(LinearLayout container, CircularIconSet iconSet) { Resources res = getContext().getResources(); - int availableSpace = mIconContainer.getMeasuredWidth(); + 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; @@ -177,79 +168,60 @@ public class CircularIconsPreference extends RestrictedPreference { extraItems = 0; } - displayIconsWhenReady(iconFutures, extraItems); - } - - private void displayIconsWhenReady(List> iconFutures, - int extraItems) { - checkState(mIconContainer != null); - if (mPendingLoadIconsFuture != null) { - mPendingLoadIconsFuture.cancel(true); - } - - // Rearrange child views until we have ImageViews... - LayoutInflater inflater = LayoutInflater.from(getContext()); - int numImages = iconFutures.size(); - int numImageViews = getChildCount(mIconContainer, ImageView.class); - if (numImages > numImageViews) { - for (int i = 0; i < numImages - numImageViews; i++) { - ImageView imageView = (ImageView) inflater.inflate( - R.layout.preference_circular_icons_item, mIconContainer, false); - mIconContainer.addView(imageView, 0); - } - } else if (numImageViews > numImages) { - for (int i = 0; i < numImageViews - numImages; i++) { - mIconContainer.removeViewAt(0); - } - } - // ... plus 0/1 TextViews at the end. - if (extraItems > 0 && !(getLastChild(mIconContainer) instanceof TextView)) { - TextView plusView = (TextView) inflater.inflate( - R.layout.preference_circular_icons_plus_item, mIconContainer, false); - mIconContainer.addView(plusView); - } else if (extraItems == 0 && (getLastChild(mIconContainer) instanceof TextView)) { - mIconContainer.removeViewAt(mIconContainer.getChildCount() - 1); - } - - // Set up placeholders and extra items indicator. - for (int i = 0; i < numImages; i++) { - ImageView imageView = (ImageView) mIconContainer.getChildAt(i); - imageView.setImageDrawable(getPlaceholderImage(getContext())); - } - if (extraItems > 0) { - TextView textView = (TextView) checkNotNull(getLastChild(mIconContainer)); - textView.setText(getContext().getString(R.string.zen_mode_plus_n_items, extraItems)); - } - - applyEnabledToIcons(mIconContainer, isEnabled()); - // Display icons when all are ready (more consistent than randomly loading). mPendingLoadIconsFuture = Futures.allAsList(iconFutures); FutureUtil.whenDone( mPendingLoadIconsFuture, icons -> { - checkState(mIconContainer != null); - for (int i = 0; i < icons.size(); i++) { - ((ImageView) mIconContainer.getChildAt(i)).setImageDrawable(icons.get(i)); - } + mLoadedIcons = new LoadedIcons(ImmutableList.copyOf(icons), extraItems); + notifyChanged(); // So that view is rebound and icons actually shown. }, mUiExecutor); } - private void applyEnabledToIcons(ViewGroup container, boolean enabled) { + 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(enabled ? 1.0f : DISABLED_ITEM_ALPHA); + child.setAlpha(isEnabled() ? 1.0f : DISABLED_ITEM_ALPHA); } } - private static Drawable getPlaceholderImage(Context context) { - ShapeDrawable placeholder = new ShapeDrawable(new OvalShape()); - placeholder.setTintList(Utils.getColorAttr(context, - com.android.internal.R.attr.materialColorSecondaryContainer)); - return placeholder; - } - private static int getChildCount(ViewGroup parent, Class childClass) { int count = 0; for (int i = 0; i < parent.getChildCount(); i++) { @@ -268,41 +240,9 @@ public class CircularIconsPreference extends RestrictedPreference { return parent.getChildAt(parent.getChildCount() - 1); } - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - List getViews() { - if (mIconContainer == null) { - return List.of(); - } - ArrayList views = new ArrayList<>(); - for (int i = 0; i < mIconContainer.getChildCount(); i++) { - views.add(mIconContainer.getChildAt(i)); - } - return views; - } - - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - List getIcons() { - if (mIconContainer == null) { - return List.of(); - } - ArrayList drawables = new ArrayList<>(); - for (int i = 0; i < getChildCount(mIconContainer, ImageView.class); i++) { - drawables.add(((ImageView) mIconContainer.getChildAt(i)).getDrawable()); - } - return drawables; - } - @VisibleForTesting(otherwise = VisibleForTesting.NONE) @Nullable - String getPlusText() { - if (mIconContainer == null) { - return null; - } - View lastChild = getLastChild(mIconContainer); - if (lastChild instanceof TextView tv) { - return tv.getText() != null ? tv.getText().toString() : null; - } else { - return null; - } + LoadedIcons getLoadedIcons() { + return mLoadedIcons; } } diff --git a/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java index b5938c6d3ea..0d3a7219099 100644 --- a/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java +++ b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java @@ -46,9 +46,10 @@ import com.android.settings.notification.modes.ZenHelperBackend.Contact; import com.android.settingslib.notification.ConversationIconFactory; import com.android.settingslib.notification.modes.ZenMode; +import com.google.common.base.Equivalence; import com.google.common.collect.ImmutableList; -import java.util.function.Function; +import java.util.Objects; /** * Preference with a link and summary about what calls and messages can break through the mode, @@ -94,29 +95,60 @@ class ZenModePeopleLinkPreferenceController extends AbstractZenModePreferenceCon preference.setEnabled(zenMode.isEnabled()); preference.setSummary(mSummaryHelper.getPeopleSummary(zenMode.getPolicy())); - ((CircularIconsPreference) preference).displayIcons(getPeopleIcons(zenMode.getPolicy())); + ((CircularIconsPreference) preference).displayIcons(getPeopleIcons(zenMode.getPolicy()), + PEOPLE_ITEM_EQUIVALENCE); } - // Represents "Either". - record PeopleItem(@Nullable Contact contact, - @Nullable ConversationChannelWrapper conversation) { + // Represents "Either". + private record PeopleItem(boolean all, + @Nullable Contact contact, + @Nullable ConversationChannelWrapper conversation) { + + private static final PeopleItem ALL = new PeopleItem(true, null, null); PeopleItem(@NonNull Contact contact) { - this(contact, null); + this(false, contact, null); } PeopleItem(@NonNull ConversationChannelWrapper conversation) { - this(null, conversation); + this(false, null, conversation); } - } - private CircularIconSet getPeopleIcons(ZenPolicy policy) { + private static final Equivalence PEOPLE_ITEM_EQUIVALENCE = new Equivalence<>() { + @Override + protected boolean doEquivalent(@NonNull PeopleItem a, @NonNull PeopleItem b) { + if (a.all && b.all) { + return true; + } else if (a.contact != null && b.contact != null) { + return a.contact.equals(b.contact); + } else if (a.conversation != null && b.conversation != null) { + ConversationChannelWrapper c1 = a.conversation; + ConversationChannelWrapper c2 = b.conversation; + // Skip comparing ShortcutInfo which doesn't implement equals(). We assume same + // conversation channel means same icon (which is not 100% correct but unlikely to + // change while on this screen). + return Objects.equals(c1.getNotificationChannel(), c2.getNotificationChannel()) + && Objects.equals(c1.getGroupLabel(), c2.getGroupLabel()) + && Objects.equals(c1.getParentChannelLabel(), c2.getParentChannelLabel()) + && Objects.equals(c1.getPkg(), c2.getPkg()) + && Objects.equals(c1.getUid(), c2.getUid()); + } else { + return false; + } + } + + @Override + protected int doHash(@NonNull PeopleItem item) { + return Objects.hash(item.all, item.contact, item.conversation); + } + }; + + private CircularIconSet getPeopleIcons(ZenPolicy policy) { if (getCallersOrMessagesAllowed(policy) == PEOPLE_TYPE_ANYONE) { return new CircularIconSet<>( - ImmutableList.of(IconUtil.makeCircularIconPreferenceItem(mContext, - R.drawable.ic_zen_mode_people_all)), - Function.identity()); + ImmutableList.of(PeopleItem.ALL), + this::loadPeopleIcon); } ImmutableList.Builder peopleItems = ImmutableList.builder(); @@ -181,7 +213,10 @@ class ZenModePeopleLinkPreferenceController extends AbstractZenModePreferenceCon @WorkerThread private Drawable loadPeopleIcon(PeopleItem peopleItem) { - if (peopleItem.contact != null) { + if (peopleItem.all) { + return IconUtil.makeCircularIconPreferenceItem(mContext, + R.drawable.ic_zen_mode_people_all); + } else if (peopleItem.contact != null) { return mHelperBackend.getContactPhoto(peopleItem.contact); } else if (peopleItem.conversation != null) { return mConversationIconFactory.getConversationDrawable( @@ -190,7 +225,7 @@ class ZenModePeopleLinkPreferenceController extends AbstractZenModePreferenceCon peopleItem.conversation.getUid(), /* important= */ true); } else { - throw new IllegalArgumentException("Neither contact nor conversation!"); + throw new IllegalArgumentException("Neither all nor contact nor conversation!"); } } } 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 ce23fc4ba05..d145f255c5b 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/CircularIconsPreferenceTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/CircularIconsPreferenceTest.java @@ -27,9 +27,14 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.content.res.Resources; import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.annotation.Nullable; import androidx.preference.PreferenceViewHolder; import com.android.settings.R; @@ -46,6 +51,8 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import java.util.ArrayList; +import java.util.List; import java.util.stream.IntStream; @RunWith(RobolectricTestRunner.class) @@ -55,7 +62,8 @@ public class CircularIconsPreferenceTest { private Context mContext; private CircularIconsPreference mPreference; - private View mIconContainer; + private PreferenceViewHolder mViewHolder; + private ViewGroup mContainer; private int mOneIconWidth; @@ -64,7 +72,7 @@ public class CircularIconsPreferenceTest { MockitoAnnotations.initMocks(this); mContext = RuntimeEnvironment.application; CircularIconSet.sExecutorService = MoreExecutors.newDirectExecutorService(); - mPreference = new CircularIconsPreference(mContext, MoreExecutors.directExecutor()); + mPreference = new TestableCircularIconsPreference(mContext); // Tests should call bindAndMeasureViewHolder() so that icons can be added. Resources res = mContext.getResources(); @@ -80,16 +88,16 @@ public class CircularIconsPreferenceTest { private void bindViewHolder() { View preferenceView = LayoutInflater.from(mContext).inflate(mPreference.getLayoutResource(), null); - mIconContainer = checkNotNull(preferenceView.findViewById(R.id.circles_container)); - PreferenceViewHolder holder = PreferenceViewHolder.createInstanceForTests(preferenceView); - mPreference.onBindViewHolder(holder); + mContainer = checkNotNull(preferenceView.findViewById(R.id.circles_container)); + mViewHolder = PreferenceViewHolder.createInstanceForTests(preferenceView); + mPreference.onBindViewHolder(mViewHolder); } private void measureViewHolder(int viewWidth) { - checkState(mIconContainer != null, "Call bindViewHolder() first!"); - mIconContainer.measure(makeMeasureSpec(viewWidth, View.MeasureSpec.EXACTLY), + checkState(mContainer != null, "Call bindViewHolder() first!"); + mContainer.measure(makeMeasureSpec(viewWidth, View.MeasureSpec.EXACTLY), makeMeasureSpec(1000, View.MeasureSpec.EXACTLY)); - mIconContainer.getViewTreeObserver().dispatchOnGlobalLayout(); + mContainer.getViewTreeObserver().dispatchOnGlobalLayout(); } @Test @@ -100,11 +108,10 @@ public class CircularIconsPreferenceTest { bindAndMeasureViewHolder(VIEW_WIDTH); mPreference.displayIcons(iconSet); - assertThat(mPreference.getIcons()).hasSize(2); - assertThat(((ColorDrawable) mPreference.getIcons().get(0)).getColor()).isEqualTo(1); - assertThat(((ColorDrawable) mPreference.getIcons().get(1)).getColor()).isEqualTo(2); - assertThat(mPreference.getPlusText()).isNull(); - assertThat(mIconContainer.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(getIcons(mContainer)).hasSize(2); + assertThat(((ColorDrawable) getIcons(mContainer).get(0)).getColor()).isEqualTo(1); + assertThat(((ColorDrawable) getIcons(mContainer).get(1)).getColor()).isEqualTo(2); + assertThat(getPlusText(mContainer)).isNull(); } @Test @@ -115,7 +122,7 @@ public class CircularIconsPreferenceTest { bindAndMeasureViewHolder(VIEW_WIDTH); mPreference.displayIcons(iconSet); - assertThat(mIconContainer.getVisibility()).isEqualTo(View.GONE); + assertThat(mContainer.getVisibility()).isEqualTo(View.GONE); } @Test @@ -129,10 +136,10 @@ public class CircularIconsPreferenceTest { bindAndMeasureViewHolder(width); mPreference.displayIcons(iconSet); - assertThat(mPreference.getIcons()).hasSize(fittingCircles); - assertThat(mPreference.getIcons()).containsExactlyElementsIn( + assertThat(getIcons(mContainer)).hasSize(fittingCircles); + assertThat(getIcons(mContainer)).containsExactlyElementsIn( Futures.allAsList(iconSet.getIcons()).get()).inOrder(); - assertThat(mPreference.getPlusText()).isNull(); + assertThat(getPlusText(mContainer)).isNull(); } @@ -148,11 +155,11 @@ public class CircularIconsPreferenceTest { mPreference.displayIcons(iconSet); // N-1 icons, plus (+6) text. - assertThat(mPreference.getIcons()).hasSize(fittingCircles - 1); - assertThat(mPreference.getIcons()).containsExactlyElementsIn( + assertThat(getIcons(mContainer)).hasSize(fittingCircles - 1); + assertThat(getIcons(mContainer)).containsExactlyElementsIn( Futures.allAsList(iconSet.getIcons(fittingCircles - 1)).get()) .inOrder(); - assertThat(mPreference.getPlusText()).isEqualTo("+6"); + assertThat(getPlusText(mContainer)).isEqualTo("+6"); } @Test @@ -163,8 +170,8 @@ public class CircularIconsPreferenceTest { bindAndMeasureViewHolder(1); mPreference.displayIcons(iconSet); - assertThat(mPreference.getIcons()).isEmpty(); - assertThat(mPreference.getPlusText()).isEqualTo("+2"); + assertThat(getIcons(mContainer)).isEmpty(); + assertThat(getPlusText(mContainer)).isEqualTo("+2"); } @Test @@ -173,13 +180,14 @@ public class CircularIconsPreferenceTest { ColorDrawable::new); mPreference.displayIcons(iconSet); - assertThat(mPreference.getIcons()).isEmpty(); // Hold... + assertThat(mPreference.getLoadedIcons()).isNull(); // Hold... bindViewHolder(); - assertThat(mPreference.getIcons()).isEmpty(); // Hooooold... + assertThat(mPreference.getLoadedIcons()).isNull(); // Hooooold... measureViewHolder(VIEW_WIDTH); - assertThat(mPreference.getIcons()).hasSize(3); + assertThat(mPreference.getLoadedIcons().icons()).hasSize(3); + assertThat(getIcons(mContainer)).hasSize(3); } @Test @@ -189,10 +197,10 @@ public class CircularIconsPreferenceTest { bindViewHolder(); mPreference.displayIcons(iconSet); - assertThat(mPreference.getIcons()).isEmpty(); + assertThat(mPreference.getLoadedIcons()).isNull(); measureViewHolder(VIEW_WIDTH); - assertThat(mPreference.getIcons()).hasSize(3); + assertThat(getIcons(mContainer)).hasSize(3); } @Test @@ -206,11 +214,16 @@ public class CircularIconsPreferenceTest { bindAndMeasureViewHolder(VIEW_WIDTH); mPreference.displayIcons(threeIcons); - assertThat(mPreference.getIcons()).hasSize(3); + assertThat(mPreference.getLoadedIcons()).isNotNull(); + assertThat(getIcons(mContainer)).hasSize(3); + mPreference.displayIcons(twoIcons); - assertThat(mPreference.getIcons()).hasSize(2); + assertThat(mPreference.getLoadedIcons()).isNotNull(); + assertThat(getIcons(mContainer)).hasSize(2); + mPreference.displayIcons(fourIcons); - assertThat(mPreference.getIcons()).hasSize(4); + assertThat(mPreference.getLoadedIcons()).isNotNull(); + assertThat(getIcons(mContainer)).hasSize(4); } @Test @@ -224,22 +237,60 @@ public class CircularIconsPreferenceTest { bindAndMeasureViewHolder(VIEW_WIDTH); mPreference.displayIcons(one); + mPreference.displayIcons(same); // if no exception, wasn't called. } + @Test + public void onBindViewHolder_withDifferentView_reloadsIconsCorrectly() { + View preferenceViewOne = LayoutInflater.from(mContext).inflate( + mPreference.getLayoutResource(), null); + ViewGroup containerOne = preferenceViewOne.findViewById(R.id.circles_container); + PreferenceViewHolder viewHolderOne = PreferenceViewHolder.createInstanceForTests( + preferenceViewOne); + containerOne.measure(makeMeasureSpec(1000, View.MeasureSpec.EXACTLY), + makeMeasureSpec(1000, View.MeasureSpec.EXACTLY)); + + View preferenceViewTwo = LayoutInflater.from(mContext).inflate( + mPreference.getLayoutResource(), null); + ViewGroup containerTwo = preferenceViewTwo.findViewById(R.id.circles_container); + PreferenceViewHolder viewHolderTwo = PreferenceViewHolder.createInstanceForTests( + preferenceViewTwo); + containerTwo.measure(makeMeasureSpec(1000, View.MeasureSpec.EXACTLY), + makeMeasureSpec(1000, View.MeasureSpec.EXACTLY)); + + CircularIconSet iconSetOne = new CircularIconSet<>(ImmutableList.of(1, 2, 3), + ColorDrawable::new); + CircularIconSet iconSetTwo = new CircularIconSet<>(ImmutableList.of(1, 2), + ColorDrawable::new); + + mPreference.onBindViewHolder(viewHolderOne); + mPreference.displayIcons(iconSetOne); + assertThat(getIcons(containerOne)).hasSize(3); + + mPreference.onBindViewHolder(viewHolderTwo); + assertThat(getIcons(containerTwo)).hasSize(3); + + mPreference.displayIcons(iconSetTwo); + + // The second view is updated and the first view is unaffected. + assertThat(getIcons(containerTwo)).hasSize(2); + assertThat(getIcons(containerOne)).hasSize(3); + } + @Test public void setEnabled_afterDisplayIcons_showsEnabledOrDisabledImages() { CircularIconSet iconSet = new CircularIconSet<>(ImmutableList.of(1, 2), ColorDrawable::new); bindAndMeasureViewHolder(VIEW_WIDTH); mPreference.displayIcons(iconSet); - assertThat(mPreference.getViews()).hasSize(2); + assertThat(getViews(mContainer)).hasSize(2); mPreference.setEnabled(false); - assertThat(mPreference.getViews().get(0).getAlpha()).isLessThan(1f); + assertThat(getViews(mContainer).get(0).getAlpha()).isLessThan(1f); mPreference.setEnabled(true); - assertThat(mPreference.getViews().get(0).getAlpha()).isEqualTo(1f); + assertThat(getViews(mContainer).get(0).getAlpha()).isEqualTo(1f); } @Test @@ -251,7 +302,36 @@ public class CircularIconsPreferenceTest { bindAndMeasureViewHolder(VIEW_WIDTH); mPreference.displayIcons(iconSet); - assertThat(mPreference.getViews()).hasSize(2); - assertThat(mPreference.getViews().get(0).getAlpha()).isLessThan(1f); + assertThat(getViews(mContainer)).hasSize(2); + assertThat(getViews(mContainer).get(0).getAlpha()).isLessThan(1f); + } + + private static List getViews(ViewGroup container) { + ArrayList views = new ArrayList<>(); + for (int i = 0; i < container.getChildCount(); i++) { + views.add(container.getChildAt(i)); + } + return views; + } + + private static List getIcons(ViewGroup container) { + ArrayList drawables = new ArrayList<>(); + for (int i = 0; i < container.getChildCount(); i++) { + if (container.getChildAt(i) instanceof ImageView imageView) { + drawables.add(imageView.getDrawable()); + + } + } + return drawables; + } + + @Nullable + private static String getPlusText(ViewGroup container) { + View lastChild = container.getChildAt(container.getChildCount() - 1); + if (lastChild instanceof TextView tv) { + return tv.getText() != null ? tv.getText().toString() : null; + } else { + return null; + } } } diff --git a/tests/robotests/src/com/android/settings/notification/modes/TestableCircularIconsPreference.java b/tests/robotests/src/com/android/settings/notification/modes/TestableCircularIconsPreference.java new file mode 100644 index 00000000000..6fefcacad21 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/TestableCircularIconsPreference.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +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()); + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + mLastViewHolder = holder; + } + + @Override + protected void notifyChanged() { + // Calling androidx.preference.Preference.notifyChanged() will, through an internal + // listener added by PreferenceGroupAdapter, eventually rebind the Preference to its + // corresponding view in the RecyclerView. This will not happen to a Preference that is + // created without a proper PreferencesScreen/RecyclerView/etc, so we simulate it here. + if (mLastViewHolder != null) { + onBindViewHolder(mLastViewHolder); + } + } +} 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 301ff9092e0..9263ffdb8c7 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeAppsLinkPreferenceControllerTest.java @@ -101,7 +101,7 @@ public final class ZenModeAppsLinkPreferenceControllerTest { MockitoAnnotations.initMocks(this); mContext = RuntimeEnvironment.application; CircularIconSet.sExecutorService = MoreExecutors.newDirectExecutorService(); - mPreference = new CircularIconsPreference(mContext, MoreExecutors.directExecutor()); + mPreference = new TestableCircularIconsPreference(mContext); when(mApplicationsState.newSession(any(), any())).thenReturn(mSession); mController = new ZenModeAppsLinkPreferenceController( @@ -270,7 +270,7 @@ public final class ZenModeAppsLinkPreferenceControllerTest { appEntries.add(createAppEntry("test2", mContext.getUserId())); mController.mAppSessionCallbacks.onRebuildComplete(appEntries); - assertThat(mPreference.getIcons()).hasSize(2); + assertThat(mPreference.getLoadedIcons().icons()).hasSize(2); } @Test 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 63068fa201f..a4d141e9ca3 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java @@ -89,7 +89,7 @@ public final class ZenModePeopleLinkPreferenceControllerTest { MockitoAnnotations.initMocks(this); mContext = RuntimeEnvironment.application; CircularIconSet.sExecutorService = MoreExecutors.newDirectExecutorService(); - mPreference = new CircularIconsPreference(mContext, MoreExecutors.directExecutor()); + mPreference = new TestableCircularIconsPreference(mContext); // Ensure the preference view is bound & measured (needed to add icons). View preferenceView = LayoutInflater.from(mContext).inflate(mPreference.getLayoutResource(), @@ -142,8 +142,9 @@ public final class ZenModePeopleLinkPreferenceControllerTest { mController.updateState(mPreference, mode); - assertThat(mPreference.getIcons()).hasSize(2); - assertThat(mPreference.getIcons().stream() + assertThat(mPreference.getLoadedIcons()).isNotNull(); + assertThat(mPreference.getLoadedIcons().icons()).hasSize(2); + assertThat(mPreference.getLoadedIcons().icons().stream() .map(ColorDrawable.class::cast) .map(d -> d.getColor()).toList()) .containsExactly(2, 3).inOrder(); @@ -161,8 +162,9 @@ public final class ZenModePeopleLinkPreferenceControllerTest { mController.updateState(mPreference, mode); - assertThat(mPreference.getIcons()).hasSize(4); - assertThat(mPreference.getIcons().stream() + assertThat(mPreference.getLoadedIcons()).isNotNull(); + assertThat(mPreference.getLoadedIcons().icons()).hasSize(4); + assertThat(mPreference.getLoadedIcons().icons().stream() .map(ColorDrawable.class::cast) .map(d -> d.getColor()).toList()) .containsExactly(1, 2, 3, 4).inOrder(); @@ -180,7 +182,8 @@ public final class ZenModePeopleLinkPreferenceControllerTest { mController.updateState(mPreference, mode); - assertThat(mPreference.getIcons()).hasSize(1); + assertThat(mPreference.getLoadedIcons()).isNotNull(); + assertThat(mPreference.getLoadedIcons().icons()).hasSize(1); verify(mHelperBackend, never()).getContactPhoto(any()); } @@ -198,7 +201,8 @@ public final class ZenModePeopleLinkPreferenceControllerTest { mController.updateState(mPreference, mode); - assertThat(mPreference.getIcons()).hasSize(3); + assertThat(mPreference.getLoadedIcons()).isNotNull(); + assertThat(mPreference.getLoadedIcons().icons()).hasSize(3); verify(mConversationIconFactory, times(3)).getConversationDrawable((ShortcutInfo) any(), any(), anyInt(), anyBoolean()); }