From d20641059fd52e0e51d434c8b2fdd3bf4a97c96f Mon Sep 17 00:00:00 2001 From: Mill Chen Date: Fri, 2 Nov 2018 16:27:23 +0800 Subject: [PATCH] Adjust lookup table mechanism to support new UI of conditional card Use layout resource id as the return value of getItemViewType in the ContextualCardsAdapter to make sure the RecyclerView could work normally, and adjust the lookup table mechanism to meet the current design as well. Bug: 113451905, 112578070 Test: visual, robotest Change-Id: I8fa299e44025a0b71b6990d020e7f0683c153337 --- .../ContextualCardLookupTable.java | 55 +++++-- .../ContextualCardRenderer.java | 4 +- .../ContextualCardsAdapter.java | 22 +-- .../ControllerRendererPool.java | 28 +++- .../ConditionContextualCardRenderer.java | 5 +- .../slices/SliceContextualCardRenderer.java | 5 +- .../ContextualCardLookupTableTest.java | 115 +++++++++++++++ .../ControllerRendererPoolTest.java | 137 ++++++++++++++++++ .../ConditionContextualCardRendererTest.java | 4 +- .../SliceContextualCardRendererTest.java | 2 +- 10 files changed, 339 insertions(+), 38 deletions(-) create mode 100644 tests/robotests/src/com/android/settings/homepage/contextualcards/ContextualCardLookupTableTest.java create mode 100644 tests/robotests/src/com/android/settings/homepage/contextualcards/ControllerRendererPoolTest.java diff --git a/src/com/android/settings/homepage/contextualcards/ContextualCardLookupTable.java b/src/com/android/settings/homepage/contextualcards/ContextualCardLookupTable.java index 0268fb097c8..2ac0ad6a910 100644 --- a/src/com/android/settings/homepage/contextualcards/ContextualCardLookupTable.java +++ b/src/com/android/settings/homepage/contextualcards/ContextualCardLookupTable.java @@ -16,43 +16,57 @@ package com.android.settings.homepage.contextualcards; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; + import com.android.settings.homepage.contextualcards.ContextualCard.CardType; import com.android.settings.homepage.contextualcards.conditional.ConditionContextualCardController; import com.android.settings.homepage.contextualcards.conditional.ConditionContextualCardRenderer; import com.android.settings.homepage.contextualcards.slices.SliceContextualCardController; import com.android.settings.homepage.contextualcards.slices.SliceContextualCardRenderer; +import java.util.Comparator; +import java.util.List; import java.util.Set; import java.util.TreeSet; +import java.util.stream.Collectors; public class ContextualCardLookupTable { - + private static final String TAG = "ContextualCardLookup"; static class ControllerRendererMapping implements Comparable { @CardType - private final int mCardType; - private final Class mControllerClass; - private final Class mRendererClass; + final int mCardType; + final int mViewType; + final Class mControllerClass; + final Class mRendererClass; - private ControllerRendererMapping(@CardType int cardType, + ControllerRendererMapping(@CardType int cardType, int viewType, Class controllerClass, Class rendererClass) { mCardType = cardType; + mViewType = viewType; mControllerClass = controllerClass; mRendererClass = rendererClass; } @Override public int compareTo(ControllerRendererMapping other) { - return Integer.compare(this.mCardType, other.mCardType); + return Comparator.comparingInt((ControllerRendererMapping mapping) -> mapping.mCardType) + .thenComparingInt(mapping -> mapping.mViewType) + .compare(this, other); } } - private static final Set LOOKUP_TABLE = + @VisibleForTesting + static final Set LOOKUP_TABLE = new TreeSet() {{ add(new ControllerRendererMapping(CardType.CONDITIONAL, + ConditionContextualCardRenderer.VIEW_TYPE, ConditionContextualCardController.class, ConditionContextualCardRenderer.class)); add(new ControllerRendererMapping(CardType.SLICE, + SliceContextualCardRenderer.VIEW_TYPE, SliceContextualCardController.class, SliceContextualCardRenderer.class)); }}; @@ -67,14 +81,27 @@ public class ContextualCardLookupTable { return null; } - //TODO(b/112578070): Implement multi renderer cases. - public static Class getCardRendererClasses( + public static Class getCardRendererClassByCardType( @CardType int cardType) { - for (ControllerRendererMapping mapping : LOOKUP_TABLE) { - if (mapping.mCardType == cardType) { - return mapping.mRendererClass; - } + return LOOKUP_TABLE.stream() + .filter(m -> m.mCardType == cardType) + .findFirst() + .map(mapping -> mapping.mRendererClass) + .orElse(null); + } + + public static Class getCardRendererClassByViewType( + int viewType) throws IllegalStateException { + List validMappings = LOOKUP_TABLE.stream() + .filter(m -> m.mViewType == viewType).collect(Collectors.toList()); + if (validMappings == null || validMappings.isEmpty()) { + Log.w(TAG, "No matching mapping"); + return null; } - return null; + if (validMappings.size() != 1) { + throw new IllegalStateException("Have duplicate VIEW_TYPE in lookup table."); + } + + return validMappings.get(0).mRendererClass; } } diff --git a/src/com/android/settings/homepage/contextualcards/ContextualCardRenderer.java b/src/com/android/settings/homepage/contextualcards/ContextualCardRenderer.java index 689b57232c2..bb85bd23916 100644 --- a/src/com/android/settings/homepage/contextualcards/ContextualCardRenderer.java +++ b/src/com/android/settings/homepage/contextualcards/ContextualCardRenderer.java @@ -26,9 +26,9 @@ import androidx.recyclerview.widget.RecyclerView; public interface ContextualCardRenderer { /** - * The layout type of the controller. + * The layout type of the renderer. */ - int getViewType(); + int getViewType(boolean isHalfWidth); /** * When {@link ContextualCardsAdapter} calls {@link ContextualCardsAdapter#onCreateViewHolder}, diff --git a/src/com/android/settings/homepage/contextualcards/ContextualCardsAdapter.java b/src/com/android/settings/homepage/contextualcards/ContextualCardsAdapter.java index 865d2421361..0a8749d37d4 100644 --- a/src/com/android/settings/homepage/contextualcards/ContextualCardsAdapter.java +++ b/src/com/android/settings/homepage/contextualcards/ContextualCardsAdapter.java @@ -61,26 +61,26 @@ public class ContextualCardsAdapter extends RecyclerView.Adapter getControllers() { + @VisibleForTesting + Set getControllers() { return mControllers; } - public ContextualCardRenderer getRenderer(Context context, LifecycleOwner lifecycleOwner, - @ContextualCard.CardType int cardType) { + @VisibleForTesting + Set getRenderers() { + return mRenderers; + } + + public ContextualCardRenderer getRendererByViewType(Context context, + LifecycleOwner lifecycleOwner, int viewType) { final Class clz = - ContextualCardLookupTable.getCardRendererClasses(cardType); + ContextualCardLookupTable.getCardRendererClassByViewType(viewType); + return getRenderer(context, lifecycleOwner, clz); + } + + public ContextualCardRenderer getRendererByCardType(Context context, + LifecycleOwner lifecycleOwner, @ContextualCard.CardType int cardType) { + final Class clz = + ContextualCardLookupTable.getCardRendererClassByCardType(cardType); + return getRenderer(context, lifecycleOwner, clz); + } + + private ContextualCardRenderer getRenderer(Context context, LifecycleOwner lifecycleOwner, + @NonNull Class clz) { for (ContextualCardRenderer renderer : mRenderers) { if (renderer.getClass() == clz) { Log.d(TAG, "Renderer is already there."); diff --git a/src/com/android/settings/homepage/contextualcards/conditional/ConditionContextualCardRenderer.java b/src/com/android/settings/homepage/contextualcards/conditional/ConditionContextualCardRenderer.java index 8a3635be443..40986363046 100644 --- a/src/com/android/settings/homepage/contextualcards/conditional/ConditionContextualCardRenderer.java +++ b/src/com/android/settings/homepage/contextualcards/conditional/ConditionContextualCardRenderer.java @@ -37,6 +37,7 @@ import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; * Card renderer for {@link ConditionalContextualCard}. */ public class ConditionContextualCardRenderer implements ContextualCardRenderer { + public static final int VIEW_TYPE = R.layout.homepage_condition_tile; private final Context mContext; private final ControllerRendererPool mControllerRendererPool; @@ -48,8 +49,8 @@ public class ConditionContextualCardRenderer implements ContextualCardRenderer { } @Override - public int getViewType() { - return R.layout.homepage_condition_tile; + public int getViewType(boolean isHalfWidth) { + return VIEW_TYPE; } @Override diff --git a/src/com/android/settings/homepage/contextualcards/slices/SliceContextualCardRenderer.java b/src/com/android/settings/homepage/contextualcards/slices/SliceContextualCardRenderer.java index c2bf5360132..5fc4473dd16 100644 --- a/src/com/android/settings/homepage/contextualcards/slices/SliceContextualCardRenderer.java +++ b/src/com/android/settings/homepage/contextualcards/slices/SliceContextualCardRenderer.java @@ -45,6 +45,7 @@ import java.util.Map; */ public class SliceContextualCardRenderer implements ContextualCardRenderer, SliceView.OnSliceActionListener { + public static final int VIEW_TYPE = R.layout.homepage_slice_tile; private static final String TAG = "SliceCardRenderer"; @@ -61,8 +62,8 @@ public class SliceContextualCardRenderer implements ContextualCardRenderer, } @Override - public int getViewType() { - return R.layout.homepage_slice_tile; + public int getViewType(boolean isHalfWidth) { + return VIEW_TYPE; } @Override diff --git a/tests/robotests/src/com/android/settings/homepage/contextualcards/ContextualCardLookupTableTest.java b/tests/robotests/src/com/android/settings/homepage/contextualcards/ContextualCardLookupTableTest.java new file mode 100644 index 00000000000..4724500a294 --- /dev/null +++ b/tests/robotests/src/com/android/settings/homepage/contextualcards/ContextualCardLookupTableTest.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards; + +import static com.google.common.truth.Truth.assertThat; + +import com.android.settings.homepage.contextualcards.ContextualCardLookupTable + .ControllerRendererMapping; +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(SettingsRobolectricTestRunner.class) +public class ContextualCardLookupTableTest { + + private static final int UNSUPPORTED_CARD_TYPE = -99999; + private static final int UNSUPPORTED_VIEW_TYPE = -99999; + + private List mOriginalLookupTable; + + @Before + public void setUp() { + mOriginalLookupTable = new ArrayList<>(); + ContextualCardLookupTable.LOOKUP_TABLE.stream() + .forEach(mapping -> mOriginalLookupTable.add(mapping)); + } + + @After + public void reset() { + ContextualCardLookupTable.LOOKUP_TABLE.clear(); + ContextualCardLookupTable.LOOKUP_TABLE.addAll(mOriginalLookupTable); + } + + @Test + public void getCardControllerClass_hasSupportedCardType_shouldGetCorrespondingController() { + for (ControllerRendererMapping mapping : ContextualCardLookupTable.LOOKUP_TABLE) { + assertThat(ContextualCardLookupTable.getCardControllerClass(mapping.mCardType)) + .isEqualTo(mapping.mControllerClass); + } + } + + @Test + public void getCardControllerClass_hasUnsupportedCardType_shouldAlwaysGetNull() { + assertThat(ContextualCardLookupTable.getCardControllerClass(UNSUPPORTED_CARD_TYPE)) + .isNull(); + } + + @Test + public void + getCardRendererClassByViewType_hasSupportedViewType_shouldGetCorrespondingRenderer() { + for (ControllerRendererMapping mapping : ContextualCardLookupTable.LOOKUP_TABLE) { + assertThat(ContextualCardLookupTable.getCardRendererClassByViewType(mapping.mViewType)) + .isEqualTo(mapping.mRendererClass); + } + } + + @Test + public void getCardRendererClassByViewType_hasUnsupportedViewType_shouldAlwaysGetNull() { + assertThat(ContextualCardLookupTable.getCardRendererClassByViewType( + UNSUPPORTED_VIEW_TYPE)).isNull(); + } + + @Test(expected = IllegalStateException.class) + public void + getCardRendererClassByViewType_hasDuplicateViewType_shouldThrowsIllegalStateException() { + final ControllerRendererMapping mapping1 = + new ControllerRendererMapping( + 1111 /* cardType */, UNSUPPORTED_VIEW_TYPE /* viewType */, + ContextualCardController.class, ContextualCardRenderer.class + ); + final ControllerRendererMapping mapping2 = + new ControllerRendererMapping( + 2222 /* cardType */, UNSUPPORTED_VIEW_TYPE /* viewType */, + ContextualCardController.class, ContextualCardRenderer.class + ); + ContextualCardLookupTable.LOOKUP_TABLE.add(mapping1); + ContextualCardLookupTable.LOOKUP_TABLE.add(mapping2); + + ContextualCardLookupTable.getCardRendererClassByViewType(UNSUPPORTED_VIEW_TYPE); + } + + @Test + public void getRendererClassByCardType_hasSupportedCardType_shouldGetCorrespondingRenderer() { + for (ControllerRendererMapping mapping : ContextualCardLookupTable.LOOKUP_TABLE) { + assertThat(ContextualCardLookupTable.getCardRendererClassByCardType(mapping.mCardType)) + .isEqualTo(mapping.mRendererClass); + } + } + + @Test + public void getCardRendererClassByCardType_hasUnsupportedCardType_shouldAlwaysGetNull() { + assertThat(ContextualCardLookupTable.getCardRendererClassByCardType(UNSUPPORTED_CARD_TYPE)) + .isNull(); + } +} diff --git a/tests/robotests/src/com/android/settings/homepage/contextualcards/ControllerRendererPoolTest.java b/tests/robotests/src/com/android/settings/homepage/contextualcards/ControllerRendererPoolTest.java new file mode 100644 index 00000000000..e51169e3972 --- /dev/null +++ b/tests/robotests/src/com/android/settings/homepage/contextualcards/ControllerRendererPoolTest.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2018 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.homepage.contextualcards; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; + +import androidx.lifecycle.LifecycleOwner; + +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settingslib.core.lifecycle.Lifecycle; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RuntimeEnvironment; + +@RunWith(SettingsRobolectricTestRunner.class) +public class ControllerRendererPoolTest { + + private static final int UNSUPPORTED_CARD_TYPE = -99999; + private static final int UNSUPPORTED_VIEW_TYPE = -99999; + + private ControllerRendererPool mPool; + private Context mContext; + private Lifecycle mLifecycle; + private LifecycleOwner mLifecycleOwner; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + mLifecycleOwner = () -> mLifecycle; + mLifecycle = new Lifecycle(mLifecycleOwner); + + mPool = new ControllerRendererPool(); + } + + @Test + public void getController_hasSupportedCardType_shouldReturnCorrespondingController() { + ContextualCardLookupTable.LOOKUP_TABLE.stream().forEach(mapping -> assertThat( + mPool.getController(mContext, mapping.mCardType).getClass()).isEqualTo( + mapping.mControllerClass)); + } + + @Test + public void getController_hasSupportedCardType_shouldHaveTwoControllersInPool() { + final long count = ContextualCardLookupTable.LOOKUP_TABLE.stream().map( + mapping -> mapping.mControllerClass).distinct().count(); + + ContextualCardLookupTable.LOOKUP_TABLE.stream().forEach( + mapping -> mPool.getController(mContext, mapping.mCardType)); + + assertThat(mPool.getControllers()).hasSize((int) count); + } + + @Test + public void getController_hasUnsupportedCardType_shouldReturnNullAndPoolIsEmpty() { + final ContextualCardController controller = mPool.getController(mContext, + UNSUPPORTED_CARD_TYPE); + + assertThat(controller).isNull(); + assertThat(mPool.getControllers()).isEmpty(); + } + + @Test + public void getRenderer_hasSupportedViewType_shouldReturnCorrespondingRenderer() { + ContextualCardLookupTable.LOOKUP_TABLE.stream().forEach(mapping -> assertThat( + mPool.getRendererByViewType(mContext, mLifecycleOwner, + mapping.mViewType).getClass()).isEqualTo(mapping.mRendererClass)); + } + + @Test + public void getRenderer_hasSupportedViewType_shouldHaveDistinctRenderersInPool() { + final long count = ContextualCardLookupTable.LOOKUP_TABLE.stream().map( + mapping -> mapping.mRendererClass).distinct().count(); + + ContextualCardLookupTable.LOOKUP_TABLE.stream().forEach( + mapping -> mPool.getRendererByViewType(mContext, mLifecycleOwner, + mapping.mViewType)); + + assertThat(mPool.getRenderers()).hasSize((int) count); + } + + @Test + public void getRenderer_hasUnsupportedViewType_shouldReturnNullAndPoolIsEmpty() { + final ContextualCardRenderer renderer = mPool.getRendererByViewType(mContext, + mLifecycleOwner, + UNSUPPORTED_VIEW_TYPE); + + assertThat(renderer).isNull(); + assertThat(mPool.getRenderers()).isEmpty(); + } + + @Test + public void getRenderer_hasSupportedCardTypeAndWidth_shouldReturnCorrespondingRenderer() { + ContextualCardLookupTable.LOOKUP_TABLE.stream().forEach(mapping -> assertThat( + mPool.getRendererByCardType(mContext, mLifecycleOwner, + mapping.mCardType).getClass()).isEqualTo(mapping.mRendererClass)); + } + + @Test + public void getRenderer_hasSupportedCardTypeAndWidth_shouldHaveDistinctRenderersInPool() { + final long count = ContextualCardLookupTable.LOOKUP_TABLE.stream().map( + mapping -> mapping.mRendererClass).distinct().count(); + + ContextualCardLookupTable.LOOKUP_TABLE.stream().forEach( + mapping -> mPool.getRendererByCardType(mContext, mLifecycleOwner, + mapping.mCardType)); + + assertThat(mPool.getRenderers()).hasSize((int) count); + } + + @Test + public void getRenderer_hasUnsupportedCardType_shouldReturnNullAndPoolIsEmpty() { + final ContextualCardRenderer renderer = mPool.getRendererByCardType(mContext, + mLifecycleOwner, + UNSUPPORTED_CARD_TYPE); + + assertThat(renderer).isNull(); + assertThat(mPool.getRenderers()).isEmpty(); + } +} diff --git a/tests/robotests/src/com/android/settings/homepage/contextualcards/conditional/ConditionContextualCardRendererTest.java b/tests/robotests/src/com/android/settings/homepage/contextualcards/conditional/ConditionContextualCardRendererTest.java index ccfbbfb2fe8..376042d6588 100644 --- a/tests/robotests/src/com/android/settings/homepage/contextualcards/conditional/ConditionContextualCardRendererTest.java +++ b/tests/robotests/src/com/android/settings/homepage/contextualcards/conditional/ConditionContextualCardRendererTest.java @@ -61,7 +61,7 @@ public class ConditionContextualCardRendererTest { @Test public void bindView_shouldSetListener() { - final int viewType = mRenderer.getViewType(); + final int viewType = mRenderer.getViewType(false /* isHalfWidth */); final RecyclerView recyclerView = new RecyclerView(mContext); recyclerView.setLayoutManager(new LinearLayoutManager(mContext)); final View view = LayoutInflater.from(mContext).inflate(viewType, recyclerView, false); @@ -78,7 +78,7 @@ public class ConditionContextualCardRendererTest { @Test public void viewClick_shouldInvokeControllerPrimaryClick() { - final int viewType = mRenderer.getViewType(); + final int viewType = mRenderer.getViewType(false /* isHalfWidth */); final RecyclerView recyclerView = new RecyclerView(mContext); recyclerView.setLayoutManager(new LinearLayoutManager(mContext)); final View view = LayoutInflater.from(mContext).inflate(viewType, recyclerView, false); diff --git a/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/SliceContextualCardRendererTest.java b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/SliceContextualCardRendererTest.java index 025f2623695..7a07d35dce3 100644 --- a/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/SliceContextualCardRendererTest.java +++ b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/SliceContextualCardRendererTest.java @@ -113,7 +113,7 @@ public class SliceContextualCardRendererTest { } private RecyclerView.ViewHolder getSliceViewHolder() { - final int viewType = mRenderer.getViewType(); + final int viewType = mRenderer.getViewType(false /* isHalfWidth */); final RecyclerView recyclerView = new RecyclerView(mContext); recyclerView.setLayoutManager(new LinearLayoutManager(mContext)); final View view = LayoutInflater.from(mContext).inflate(viewType, recyclerView, false);