Refactor slice renderer to handle different card width

- Refactor SliceContextualCardRenderer to support for displaying slice
in half/full width card.
- Add two helper classes to separately deal with different card width.

Only the skeleton of the half card helper is put in this CL, the
implementation hasn't been filled in yet. Will implement the detail in
next CL.

Bug: 119655434
Test: visual, robotests
Change-Id: Iacdc90c23bf41cfa7ccae3c0c70a3b663e89307d
This commit is contained in:
Mill Chen
2018-12-19 00:07:17 +08:00
parent 06c514db6f
commit 497b3529dc
6 changed files with 350 additions and 85 deletions

View File

@@ -84,7 +84,11 @@ public class ContextualCardLookupTable {
LegacySuggestionContextualCardController.class, LegacySuggestionContextualCardController.class,
LegacySuggestionContextualCardRenderer.class)); LegacySuggestionContextualCardRenderer.class));
add(new ControllerRendererMapping(CardType.SLICE, add(new ControllerRendererMapping(CardType.SLICE,
SliceContextualCardRenderer.VIEW_TYPE, SliceContextualCardRenderer.VIEW_TYPE_FULL_WIDTH,
SliceContextualCardController.class,
SliceContextualCardRenderer.class));
add(new ControllerRendererMapping(CardType.SLICE,
SliceContextualCardRenderer.VIEW_TYPE_HALF_WIDTH,
SliceContextualCardController.class, SliceContextualCardController.class,
SliceContextualCardRenderer.class)); SliceContextualCardRenderer.class));
add(new ControllerRendererMapping(CardType.CONDITIONAL_FOOTER, add(new ControllerRendererMapping(CardType.CONDITIONAL_FOOTER,

View File

@@ -26,7 +26,6 @@ import android.view.View;
import android.widget.Button; import android.widget.Button;
import android.widget.ViewFlipper; import android.widget.ViewFlipper;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Lifecycle; import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.LifecycleObserver;
@@ -35,40 +34,40 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.OnLifecycleEvent; import androidx.lifecycle.OnLifecycleEvent;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.slice.Slice; import androidx.slice.Slice;
import androidx.slice.SliceItem;
import androidx.slice.widget.EventInfo;
import androidx.slice.widget.SliceLiveData; import androidx.slice.widget.SliceLiveData;
import androidx.slice.widget.SliceView;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.homepage.contextualcards.CardContentProvider; import com.android.settings.homepage.contextualcards.CardContentProvider;
import com.android.settings.homepage.contextualcards.ContextualCard; import com.android.settings.homepage.contextualcards.ContextualCard;
import com.android.settings.homepage.contextualcards.ContextualCardFeatureProvider;
import com.android.settings.homepage.contextualcards.ContextualCardRenderer; import com.android.settings.homepage.contextualcards.ContextualCardRenderer;
import com.android.settings.homepage.contextualcards.ControllerRendererPool; import com.android.settings.homepage.contextualcards.ControllerRendererPool;
import com.android.settings.overlay.FeatureFactory;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
/** /**
* Card renderer for {@link ContextualCard} built as slices. * Card renderer for {@link ContextualCard} built as slice full card or slice half card.
*/ */
public class SliceContextualCardRenderer implements ContextualCardRenderer, public class SliceContextualCardRenderer implements ContextualCardRenderer, LifecycleObserver {
SliceView.OnSliceActionListener, LifecycleObserver { public static final int VIEW_TYPE_FULL_WIDTH = R.layout.homepage_slice_tile;
public static final int VIEW_TYPE = R.layout.homepage_slice_tile; public static final int VIEW_TYPE_HALF_WIDTH = R.layout.homepage_slice_half_tile;
private static final String TAG = "SliceCardRenderer"; private static final String TAG = "SliceCardRenderer";
@VisibleForTesting @VisibleForTesting
final Map<Uri, LiveData<Slice>> mSliceLiveDataMap; final Map<Uri, LiveData<Slice>> mSliceLiveDataMap;
@VisibleForTesting @VisibleForTesting
final Set<SliceViewHolder> mFlippedCardSet; final Set<RecyclerView.ViewHolder> mFlippedCardSet;
private final Context mContext; private final Context mContext;
private final LifecycleOwner mLifecycleOwner; private final LifecycleOwner mLifecycleOwner;
private final ControllerRendererPool mControllerRendererPool; private final ControllerRendererPool mControllerRendererPool;
private final Set<ContextualCard> mCardSet; private final Set<ContextualCard> mCardSet;
private final SliceFullCardRendererHelper mFullCardHelper;
private final SliceHalfCardRendererHelper mHalfCardHelper;
//TODO(b/121303357): Remove isHalfWidth field from SliceContextualCardRenderer class.
private boolean mIsHalfWidth;
public SliceContextualCardRenderer(Context context, LifecycleOwner lifecycleOwner, public SliceContextualCardRenderer(Context context, LifecycleOwner lifecycleOwner,
ControllerRendererPool controllerRendererPool) { ControllerRendererPool controllerRendererPool) {
@@ -79,21 +78,26 @@ public class SliceContextualCardRenderer implements ContextualCardRenderer,
mCardSet = new ArraySet<>(); mCardSet = new ArraySet<>();
mFlippedCardSet = new ArraySet<>(); mFlippedCardSet = new ArraySet<>();
mLifecycleOwner.getLifecycle().addObserver(this); mLifecycleOwner.getLifecycle().addObserver(this);
mFullCardHelper = new SliceFullCardRendererHelper(context);
mHalfCardHelper = new SliceHalfCardRendererHelper(context);
} }
@Override @Override
public int getViewType(boolean isHalfWidth) { public int getViewType(boolean isHalfWidth) {
return VIEW_TYPE; mIsHalfWidth = isHalfWidth;
return isHalfWidth? VIEW_TYPE_HALF_WIDTH : VIEW_TYPE_FULL_WIDTH;
} }
@Override @Override
public RecyclerView.ViewHolder createViewHolder(View view) { public RecyclerView.ViewHolder createViewHolder(View view) {
return new SliceViewHolder(view); if (mIsHalfWidth) {
return mHalfCardHelper.createViewHolder(view);
}
return mFullCardHelper.createViewHolder(view);
} }
@Override @Override
public void bindView(RecyclerView.ViewHolder holder, ContextualCard card) { public void bindView(RecyclerView.ViewHolder holder, ContextualCard card) {
final SliceViewHolder cardHolder = (SliceViewHolder) holder;
final Uri uri = card.getSliceUri(); final Uri uri = card.getSliceUri();
//TODO(b/120629936): Take this out once blank card issue is fixed. //TODO(b/120629936): Take this out once blank card issue is fixed.
Log.d(TAG, "bindView - uri = " + uri); Log.d(TAG, "bindView - uri = " + uri);
@@ -103,10 +107,6 @@ public class SliceContextualCardRenderer implements ContextualCardRenderer,
return; return;
} }
cardHolder.sliceView.setScrollable(false);
cardHolder.sliceView.setTag(uri);
//TODO(b/114009676): We will soon have a field to decide what slice mode we should set.
cardHolder.sliceView.setMode(SliceView.MODE_LARGE);
LiveData<Slice> sliceLiveData = mSliceLiveDataMap.get(uri); LiveData<Slice> sliceLiveData = mSliceLiveDataMap.get(uri);
if (sliceLiveData == null) { if (sliceLiveData == null) {
@@ -125,82 +125,58 @@ public class SliceContextualCardRenderer implements ContextualCardRenderer,
//TODO(b/120629936): Take this out once blank card issue is fixed. //TODO(b/120629936): Take this out once blank card issue is fixed.
Log.d(TAG, "Slice callback - uri = " + slice.getUri()); Log.d(TAG, "Slice callback - uri = " + slice.getUri());
} }
cardHolder.sliceView.setSlice(slice); if (holder.getItemViewType() == VIEW_TYPE_HALF_WIDTH) {
mHalfCardHelper.bindView(holder, card, slice);
} else {
mFullCardHelper.bindView(holder, card, slice, mCardSet);
}
}); });
// Set this listener so we can log the interaction users make on the slice if (holder.getItemViewType() == VIEW_TYPE_HALF_WIDTH) {
cardHolder.sliceView.setOnSliceActionListener(this); initDismissalActions(holder, card, R.id.content);
} else {
// Customize slice view for Settings initDismissalActions(holder, card, R.id.slice_view);
cardHolder.sliceView.showTitleItems(true);
if (card.isLargeCard()) {
cardHolder.sliceView.showHeaderDivider(true);
cardHolder.sliceView.showActionDividers(true);
} }
initDismissalActions(cardHolder, card);
} }
private void initDismissalActions(SliceViewHolder cardHolder, ContextualCard card) { private void initDismissalActions(RecyclerView.ViewHolder holder, ContextualCard card,
cardHolder.sliceView.setOnLongClickListener(v -> { int initialViewId) {
cardHolder.viewFlipper.showNext(); // initialView is the first view in the ViewFlipper.
mFlippedCardSet.add(cardHolder); final View initialView = holder.itemView.findViewById(initialViewId);
initialView.setOnLongClickListener(v -> {
flipCardToDismissalView(holder);
mFlippedCardSet.add(holder);
return true; return true;
}); });
final Button btnKeep = cardHolder.itemView.findViewById(R.id.keep); final Button btnKeep = holder.itemView.findViewById(R.id.keep);
btnKeep.setOnClickListener(v -> { btnKeep.setOnClickListener(v -> {
cardHolder.resetCard(); mFlippedCardSet.remove(holder);
mFlippedCardSet.remove(cardHolder); resetCardView(holder);
}); });
final Button btnRemove = cardHolder.itemView.findViewById(R.id.remove); final Button btnRemove = holder.itemView.findViewById(R.id.remove);
btnRemove.setOnClickListener(v -> { btnRemove.setOnClickListener(v -> {
mControllerRendererPool.getController(mContext, card.getCardType()).onDismissed(card); mControllerRendererPool.getController(mContext, card.getCardType()).onDismissed(card);
cardHolder.resetCard(); mFlippedCardSet.remove(holder);
mFlippedCardSet.remove(cardHolder); resetCardView(holder);
mSliceLiveDataMap.get(card.getSliceUri()).removeObservers(mLifecycleOwner); mSliceLiveDataMap.get(card.getSliceUri()).removeObservers(mLifecycleOwner);
}); });
} }
@Override
public void onSliceAction(@NonNull EventInfo eventInfo, @NonNull SliceItem sliceItem) {
//TODO(b/79698338): Log user interaction
// sliceItem.getSlice().getUri() is like
// content://android.settings.slices/action/wifi/_gen/0/_gen/0
// contextualCard.getSliceUri() is prefix of sliceItem.getSlice().getUri()
for (ContextualCard card : mCardSet) {
if (sliceItem.getSlice().getUri().toString().startsWith(
card.getSliceUri().toString())) {
ContextualCardFeatureProvider contexualCardFeatureProvider =
FeatureFactory.getFactory(mContext)
.getContextualCardFeatureProvider(mContext);
contexualCardFeatureProvider.logContextualCardClick(card,
eventInfo.rowIndex, eventInfo.actionType);
break;
}
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP) @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
public void onStop() { public void onStop() {
mFlippedCardSet.stream().forEach(holder -> holder.resetCard()); mFlippedCardSet.stream().forEach(holder -> resetCardView(holder));
mFlippedCardSet.clear(); mFlippedCardSet.clear();
} }
public static class SliceViewHolder extends RecyclerView.ViewHolder { private void resetCardView(RecyclerView.ViewHolder holder) {
public final SliceView sliceView; final ViewFlipper viewFlipper = holder.itemView.findViewById(R.id.view_flipper);
public final ViewFlipper viewFlipper; viewFlipper.setDisplayedChild(0 /* whichChild */);
}
public SliceViewHolder(View view) { private void flipCardToDismissalView(RecyclerView.ViewHolder holder) {
super(view); final ViewFlipper viewFlipper = holder.itemView.findViewById(R.id.view_flipper);
sliceView = view.findViewById(R.id.slice_view); viewFlipper.showNext();
viewFlipper = view.findViewById(R.id.view_flipper);
}
public void resetCard() {
viewFlipper.setDisplayedChild(0);
}
} }
} }

View File

@@ -0,0 +1,99 @@
/*
* Copyright (C) 2019 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.slices;
import android.content.Context;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.slice.Slice;
import androidx.slice.SliceItem;
import androidx.slice.widget.EventInfo;
import androidx.slice.widget.SliceView;
import com.android.settings.R;
import com.android.settings.homepage.contextualcards.ContextualCard;
import com.android.settings.homepage.contextualcards.ContextualCardFeatureProvider;
import com.android.settings.overlay.FeatureFactory;
import java.util.Set;
/**
* Card renderer helper for {@link ContextualCard} built as slice full card.
*/
class SliceFullCardRendererHelper implements SliceView.OnSliceActionListener {
private static final String TAG = "SliceFCRendererHelper";
private final Context mContext;
private Set<ContextualCard> mCardSet;
SliceFullCardRendererHelper(Context context) {
mContext = context;
}
RecyclerView.ViewHolder createViewHolder(View view) {
return new SliceViewHolder(view);
}
void bindView(RecyclerView.ViewHolder holder, ContextualCard card, Slice slice,
Set<ContextualCard> cardSet) {
final SliceViewHolder cardHolder = (SliceViewHolder) holder;
cardHolder.sliceView.setScrollable(false);
cardHolder.sliceView.setTag(card.getSliceUri());
//TODO(b/114009676): We will soon have a field to decide what slice mode we should set.
cardHolder.sliceView.setMode(SliceView.MODE_LARGE);
cardHolder.sliceView.setSlice(slice);
mCardSet = cardSet;
// Set this listener so we can log the interaction users make on the slice
cardHolder.sliceView.setOnSliceActionListener(this);
// Customize slice view for Settings
cardHolder.sliceView.showTitleItems(true);
if (card.isLargeCard()) {
cardHolder.sliceView.showHeaderDivider(true);
cardHolder.sliceView.showActionDividers(true);
}
}
@Override
public void onSliceAction(@NonNull EventInfo eventInfo, @NonNull SliceItem sliceItem) {
// sliceItem.getSlice().getUri() is like
// content://android.settings.slices/action/wifi/_gen/0/_gen/0
// contextualCard.getSliceUri() is prefix of sliceItem.getSlice().getUri()
final ContextualCardFeatureProvider contextualCardFeatureProvider =
FeatureFactory.getFactory(mContext).getContextualCardFeatureProvider(mContext);
for (ContextualCard card : mCardSet) {
if (sliceItem.getSlice().getUri().toString().startsWith(
card.getSliceUri().toString())) {
contextualCardFeatureProvider.logContextualCardClick(card, eventInfo.rowIndex,
eventInfo.actionType);
break;
}
}
}
static class SliceViewHolder extends RecyclerView.ViewHolder {
public final SliceView sliceView;
public SliceViewHolder(View view) {
super(view);
sliceView = view.findViewById(R.id.slice_view);
}
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright (C) 2019 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.slices;
import android.content.Context;
import android.view.View;
import androidx.recyclerview.widget.RecyclerView;
import androidx.slice.Slice;
import com.android.settings.homepage.contextualcards.ContextualCard;
/**
* Card renderer helper for {@link ContextualCard} built as slice half card.
*/
class SliceHalfCardRendererHelper {
private static final String TAG = "SliceHCRendererHelper";
private final Context mContext;
SliceHalfCardRendererHelper(Context context) {
mContext = context;
}
RecyclerView.ViewHolder createViewHolder(View view) {
return null;
}
void bindView(RecyclerView.ViewHolder holder, ContextualCard card, Slice slice) {
}
}

View File

@@ -78,17 +78,6 @@ public class SliceContextualCardRendererTest {
mControllerRendererPool); mControllerRendererPool);
} }
@Test
public void bindView_shouldSetScrollableToFalse() {
RecyclerView.ViewHolder viewHolder = getSliceViewHolder();
mRenderer.bindView(viewHolder, buildContextualCard(TEST_SLICE_URI));
assertThat(
((SliceContextualCardRenderer.SliceViewHolder) viewHolder).sliceView.isScrollable
()).isFalse();
}
@Test @Test
public void bindView_invalidScheme_sliceShouldBeNull() { public void bindView_invalidScheme_sliceShouldBeNull() {
final Uri sliceUri = Uri.parse("contet://com.android.settings.slices/action/flashlight"); final Uri sliceUri = Uri.parse("contet://com.android.settings.slices/action/flashlight");
@@ -97,7 +86,7 @@ public class SliceContextualCardRendererTest {
mRenderer.bindView(viewHolder, buildContextualCard(sliceUri)); mRenderer.bindView(viewHolder, buildContextualCard(sliceUri));
assertThat( assertThat(
((SliceContextualCardRenderer.SliceViewHolder) viewHolder).sliceView.getSlice()) ((SliceFullCardRendererHelper.SliceViewHolder) viewHolder).sliceView.getSlice())
.isNull(); .isNull();
} }

View File

@@ -0,0 +1,151 @@
/*
* Copyright (C) 2019 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.slices;
import static com.android.settings.homepage.contextualcards.slices.SliceContextualCardRenderer.VIEW_TYPE_FULL_WIDTH;
import static com.google.common.truth.Truth.assertThat;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import androidx.core.graphics.drawable.IconCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.slice.Slice;
import androidx.slice.SliceProvider;
import androidx.slice.builders.ListBuilder;
import androidx.slice.builders.SliceAction;
import androidx.slice.widget.SliceLiveData;
import androidx.slice.widget.SliceView;
import com.android.settings.R;
import com.android.settings.homepage.contextualcards.ContextualCard;
import com.android.settings.homepage.contextualcards.slices.SliceFullCardRendererHelper.SliceViewHolder;
import com.android.settings.intelligence.ContextualCardProto;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import java.util.Collections;
@RunWith(RobolectricTestRunner.class)
public class SliceFullCardRendererHelperTest {
private static final Uri TEST_SLICE_URI = Uri.parse("content://test/test");
private Activity mActivity;
private SliceFullCardRendererHelper mHelper;
@Before
public void setUp() {
// Set-up specs for SliceMetadata.
SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS);
mActivity = Robolectric.buildActivity(Activity.class).create().get();
mActivity.setTheme(R.style.Theme_Settings_Home);
mHelper = new SliceFullCardRendererHelper(mActivity);
}
@Test
public void createViewHolder_shouldAlwaysReturnSliceViewHolder() {
final RecyclerView.ViewHolder viewHolder = getSliceViewHolder();
assertThat(viewHolder).isInstanceOf(SliceViewHolder.class);
}
@Test
public void bindView_shouldSetScrollableToFalse() {
final RecyclerView.ViewHolder viewHolder = getSliceViewHolder();
mHelper.bindView(viewHolder, buildContextualCard(), buildSlice(), Collections.emptySet());
assertThat(((SliceViewHolder) viewHolder).sliceView.isScrollable()).isFalse();
}
@Test
public void bindView_shouldSetTagToSliceUri() {
final RecyclerView.ViewHolder viewHolder = getSliceViewHolder();
final ContextualCard card = buildContextualCard();
mHelper.bindView(viewHolder, card, buildSlice(), Collections.emptySet());
assertThat(((SliceViewHolder) viewHolder).sliceView.getTag()).isEqualTo(card.getSliceUri());
}
@Test
public void bindView_shouldSetModeToLarge() {
final RecyclerView.ViewHolder viewHolder = getSliceViewHolder();
mHelper.bindView(viewHolder, buildContextualCard(), buildSlice(), Collections.emptySet());
assertThat(((SliceViewHolder) viewHolder).sliceView.getMode()).isEqualTo(
SliceView.MODE_LARGE);
}
@Test
public void bindView_shouldSetSlice() {
final RecyclerView.ViewHolder viewHolder = getSliceViewHolder();
mHelper.bindView(viewHolder, buildContextualCard(), buildSlice(), Collections.emptySet());
assertThat(((SliceViewHolder) viewHolder).sliceView.getSlice().getUri()).isEqualTo(
TEST_SLICE_URI);
}
private RecyclerView.ViewHolder getSliceViewHolder() {
final RecyclerView recyclerView = new RecyclerView(mActivity);
recyclerView.setLayoutManager(new LinearLayoutManager(mActivity));
final View view = LayoutInflater.from(mActivity).inflate(VIEW_TYPE_FULL_WIDTH, recyclerView,
false);
return mHelper.createViewHolder(view);
}
private ContextualCard buildContextualCard() {
return new ContextualCard.Builder()
.setName("test_name")
.setCategory(ContextualCardProto.ContextualCard.Category.SUGGESTION_VALUE)
.setCardType(ContextualCard.CardType.SLICE)
.setSliceUri(TEST_SLICE_URI)
.setIsHalfWidth(false /* isHalfWidth */)
.build();
}
private Slice buildSlice() {
final String title = "test_title";
final IconCompat icon = IconCompat.createWithResource(mActivity, R.drawable.empty_icon);
final PendingIntent pendingIntent = PendingIntent.getActivity(
mActivity,
title.hashCode() /* requestCode */,
new Intent("test action"),
0 /* flags */);
final SliceAction action
= SliceAction.createDeeplink(pendingIntent, icon, ListBuilder.SMALL_IMAGE, title);
return new ListBuilder(mActivity, TEST_SLICE_URI, ListBuilder.INFINITY)
.addRow(new ListBuilder.RowBuilder()
.addEndItem(icon, ListBuilder.ICON_IMAGE)
.setTitle(title)
.setPrimaryAction(action))
.build();
}
}