diff --git a/src/com/android/settings/homepage/contextualcards/ContextualCardLookupTable.java b/src/com/android/settings/homepage/contextualcards/ContextualCardLookupTable.java index a4a84199c98..90974f651bb 100644 --- a/src/com/android/settings/homepage/contextualcards/ContextualCardLookupTable.java +++ b/src/com/android/settings/homepage/contextualcards/ContextualCardLookupTable.java @@ -84,7 +84,11 @@ public class ContextualCardLookupTable { LegacySuggestionContextualCardController.class, LegacySuggestionContextualCardRenderer.class)); 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, SliceContextualCardRenderer.class)); add(new ControllerRendererMapping(CardType.CONDITIONAL_FOOTER, diff --git a/src/com/android/settings/homepage/contextualcards/slices/SliceContextualCardRenderer.java b/src/com/android/settings/homepage/contextualcards/slices/SliceContextualCardRenderer.java index 7ca6e9aad84..5a43f66241b 100644 --- a/src/com/android/settings/homepage/contextualcards/slices/SliceContextualCardRenderer.java +++ b/src/com/android/settings/homepage/contextualcards/slices/SliceContextualCardRenderer.java @@ -26,7 +26,6 @@ import android.view.View; import android.widget.Button; import android.widget.ViewFlipper; -import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleObserver; @@ -35,40 +34,40 @@ import androidx.lifecycle.LiveData; import androidx.lifecycle.OnLifecycleEvent; import androidx.recyclerview.widget.RecyclerView; import androidx.slice.Slice; -import androidx.slice.SliceItem; -import androidx.slice.widget.EventInfo; import androidx.slice.widget.SliceLiveData; -import androidx.slice.widget.SliceView; import com.android.settings.R; import com.android.settings.homepage.contextualcards.CardContentProvider; 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.ControllerRendererPool; -import com.android.settings.overlay.FeatureFactory; import java.util.Map; 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, - SliceView.OnSliceActionListener, LifecycleObserver { - public static final int VIEW_TYPE = R.layout.homepage_slice_tile; +public class SliceContextualCardRenderer implements ContextualCardRenderer, LifecycleObserver { + public static final int VIEW_TYPE_FULL_WIDTH = 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"; @VisibleForTesting final Map> mSliceLiveDataMap; @VisibleForTesting - final Set mFlippedCardSet; + final Set mFlippedCardSet; private final Context mContext; private final LifecycleOwner mLifecycleOwner; private final ControllerRendererPool mControllerRendererPool; private final Set 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, ControllerRendererPool controllerRendererPool) { @@ -79,21 +78,26 @@ public class SliceContextualCardRenderer implements ContextualCardRenderer, mCardSet = new ArraySet<>(); mFlippedCardSet = new ArraySet<>(); mLifecycleOwner.getLifecycle().addObserver(this); + mFullCardHelper = new SliceFullCardRendererHelper(context); + mHalfCardHelper = new SliceHalfCardRendererHelper(context); } @Override public int getViewType(boolean isHalfWidth) { - return VIEW_TYPE; + mIsHalfWidth = isHalfWidth; + return isHalfWidth? VIEW_TYPE_HALF_WIDTH : VIEW_TYPE_FULL_WIDTH; } @Override public RecyclerView.ViewHolder createViewHolder(View view) { - return new SliceViewHolder(view); + if (mIsHalfWidth) { + return mHalfCardHelper.createViewHolder(view); + } + return mFullCardHelper.createViewHolder(view); } @Override public void bindView(RecyclerView.ViewHolder holder, ContextualCard card) { - final SliceViewHolder cardHolder = (SliceViewHolder) holder; final Uri uri = card.getSliceUri(); //TODO(b/120629936): Take this out once blank card issue is fixed. Log.d(TAG, "bindView - uri = " + uri); @@ -103,10 +107,6 @@ public class SliceContextualCardRenderer implements ContextualCardRenderer, 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 sliceLiveData = mSliceLiveDataMap.get(uri); if (sliceLiveData == null) { @@ -125,82 +125,58 @@ public class SliceContextualCardRenderer implements ContextualCardRenderer, //TODO(b/120629936): Take this out once blank card issue is fixed. 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 - cardHolder.sliceView.setOnSliceActionListener(this); - - // Customize slice view for Settings - cardHolder.sliceView.showTitleItems(true); - if (card.isLargeCard()) { - cardHolder.sliceView.showHeaderDivider(true); - cardHolder.sliceView.showActionDividers(true); + if (holder.getItemViewType() == VIEW_TYPE_HALF_WIDTH) { + initDismissalActions(holder, card, R.id.content); + } else { + initDismissalActions(holder, card, R.id.slice_view); } - - initDismissalActions(cardHolder, card); } - private void initDismissalActions(SliceViewHolder cardHolder, ContextualCard card) { - cardHolder.sliceView.setOnLongClickListener(v -> { - cardHolder.viewFlipper.showNext(); - mFlippedCardSet.add(cardHolder); + private void initDismissalActions(RecyclerView.ViewHolder holder, ContextualCard card, + int initialViewId) { + // initialView is the first view in the ViewFlipper. + final View initialView = holder.itemView.findViewById(initialViewId); + initialView.setOnLongClickListener(v -> { + flipCardToDismissalView(holder); + mFlippedCardSet.add(holder); return true; }); - final Button btnKeep = cardHolder.itemView.findViewById(R.id.keep); + final Button btnKeep = holder.itemView.findViewById(R.id.keep); btnKeep.setOnClickListener(v -> { - cardHolder.resetCard(); - mFlippedCardSet.remove(cardHolder); + mFlippedCardSet.remove(holder); + resetCardView(holder); }); - final Button btnRemove = cardHolder.itemView.findViewById(R.id.remove); + final Button btnRemove = holder.itemView.findViewById(R.id.remove); btnRemove.setOnClickListener(v -> { mControllerRendererPool.getController(mContext, card.getCardType()).onDismissed(card); - cardHolder.resetCard(); - mFlippedCardSet.remove(cardHolder); + mFlippedCardSet.remove(holder); + resetCardView(holder); 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) public void onStop() { - mFlippedCardSet.stream().forEach(holder -> holder.resetCard()); + mFlippedCardSet.stream().forEach(holder -> resetCardView(holder)); mFlippedCardSet.clear(); } - public static class SliceViewHolder extends RecyclerView.ViewHolder { - public final SliceView sliceView; - public final ViewFlipper viewFlipper; + private void resetCardView(RecyclerView.ViewHolder holder) { + final ViewFlipper viewFlipper = holder.itemView.findViewById(R.id.view_flipper); + viewFlipper.setDisplayedChild(0 /* whichChild */); + } - public SliceViewHolder(View view) { - super(view); - sliceView = view.findViewById(R.id.slice_view); - viewFlipper = view.findViewById(R.id.view_flipper); - } - - public void resetCard() { - viewFlipper.setDisplayedChild(0); - } + private void flipCardToDismissalView(RecyclerView.ViewHolder holder) { + final ViewFlipper viewFlipper = holder.itemView.findViewById(R.id.view_flipper); + viewFlipper.showNext(); } } diff --git a/src/com/android/settings/homepage/contextualcards/slices/SliceFullCardRendererHelper.java b/src/com/android/settings/homepage/contextualcards/slices/SliceFullCardRendererHelper.java new file mode 100644 index 00000000000..ef0a67d0e20 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/slices/SliceFullCardRendererHelper.java @@ -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 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 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); + } + } +} diff --git a/src/com/android/settings/homepage/contextualcards/slices/SliceHalfCardRendererHelper.java b/src/com/android/settings/homepage/contextualcards/slices/SliceHalfCardRendererHelper.java new file mode 100644 index 00000000000..2986dbc8fe7 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/slices/SliceHalfCardRendererHelper.java @@ -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) { + + } +} 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 0b87525b6bd..4d9a21db9a6 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 @@ -78,17 +78,6 @@ public class SliceContextualCardRendererTest { 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 public void bindView_invalidScheme_sliceShouldBeNull() { final Uri sliceUri = Uri.parse("contet://com.android.settings.slices/action/flashlight"); @@ -97,7 +86,7 @@ public class SliceContextualCardRendererTest { mRenderer.bindView(viewHolder, buildContextualCard(sliceUri)); assertThat( - ((SliceContextualCardRenderer.SliceViewHolder) viewHolder).sliceView.getSlice()) + ((SliceFullCardRendererHelper.SliceViewHolder) viewHolder).sliceView.getSlice()) .isNull(); } diff --git a/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/SliceFullCardRendererHelperTest.java b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/SliceFullCardRendererHelperTest.java new file mode 100644 index 00000000000..9172300df44 --- /dev/null +++ b/tests/robotests/src/com/android/settings/homepage/contextualcards/slices/SliceFullCardRendererHelperTest.java @@ -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(); + } +}