diff --git a/res/layout/homepage_slice_tile.xml b/res/layout/homepage_slice_tile.xml new file mode 100644 index 00000000000..b947d656097 --- /dev/null +++ b/res/layout/homepage_slice_tile.xml @@ -0,0 +1,39 @@ + + + + + + + + diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 70672ca3594..37b3c4b812b 100755 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -340,4 +340,15 @@ 34dp 68dp + + 8dp + 2dp + 6dp + 6dp + 16dp + 16dp + 16dp + 6dp + 6dp + diff --git a/src/com/android/settings/homepage/ContextualCardLookupTable.java b/src/com/android/settings/homepage/ContextualCardLookupTable.java index e70b700fea2..1acd812d57c 100644 --- a/src/com/android/settings/homepage/ContextualCardLookupTable.java +++ b/src/com/android/settings/homepage/ContextualCardLookupTable.java @@ -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 getCardControllerClass( diff --git a/src/com/android/settings/homepage/ContextualCardsAdapter.java b/src/com/android/settings/homepage/ContextualCardsAdapter.java index 9ab88936790..81cae39bde8 100644 --- a/src/com/android/settings/homepage/ContextualCardsAdapter.java +++ b/src/com/android/settings/homepage/ContextualCardsAdapter.java @@ -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 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 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 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; } - } diff --git a/src/com/android/settings/homepage/PersonalSettingsFragment.java b/src/com/android/settings/homepage/PersonalSettingsFragment.java index 4a0041ea19d..93c723b9522 100644 --- a/src/com/android/settings/homepage/PersonalSettingsFragment.java +++ b/src/com/android/settings/homepage/PersonalSettingsFragment.java @@ -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); diff --git a/src/com/android/settings/homepage/slices/SliceContextualCardController.java b/src/com/android/settings/homepage/slices/SliceContextualCardController.java new file mode 100644 index 00000000000..e6fc283d206 --- /dev/null +++ b/src/com/android/settings/homepage/slices/SliceContextualCardController.java @@ -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) { + + } +} diff --git a/src/com/android/settings/homepage/slices/SliceContextualCardRenderer.java b/src/com/android/settings/homepage/slices/SliceContextualCardRenderer.java new file mode 100644 index 00000000000..983b4aaec02 --- /dev/null +++ b/src/com/android/settings/homepage/slices/SliceContextualCardRenderer.java @@ -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> 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 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); + } + } +} diff --git a/tests/robotests/src/com/android/settings/homepage/slices/SliceContextualCardRendererTest.java b/tests/robotests/src/com/android/settings/homepage/slices/SliceContextualCardRendererTest.java new file mode 100644 index 00000000000..3239971d34f --- /dev/null +++ b/tests/robotests/src/com/android/settings/homepage/slices/SliceContextualCardRendererTest.java @@ -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(); + } +} diff --git a/tests/robotests/src/com/android/settings/testutils/SettingsRobolectricTestRunner.java b/tests/robotests/src/com/android/settings/testutils/SettingsRobolectricTestRunner.java index 14d5c75e4eb..2e3a140dfed 100644 --- a/tests/robotests/src/com/android/settings/testutils/SettingsRobolectricTestRunner.java +++ b/tests/robotests/src/com/android/settings/testutils/SettingsRobolectricTestRunner.java @@ -93,6 +93,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); }