Allow remote views to be used in Tiles

If a suggeested action Tile has a remote view defined, inflate a card
layout to contain the remote view, instead of showing the default
title-summary layout.

Additionally, if the remote view has a view with ID
@android:id/primary, that will be used as the click target rather
than the entire card.

Test: cd tests/robotests && mma
Bug: 35668836
Change-Id: I0fd1b9c637b6d7a3d7d2f6268669917408a882eb
This commit is contained in:
Maurice Lam
2017-03-23 21:23:09 -07:00
parent a2218a938f
commit fc972a75a9
8 changed files with 192 additions and 29 deletions

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2016 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.
-->
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?android:attr/colorControlHighlight">
<item android:drawable="?android:attr/colorBackground"/>
</ripple>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 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.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="@drawable/selectable_card"
android:clickable="true"
android:elevation="2dp"
android:focusable="true"
android:minHeight="@dimen/dashboard_tile_minimum_height" />

View File

@@ -237,6 +237,7 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
mDashboardData.getItemEntityByPosition(position));
break;
case R.layout.suggestion_tile:
case R.layout.suggestion_tile_card:
final Tile suggestion = (Tile) mDashboardData.getItemEntityByPosition(position);
final String suggestionId = mSuggestionFeatureProvider.getSuggestionIdentifier(
mContext, suggestion);
@@ -247,7 +248,16 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
mSuggestionsShownLogged.add(suggestionId);
}
onBindTile(holder, suggestion);
holder.itemView.setOnClickListener(v -> {
View clickHandler = holder.itemView;
// If a view with @android:id/primary is defined, use that as the click handler
// instead.
final View primaryAction = holder.itemView.findViewById(android.R.id.primary);
if (primaryAction != null) {
clickHandler = primaryAction;
// set the item view to disabled to remove any touch effects
holder.itemView.setEnabled(false);
}
clickHandler.setOnClickListener(v -> {
mMetricsFeatureProvider.action(mContext,
MetricsEvent.ACTION_SETTINGS_SUGGESTION, suggestionId);
((SettingsActivity) mContext).startSuggestion(suggestion.intent);
@@ -405,13 +415,18 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
}
private void onBindTile(DashboardItemHolder holder, Tile tile) {
holder.icon.setImageDrawable(mCache.getIcon(tile.icon));
holder.title.setText(tile.title);
if (!TextUtils.isEmpty(tile.summary)) {
holder.summary.setText(tile.summary);
holder.summary.setVisibility(View.VISIBLE);
if (tile.remoteViews != null) {
final ViewGroup itemView = (ViewGroup) holder.itemView;
itemView.addView(tile.remoteViews.apply(itemView.getContext(), itemView));
} else {
holder.summary.setVisibility(View.GONE);
holder.icon.setImageDrawable(mCache.getIcon(tile.icon));
holder.title.setText(tile.title);
if (!TextUtils.isEmpty(tile.summary)) {
holder.summary.setText(tile.summary);
holder.summary.setVisibility(View.VISIBLE);
} else {
holder.summary.setVisibility(View.GONE);
}
}
}

View File

@@ -218,7 +218,13 @@ public class DashboardData {
*/
private void countSuggestion(Tile tile, boolean add) {
if (add) {
mItems.add(new Item(tile, R.layout.suggestion_tile, Objects.hash(tile.title), false));
mItems.add(new Item(
tile,
tile.remoteViews != null
? R.layout.suggestion_tile_card
: R.layout.suggestion_tile,
Objects.hash(tile.title),
false));
}
mId++;
}

View File

@@ -65,7 +65,9 @@ public class SuggestionDismissController extends ItemTouchHelper.SimpleCallback
@Override
public int getSwipeDirs(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
if (viewHolder.getItemViewType() == R.layout.suggestion_tile) {
final int layoutId = viewHolder.getItemViewType();
if (layoutId == R.layout.suggestion_tile
|| layoutId == R.layout.suggestion_tile_card) {
// Only return swipe direction for suggestion tiles. All other types are not swipeable.
return super.getSwipeDirs(recyclerView, viewHolder);
}

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2017 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.
-->
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?android:attr/colorControlHighlight">
<!-- Overlay this since Robolectric has trouble inflating ?android:attr/colorBackground -->
<item android:drawable="@android:color/transparent"/>
</ripple>

View File

@@ -15,14 +15,32 @@
*/
package com.android.settings.dashboard;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.RuntimeEnvironment.application;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.view.ContextThemeWrapper;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.RemoteViews;
import android.widget.TextView;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.SettingsRobolectricTestRunner;
import com.android.settings.TestConfig;
import com.android.settings.dashboard.conditional.Condition;
@@ -39,18 +57,13 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowApplication;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH,
sdk = TestConfig.SDK_VERSION,
@@ -62,7 +75,7 @@ import static org.mockito.Mockito.when;
public class DashboardAdapterTest {
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private Context mContext;
private SettingsActivity mContext;
@Mock
private View mView;
@Mock
@@ -109,7 +122,7 @@ public class DashboardAdapterTest {
@Test
public void testSuggestionsLogs_NotExpanded() {
setUpSuggestions(makeSuggestions(new String[]{"pkg1", "pkg2", "pkg3"}));
setUpSuggestions(makeSuggestions("pkg1", "pkg2", "pkg3"));
verify(mFactory.metricsFeatureProvider, times(2)).action(
any(Context.class), mActionCategoryCaptor.capture(),
mActionPackageCaptor.capture());
@@ -124,7 +137,7 @@ public class DashboardAdapterTest {
@Test
public void testSuggestionsLogs_NotExpandedAndPaused() {
setUpSuggestions(makeSuggestions(new String[]{"pkg1", "pkg2", "pkg3"}));
setUpSuggestions(makeSuggestions("pkg1", "pkg2", "pkg3"));
mDashboardAdapter.onPause();
verify(mFactory.metricsFeatureProvider, times(4)).action(
any(Context.class), mActionCategoryCaptor.capture(),
@@ -141,7 +154,7 @@ public class DashboardAdapterTest {
@Test
public void testSuggestionsLogs_Expanded() {
setUpSuggestions(makeSuggestions(new String[]{"pkg1", "pkg2", "pkg3"}));
setUpSuggestions(makeSuggestions("pkg1", "pkg2", "pkg3"));
mDashboardAdapter.onBindSuggestionHeader(
mSuggestionHolder, mSuggestionHeaderData);
mSuggestionHolder.itemView.callOnClick();
@@ -160,7 +173,7 @@ public class DashboardAdapterTest {
@Test
public void testSuggestionsLogs_ExpandedAndPaused() {
setUpSuggestions(makeSuggestions(new String[]{"pkg1", "pkg2", "pkg3"}));
setUpSuggestions(makeSuggestions("pkg1", "pkg2", "pkg3"));
mDashboardAdapter.onBindSuggestionHeader(
mSuggestionHolder, mSuggestionHeaderData);
mSuggestionHolder.itemView.callOnClick();
@@ -183,7 +196,7 @@ public class DashboardAdapterTest {
@Test
public void testSuggestionsLogs_ExpandedAfterPause() {
setUpSuggestions(makeSuggestions(new String[]{"pkg1", "pkg2", "pkg3"}));
setUpSuggestions(makeSuggestions("pkg1", "pkg2", "pkg3"));
mDashboardAdapter.onPause();
mDashboardAdapter.onBindSuggestionHeader(
mSuggestionHolder, mSuggestionHeaderData);
@@ -208,7 +221,7 @@ public class DashboardAdapterTest {
@Test
public void testSuggestionsLogs_ExpandedAfterPauseAndPausedAgain() {
setUpSuggestions(makeSuggestions(new String[]{"pkg1", "pkg2", "pkg3"}));
setUpSuggestions(makeSuggestions("pkg1", "pkg2", "pkg3"));
mDashboardAdapter.onPause();
mDashboardAdapter.onBindSuggestionHeader(
mSuggestionHolder, mSuggestionHeaderData);
@@ -237,7 +250,7 @@ public class DashboardAdapterTest {
@Test
public void testSuggestionsLogs_ExpandedWithLessThanDefaultShown() {
setUpSuggestions(makeSuggestions(new String[]{"pkg1"}));
setUpSuggestions(makeSuggestions("pkg1"));
mDashboardAdapter.onBindSuggestionHeader(
mSuggestionHolder, mSuggestionHeaderData);
mSuggestionHolder.itemView.callOnClick();
@@ -254,7 +267,7 @@ public class DashboardAdapterTest {
@Test
public void testSuggestionsLogs_ExpandedWithLessThanDefaultShownAndPaused() {
setUpSuggestions(makeSuggestions(new String[]{"pkg1"}));
setUpSuggestions(makeSuggestions("pkg1"));
mDashboardAdapter.onBindSuggestionHeader(
mSuggestionHolder, mSuggestionHeaderData);
mSuggestionHolder.itemView.callOnClick();
@@ -273,7 +286,7 @@ public class DashboardAdapterTest {
@Test
public void testSuggestionsLogs_ExpandedWithLessThanDefaultShownAfterPause() {
setUpSuggestions(makeSuggestions(new String[]{"pkg1"}));
setUpSuggestions(makeSuggestions("pkg1"));
mDashboardAdapter.onPause();
mDashboardAdapter.onBindSuggestionHeader(
mSuggestionHolder, mSuggestionHeaderData);
@@ -293,7 +306,7 @@ public class DashboardAdapterTest {
@Test
public void testSuggestionsLogs_ExpandedWithLessThanDefaultShownAfterPauseAndPausedAgain() {
setUpSuggestions(makeSuggestions(new String[]{"pkg1"}));
setUpSuggestions(makeSuggestions("pkg1"));
mDashboardAdapter.onPause();
mDashboardAdapter.onBindSuggestionHeader(
mSuggestionHolder, mSuggestionHeaderData);
@@ -313,7 +326,54 @@ public class DashboardAdapterTest {
assertThat(mActionCategoryCaptor.getAllValues().toArray()).isEqualTo(expectedActions);
}
private List<Tile> makeSuggestions(String[] pkgNames) {
@Test
public void testBindViewHolder_inflateRemoteView() {
List<Tile> packages = makeSuggestions("pkg1");
RemoteViews remoteViews = mock(RemoteViews.class);
TextView textView = new TextView(application);
doReturn(textView).when(remoteViews).apply(any(Context.class), any(ViewGroup.class));
packages.get(0).remoteViews = remoteViews;
mDashboardAdapter.setCategoriesAndSuggestions(Collections.emptyList(), packages);
mSuggestionHolder = mDashboardAdapter.onCreateViewHolder(
new FrameLayout(application),
R.layout.suggestion_tile_card);
mDashboardAdapter.onBindViewHolder(mSuggestionHolder, 1);
assertThat(textView.getParent()).isSameAs(mSuggestionHolder.itemView);
mSuggestionHolder.itemView.performClick();
verify(mContext).startSuggestion(any(Intent.class));
}
@Test
public void testBindViewHolder_primaryViewHandlesClick() {
Context context = new ContextThemeWrapper(application, R.style.Theme_Settings);
List<Tile> packages = makeSuggestions("pkg1");
RemoteViews remoteViews = mock(RemoteViews.class);
FrameLayout layout = new FrameLayout(context);
Button primary = new Button(context);
primary.setId(android.R.id.primary);
layout.addView(primary);
doReturn(layout).when(remoteViews).apply(any(Context.class), any(ViewGroup.class));
packages.get(0).remoteViews = remoteViews;
mDashboardAdapter.setCategoriesAndSuggestions(Collections.emptyList(), packages);
mSuggestionHolder = mDashboardAdapter.onCreateViewHolder(
new FrameLayout(context),
R.layout.suggestion_tile_card);
mDashboardAdapter.onBindViewHolder(mSuggestionHolder, 1);
mSuggestionHolder.itemView.performClick();
assertThat(ShadowApplication.getInstance().getNextStartedActivity()).isNull();
verify(mContext, never()).startSuggestion(any(Intent.class));
primary.performClick();
verify(mContext).startSuggestion(any(Intent.class));
}
private List<Tile> makeSuggestions(String... pkgNames) {
final List<Tile> suggestions = new ArrayList<>();
for (String pkgName : pkgNames) {
Tile suggestion = new Tile();
@@ -327,7 +387,7 @@ public class DashboardAdapterTest {
private void setUpSuggestions(List<Tile> suggestions) {
mDashboardAdapter.setCategoriesAndSuggestions(new ArrayList<>(), suggestions);
mSuggestionHolder = mDashboardAdapter.onCreateViewHolder(
new FrameLayout(RuntimeEnvironment.application),
new FrameLayout(application),
mDashboardAdapter.getItemViewType(0));
}

View File

@@ -85,6 +85,15 @@ public class SuggestionDismissControllerTest {
.isEqualTo(ItemTouchHelper.START | ItemTouchHelper.END);
}
@Test
public void getSwipeDirs_isSuggestionTileCard_shouldReturnDirection() {
final RecyclerView.ViewHolder vh = mock(RecyclerView.ViewHolder.class);
when(vh.getItemViewType()).thenReturn(R.layout.suggestion_tile_card);
assertThat(mController.getSwipeDirs(mRecyclerView, vh))
.isEqualTo(ItemTouchHelper.START | ItemTouchHelper.END);
}
@Test
public void getSwipeDirs_isNotSuggestionTile_shouldReturn0() {
final RecyclerView.ViewHolder vh = mock(RecyclerView.ViewHolder.class);