Add a controller and renderer for Slices.

Many contextual cards will be built as slices, so we need a controller
and a renderer for them.

Change-Id: I3816db09ba0181399810652fb18fbe11ce273267
Fixes: 115709730
Test: robotests
This commit is contained in:
Emily Chuang
2018-09-14 18:26:37 +08:00
parent 8685c8562e
commit 0757e49260
10 changed files with 347 additions and 8 deletions

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/homepage_card_top_margin"
android:layout_marginBottom="@dimen/homepage_card_bottom_margin"
android:layout_marginStart="@dimen/homepage_card_side_margin"
android:layout_marginEnd="@dimen/homepage_card_side_margin"
app:cardCornerRadius="@dimen/homepage_card_corner_radius"
app:cardElevation="@dimen/homepage_card_elevation">
<androidx.slice.widget.SliceView
android:id="@+id/slice_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/homepage_card_padding_start"
android:paddingEnd="@dimen/homepage_card_padding_end"
android:paddingTop="@dimen/homepage_card_padding_top"
android:paddingBottom="@dimen/homepage_card_padding_bottom"/>
</androidx.cardview.widget.CardView>

View File

@@ -338,4 +338,15 @@
<dimen name="homepage_bottombar_top_margin">34dp</dimen>
<dimen name="homepage_bottombar_fab_cradle">68dp</dimen>
<!-- Homepage cards size and padding -->
<dimen name="homepage_card_corner_radius">8dp</dimen>
<dimen name="homepage_card_elevation">2dp</dimen>
<dimen name="homepage_card_top_margin">6dp</dimen>
<dimen name="homepage_card_bottom_margin">6dp</dimen>
<dimen name="homepage_card_side_margin">16dp</dimen>
<dimen name="homepage_card_padding_start">16dp</dimen>
<dimen name="homepage_card_padding_end">16dp</dimen>
<dimen name="homepage_card_padding_top">6dp</dimen>
<dimen name="homepage_card_padding_bottom">6dp</dimen>
</resources>

View File

@@ -19,6 +19,8 @@ package com.android.settings.homepage;
import com.android.settings.homepage.ContextualCard.CardType;
import com.android.settings.homepage.conditional.ConditionContextualCardController;
import com.android.settings.homepage.conditional.ConditionContextualCardRenderer;
import com.android.settings.homepage.slices.SliceContextualCardController;
import com.android.settings.homepage.slices.SliceContextualCardRenderer;
import java.util.Set;
import java.util.TreeSet;
@@ -50,6 +52,9 @@ public class ContextualCardLookupTable {
add(new ControllerRendererMapping(CardType.CONDITIONAL,
ConditionContextualCardController.class,
ConditionContextualCardRenderer.class));
add(new ControllerRendererMapping(CardType.SLICE,
SliceContextualCardController.class,
SliceContextualCardRenderer.class));
}};
public static Class<? extends ContextualCardController> getCardControllerClass(

View File

@@ -21,6 +21,7 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.lifecycle.LifecycleOwner;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -39,11 +40,14 @@ public class ContextualCardsAdapter extends RecyclerView.Adapter<RecyclerView.Vi
private final Context mContext;
private final ControllerRendererPool mControllerRendererPool;
private final List<ContextualCard> mContextualCards;
private final LifecycleOwner mLifecycleOwner;
public ContextualCardsAdapter(Context context, ContextualCardManager manager) {
public ContextualCardsAdapter(Context context, LifecycleOwner lifecycleOwner,
ContextualCardManager manager) {
mContext = context;
mContextualCards = new ArrayList<>();
mControllerRendererPool = manager.getControllerRendererPool();
mLifecycleOwner = lifecycleOwner;
setHasStableIds(true);
}
@@ -60,7 +64,7 @@ public class ContextualCardsAdapter extends RecyclerView.Adapter<RecyclerView.Vi
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int cardType) {
final ContextualCardRenderer renderer = mControllerRendererPool.getRenderer(mContext,
cardType);
mLifecycleOwner, cardType);
final int viewType = renderer.getViewType();
final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
@@ -71,7 +75,7 @@ public class ContextualCardsAdapter extends RecyclerView.Adapter<RecyclerView.Vi
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
final int cardType = mContextualCards.get(position).getCardType();
final ContextualCardRenderer renderer = mControllerRendererPool.getRenderer(mContext,
cardType);
mLifecycleOwner, cardType);
renderer.bindView(holder, mContextualCards.get(position));
}

View File

@@ -20,9 +20,12 @@ import android.content.Context;
import android.util.Log;
import androidx.collection.ArraySet;
import androidx.lifecycle.LifecycleOwner;
import com.android.settings.homepage.conditional.ConditionContextualCardController;
import com.android.settings.homepage.conditional.ConditionContextualCardRenderer;
import com.android.settings.homepage.slices.SliceContextualCardController;
import com.android.settings.homepage.slices.SliceContextualCardRenderer;
import java.util.Set;
@@ -64,7 +67,8 @@ public class ControllerRendererPool {
return mControllers;
}
public ContextualCardRenderer getRenderer(Context context, @ContextualCard.CardType int cardType) {
public ContextualCardRenderer getRenderer(Context context, LifecycleOwner lifecycleOwner,
@ContextualCard.CardType int cardType) {
final Class<? extends ContextualCardRenderer> clz =
ContextualCardLookupTable.getCardRendererClasses(cardType);
for (ContextualCardRenderer renderer : mRenderers) {
@@ -74,7 +78,7 @@ public class ControllerRendererPool {
}
}
final ContextualCardRenderer renderer = createCardRenderer(context, clz);
final ContextualCardRenderer renderer = createCardRenderer(context, lifecycleOwner, clz);
if (renderer != null) {
mRenderers.add(renderer);
}
@@ -85,15 +89,19 @@ public class ControllerRendererPool {
Class<? extends ContextualCardController> clz) {
if (ConditionContextualCardController.class == clz) {
return new ConditionContextualCardController(context);
} else if (SliceContextualCardController.class == clz) {
return new SliceContextualCardController();
}
return null;
}
private ContextualCardRenderer createCardRenderer(Context context, Class<?> clz) {
private ContextualCardRenderer createCardRenderer(Context context,
LifecycleOwner lifecycleOwner, Class<?> clz) {
if (ConditionContextualCardRenderer.class == clz) {
return new ConditionContextualCardRenderer(context, this /*controllerRendererPool*/);
} else if (SliceContextualCardRenderer.class == clz) {
return new SliceContextualCardRenderer(context, lifecycleOwner);
}
return null;
}
}

View File

@@ -54,7 +54,8 @@ public class PersonalSettingsFragment extends InstrumentedFragment {
mLayoutManager = new GridLayoutManager(getActivity(), SPAN_COUNT,
GridLayoutManager.VERTICAL, false /* reverseLayout */);
mCardsContainer.setLayoutManager(mLayoutManager);
mContextualCardsAdapter = new ContextualCardsAdapter(getContext(), mContextualCardManager);
mContextualCardsAdapter = new ContextualCardsAdapter(getContext(),
this /* lifecycleOwner */, mContextualCardManager);
mCardsContainer.setAdapter(mContextualCardsAdapter);
mContextualCardManager.setListener(mContextualCardsAdapter);

View File

@@ -0,0 +1,49 @@
/*
* 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.slices;
import com.android.settings.homepage.ContextualCard;
import com.android.settings.homepage.ContextualCardController;
import com.android.settings.homepage.ContextualCardUpdateListener;
import java.util.List;
/**
* Card controller for {@link ContextualCard} built as slices.
*/
public class SliceContextualCardController implements ContextualCardController {
@Override
public int getCardType() {
return ContextualCard.CardType.SLICE;
}
@Override
public void onPrimaryClick(ContextualCard card) {
}
@Override
public void onActionClick(ContextualCard card) {
}
@Override
public void setCardUpdateListener(ContextualCardUpdateListener listener) {
}
}

View File

@@ -0,0 +1,118 @@
/*
* 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.slices;
import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
import android.util.ArrayMap;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
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.ContextualCard;
import com.android.settings.homepage.ContextualCardRenderer;
import java.util.Map;
/**
* Card renderer for {@link ContextualCard} built as slices.
*/
public class SliceContextualCardRenderer implements ContextualCardRenderer,
SliceView.OnSliceActionListener {
private static final String TAG = "SliceCardRenderer";
@VisibleForTesting
final Map<String, LiveData<Slice>> mSliceLiveDataMap;
private final Context mContext;
private final LifecycleOwner mLifecycleOwner;
public SliceContextualCardRenderer(Context context, LifecycleOwner lifecycleOwner) {
mContext = context;
mLifecycleOwner = lifecycleOwner;
mSliceLiveDataMap = new ArrayMap<>();
}
@Override
public int getViewType() {
return R.layout.homepage_slice_tile;
}
@Override
public RecyclerView.ViewHolder createViewHolder(View view) {
return new SliceViewHolder(view);
}
@Override
public void bindView(RecyclerView.ViewHolder holder, ContextualCard card) {
final SliceViewHolder cardHolder = (SliceViewHolder) holder;
final Uri uri = card.getSliceUri();
//TODO(b/116063073): The URI check should be done earlier when we are performing final
// filtering after having the full list.
if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) {
Log.w(TAG, "Invalid uri, skipping slice: " + uri);
return;
}
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_SHORTCUT);
LiveData<Slice> sliceLiveData = mSliceLiveDataMap.get(uri.toString());
if (sliceLiveData == null) {
sliceLiveData = SliceLiveData.fromUri(mContext, uri);
mSliceLiveDataMap.put(uri.toString(), sliceLiveData);
sliceLiveData.observe(mLifecycleOwner, slice -> {
if (slice == null) {
Log.w(TAG, "Slice is null");
}
cardHolder.sliceView.setSlice(slice);
});
}
// Set this listener so we can log the interaction users make on the slice
cardHolder.sliceView.setOnSliceActionListener(this);
}
@Override
public void onSliceAction(@NonNull EventInfo eventInfo, @NonNull SliceItem sliceItem) {
//TODO(b/79698338): Log user interaction
}
public 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,102 @@
/*
* 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.slices;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.spy;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import androidx.lifecycle.LifecycleOwner;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.android.settings.homepage.ContextualCard;
import com.android.settings.homepage.PersonalSettingsFragment;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;
import org.robolectric.RuntimeEnvironment;
@RunWith(SettingsRobolectricTestRunner.class)
public class SliceContextualCardRendererTest {
private Context mContext;
private SliceContextualCardRenderer mRenderer;
private LifecycleOwner mLifecycleOwner;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application;
mLifecycleOwner = new PersonalSettingsFragment();
mRenderer = new SliceContextualCardRenderer(mContext, mLifecycleOwner);
}
@Test
public void bindView_invalidScheme_sliceShouldBeNull() {
final String sliceUri = "contet://com.android.settings.slices/action/flashlight";
RecyclerView.ViewHolder viewHolder = getSliceViewHolder();
mRenderer.bindView(viewHolder, buildContextualCard(sliceUri));
assertThat(
((SliceContextualCardRenderer.SliceViewHolder) viewHolder).sliceView.getSlice())
.isNull();
}
@Test
public void bindView_newSliceLiveData_shouldAddDataToMap() {
final String sliceUri = "content://com.android.settings.slices/action/flashlight";
mRenderer.bindView(getSliceViewHolder(), buildContextualCard(sliceUri));
assertThat(mRenderer.mSliceLiveDataMap.size()).isEqualTo(1);
}
@Test
public void bindView_sliceLiveDataShouldObserveSliceView() {
final String sliceUri = "content://com.android.settings.slices/action/flashlight";
mRenderer.bindView(getSliceViewHolder(), buildContextualCard(sliceUri));
assertThat(mRenderer.mSliceLiveDataMap.get(sliceUri).hasObservers()).isTrue();
}
private RecyclerView.ViewHolder getSliceViewHolder() {
final int viewType = mRenderer.getViewType();
final RecyclerView recyclerView = new RecyclerView(mContext);
recyclerView.setLayoutManager(new LinearLayoutManager(mContext));
final View view = LayoutInflater.from(mContext).inflate(viewType, recyclerView, false);
final RecyclerView.ViewHolder viewHolder = mRenderer.createViewHolder(view);
return viewHolder;
}
private ContextualCard buildContextualCard(String sliceUri) {
return new ContextualCard.Builder()
.setName("test_name")
.setSliceUri(sliceUri)
.build();
}
}

View File

@@ -91,6 +91,8 @@ public class SettingsRobolectricTestRunner extends RobolectricTestRunner {
Fs.fromURL(new URL("file:out/soong/.intermediates/prebuilts/sdk/current/extras/material-design-x/com.google.android.material_material-nodeps/android_common/aar/res/")), null));
paths.add(new ResourcePath(null,
Fs.fromURL(new URL("file:out/soong/.intermediates/prebuilts/sdk/current/androidx/androidx.cardview_cardview-nodeps/android_common/aar/res")), null));
paths.add(new ResourcePath(null,
Fs.fromURL(new URL("file:out/soong/.intermediates/prebuilts/sdk/current/androidx/androidx.slice_slice-view-nodeps/android_common/aar/res")), null));
} catch (MalformedURLException e) {
throw new RuntimeException("SettingsRobolectricTestRunner failure", e);
}