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
This commit is contained in:
Mill Chen
2018-11-02 16:27:23 +08:00
committed by Fan Zhang
parent 92792ee806
commit d20641059f
10 changed files with 339 additions and 38 deletions

View File

@@ -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<ControllerRendererMapping> {
@CardType
private final int mCardType;
private final Class<? extends ContextualCardController> mControllerClass;
private final Class<? extends ContextualCardRenderer> mRendererClass;
final int mCardType;
final int mViewType;
final Class<? extends ContextualCardController> mControllerClass;
final Class<? extends ContextualCardRenderer> mRendererClass;
private ControllerRendererMapping(@CardType int cardType,
ControllerRendererMapping(@CardType int cardType, int viewType,
Class<? extends ContextualCardController> controllerClass,
Class<? extends ContextualCardRenderer> 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<ControllerRendererMapping> LOOKUP_TABLE =
@VisibleForTesting
static final Set<ControllerRendererMapping> LOOKUP_TABLE =
new TreeSet<ControllerRendererMapping>() {{
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<? extends ContextualCardRenderer> getCardRendererClasses(
public static Class<? extends ContextualCardRenderer> 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<? extends ContextualCardRenderer> getCardRendererClassByViewType(
int viewType) throws IllegalStateException {
List<ControllerRendererMapping> 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;
}
if (validMappings.size() != 1) {
throw new IllegalStateException("Have duplicate VIEW_TYPE in lookup table.");
}
return validMappings.get(0).mRendererClass;
}
}

View File

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

View File

@@ -61,26 +61,26 @@ public class ContextualCardsAdapter extends RecyclerView.Adapter<RecyclerView.Vi
@Override
public int getItemViewType(int position) {
return mContextualCards.get(position).getCardType();
final ContextualCard card = mContextualCards.get(position);
final ContextualCardRenderer renderer = mControllerRendererPool.getRendererByCardType(
mContext, mLifecycleOwner, card.getCardType());
return renderer.getViewType(card.isHalfWidth());
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int cardType) {
final ContextualCardRenderer renderer = mControllerRendererPool.getRenderer(mContext,
mLifecycleOwner, cardType);
final int viewType = renderer.getViewType();
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final ContextualCardRenderer renderer = mControllerRendererPool.getRendererByViewType(
mContext, mLifecycleOwner, viewType);
final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
return renderer.createViewHolder(view);
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
final int cardType = mContextualCards.get(position).getCardType();
final ContextualCardRenderer renderer = mControllerRendererPool.getRenderer(mContext,
mLifecycleOwner, cardType);
renderer.bindView(holder, mContextualCards.get(position));
final ContextualCard card = mContextualCards.get(position);
final ContextualCardRenderer renderer = mControllerRendererPool.getRendererByCardType(
mContext, mLifecycleOwner, card.getCardType());
renderer.bindView(holder, card);
}
@Override

View File

@@ -16,12 +16,14 @@
package com.android.settings.homepage.contextualcards;
import android.annotation.NonNull;
import android.content.Context;
import android.util.Log;
import androidx.collection.ArraySet;
import androidx.lifecycle.LifecycleOwner;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.homepage.contextualcards.conditional.ConditionContextualCardController;
import com.android.settings.homepage.contextualcards.conditional.ConditionContextualCardRenderer;
import com.android.settings.homepage.contextualcards.slices.SliceContextualCardController;
@@ -63,14 +65,32 @@ public class ControllerRendererPool {
return (T) controller;
}
public Set<ContextualCardController> getControllers() {
@VisibleForTesting
Set<ContextualCardController> getControllers() {
return mControllers;
}
public ContextualCardRenderer getRenderer(Context context, LifecycleOwner lifecycleOwner,
@ContextualCard.CardType int cardType) {
@VisibleForTesting
Set<ContextualCardRenderer> getRenderers() {
return mRenderers;
}
public ContextualCardRenderer getRendererByViewType(Context context,
LifecycleOwner lifecycleOwner, int viewType) {
final Class<? extends ContextualCardRenderer> 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<? extends ContextualCardRenderer> clz =
ContextualCardLookupTable.getCardRendererClassByCardType(cardType);
return getRenderer(context, lifecycleOwner, clz);
}
private ContextualCardRenderer getRenderer(Context context, LifecycleOwner lifecycleOwner,
@NonNull Class<? extends ContextualCardRenderer> clz) {
for (ContextualCardRenderer renderer : mRenderers) {
if (renderer.getClass() == clz) {
Log.d(TAG, "Renderer is already there.");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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