Add conditional cards to the new homepage.

Make existing Settings Conditions show up in the new homepage as custom
view.

Bug: 113451905
Test: robotests
Change-Id: Ic49089f95ff04442a887cda97481e945136f7209
This commit is contained in:
Emily Chuang
2018-08-29 19:47:00 +08:00
parent c21f4a512a
commit 307d209322
12 changed files with 641 additions and 33 deletions

View File

@@ -0,0 +1,29 @@
<!--
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"
style="@style/SuggestionConditionStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardUseCompatPadding="true"
app:cardElevation="@dimen/condition_card_elevation"
app:cardCornerRadius="@dimen/suggestion_card_corner_radius">
<include layout="@layout/condition_tile"/>
</androidx.cardview.widget.CardView>

View File

@@ -312,6 +312,9 @@
<dimen name="suggestion_card_button_top_margin">16dp</dimen>
<dimen name="suggestion_card_button_bottom_margin">18dp</dimen>
<!-- Condition cards size and padding -->
<dimen name="condition_card_elevation">2dp</dimen>
<!-- Padding for the reset screens -->
<dimen name="reset_checkbox_padding_end">8dp</dimen>
<dimen name="reset_checkbox_title_padding_top">12dp</dimen>

View File

@@ -21,6 +21,9 @@ import android.util.Log;
import androidx.collection.ArraySet;
import com.android.settings.homepage.conditional.ConditionHomepageCardController;
import com.android.settings.homepage.conditional.ConditionHomepageCardRenderer;
import java.util.Set;
/**
@@ -80,19 +83,16 @@ public class ControllerRendererPool {
private HomepageCardController createCardController(Context context,
Class<? extends HomepageCardController> clz) {
/*
if (ConditionHomepageCardController.class == clz) {
return new ConditionHomepageCardController(context);
}
*/
return null;
}
private HomepageCardRenderer createCardRenderer(Context context, Class<?> clz) {
//if (ConditionHomepageCardRenderer.class == clz) {
// return new ConditionHomepageCardRenderer(context, this /*controllerRendererPool*/);
//}
if (ConditionHomepageCardRenderer.class == clz) {
return new ConditionHomepageCardRenderer(context, this /*controllerRendererPool*/);
}
return null;
}

View File

@@ -65,79 +65,79 @@ public class HomepageCard {
return mName;
}
int getCardType() {
public int getCardType() {
return mCardType;
}
double getRankingScore() {
public double getRankingScore() {
return mRankingScore;
}
String getTextSliceUri() {
public String getTextSliceUri() {
return mSliceUri;
}
Uri getSliceUri() {
public Uri getSliceUri() {
return Uri.parse(mSliceUri);
}
int getCategory() {
public int getCategory() {
return mCategory;
}
String getLocalizedToLocale() {
public String getLocalizedToLocale() {
return mLocalizedToLocale;
}
String getPackageName() {
public String getPackageName() {
return mPackageName;
}
String getAppVersion() {
public String getAppVersion() {
return mAppVersion;
}
String getTitleResName() {
public String getTitleResName() {
return mTitleResName;
}
String getTitleText() {
public String getTitleText() {
return mTitleText;
}
String getSummaryResName() {
public String getSummaryResName() {
return mSummaryResName;
}
String getSummaryText() {
public String getSummaryText() {
return mSummaryText;
}
String getIconResName() {
public String getIconResName() {
return mIconResName;
}
int getIconResId() {
public int getIconResId() {
return mIconResId;
}
int getCardAction() {
public int getCardAction() {
return mCardAction;
}
long getExpireTimeMS() {
public long getExpireTimeMS() {
return mExpireTimeMS;
}
Drawable getIconDrawable() {
public Drawable getIconDrawable() {
return mIconDrawable;
}
boolean isHalfWidth() {
public boolean isHalfWidth() {
return mIsHalfWidth;
}
HomepageCard(Builder builder) {
public HomepageCard(Builder builder) {
mName = builder.mName;
mCardType = builder.mCardType;
mRankingScore = builder.mRankingScore;
@@ -179,7 +179,7 @@ public class HomepageCard {
return TextUtils.equals(mName, that.mName);
}
static class Builder {
public static class Builder {
private String mName;
private int mCardType;
private double mRankingScore;

View File

@@ -18,8 +18,6 @@ package com.android.settings.homepage;
import java.util.List;
//TODO(b/111821137): add test cases
/**
* Data controller for {@link HomepageCard}.
*/

View File

@@ -17,6 +17,8 @@
package com.android.settings.homepage;
import com.android.settings.homepage.HomepageCard.CardType;
import com.android.settings.homepage.conditional.ConditionHomepageCardController;
import com.android.settings.homepage.conditional.ConditionHomepageCardRenderer;
import java.util.Set;
import java.util.TreeSet;
@@ -43,10 +45,10 @@ public class HomepageCardLookupTable {
}
}
private static final Set<HomepageMapping> LOOKUP_TABLE = new TreeSet<HomepageMapping>() {
//add(new HomepageMapping(CardType.CONDITIONAL, ConditionHomepageCardController.class,
// ConditionHomepageCardRenderer.class));
};
private static final Set<HomepageMapping> LOOKUP_TABLE = new TreeSet<HomepageMapping>() {{
add(new HomepageMapping(CardType.CONDITIONAL, ConditionHomepageCardController.class,
ConditionHomepageCardRenderer.class));
}};
public static Class<? extends HomepageCardController> getCardControllerClass(
@CardType int cardType) {

View File

@@ -26,6 +26,6 @@ import java.util.List;
* {@link HomepageManager} will notify the listeners registered, {@link HomepageAdapter} in this
* case.
*/
interface HomepageCardUpdateListener {
public interface HomepageCardUpdateListener {
void onHomepageCardUpdated(int cardType, List<HomepageCard> updateList);
}

View File

@@ -0,0 +1,78 @@
/*
* 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.conditional;
import com.android.settings.homepage.HomepageCard;
/**
* Data class representing a {@link ConditionCard}.
*
* Use this class to store additional attributes on top of {@link HomepageCard} for
* {@link ConditionalCard}.
*/
public class ConditionCard extends HomepageCard {
private final long mConditionId;
private final int mMetricsConstant;
private final CharSequence mActionText;
private ConditionCard(Builder builder) {
super(builder);
mConditionId = builder.mConditionId;
mMetricsConstant = builder.mMetricsConstant;
mActionText = builder.mActionText;
}
public long getConditionId() {
return mConditionId;
}
public int getMetricsConstant() {
return mMetricsConstant;
}
public CharSequence getActionText() {
return mActionText;
}
static class Builder extends HomepageCard.Builder {
private long mConditionId;
private int mMetricsConstant;
private CharSequence mActionText;
public Builder setConditionId(long id) {
mConditionId = id;
return this;
}
public Builder setMetricsConstant(int metricsConstant) {
mMetricsConstant = metricsConstant;
return this;
}
public Builder setActionText(CharSequence actionText) {
mActionText = actionText;
return this;
}
public ConditionCard build() {
return new ConditionCard(this);
}
}
}

View File

@@ -0,0 +1,111 @@
/*
* 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.conditional;
import android.content.Context;
import com.android.settings.homepage.HomepageCard;
import com.android.settings.homepage.HomepageCardController;
import com.android.settings.homepage.HomepageCardUpdateListener;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnStart;
import com.android.settingslib.core.lifecycle.events.OnStop;
import java.util.ArrayList;
import java.util.List;
/**
* This controller triggers the loading of conditional cards and monitors state changes to
* update the homepage.
*/
public class ConditionHomepageCardController implements HomepageCardController, ConditionListener,
LifecycleObserver, OnStart, OnStop {
private final Context mContext;
private final ConditionManager mConditionManager;
private HomepageCardUpdateListener mListener;
public ConditionHomepageCardController(Context context) {
mContext = context;
mConditionManager = new ConditionManager(context.getApplicationContext(), this);
mConditionManager.startMonitoringStateChange();
}
@Override
public void setHomepageCardUpdateListener(HomepageCardUpdateListener listener) {
mListener = listener;
}
@Override
public int getCardType() {
return HomepageCard.CardType.CONDITIONAL;
}
@Override
public void onDataUpdated(List<HomepageCard> cardList) {
mListener.onHomepageCardUpdated(getCardType(), cardList);
}
@Override
public void onStart() {
mConditionManager.startMonitoringStateChange();
}
@Override
public void onStop() {
mConditionManager.stopMonitoringStateChange();
}
@Override
public void onPrimaryClick(HomepageCard homepageCard) {
final ConditionCard card = (ConditionCard) homepageCard;
mConditionManager.onPrimaryClick(mContext, card.getConditionId());
}
@Override
public void onActionClick(HomepageCard homepageCard) {
final ConditionCard card = (ConditionCard) homepageCard;
mConditionManager.onActionClick(card.getConditionId());
}
@Override
public void onConditionsChanged() {
final List<HomepageCard> conditionCards = new ArrayList<>();
final List<ConditionalCard> conditionList = mConditionManager.getDisplayableCards();
for (ConditionalCard condition : conditionList) {
final ConditionCard conditionCard = ((ConditionCard.Builder) new ConditionCard.Builder()
.setConditionId(condition.getId())
.setMetricsConstant(condition.getMetricsConstant())
.setActionText(condition.getActionText())
.setName(mContext.getPackageName() + "/" + condition.getTitle().toString())
.setCardType(HomepageCard.CardType.CONDITIONAL)
.setTitleText(condition.getTitle().toString())
.setSummaryText(condition.getSummary().toString())
.setIconDrawable(condition.getIcon()))
.build();
conditionCards.add(conditionCard);
}
if (mListener != null) {
onDataUpdated(conditionCards);
}
}
}

View File

@@ -0,0 +1,137 @@
/*
* 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.conditional;
import android.content.Context;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.android.internal.logging.nano.MetricsProto;
import com.android.settings.R;
import com.android.settings.homepage.ControllerRendererPool;
import com.android.settings.homepage.HomepageCard;
import com.android.settings.homepage.HomepageCardRenderer;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
/**
* Card renderer for {@link ConditionCard}.
*/
public class ConditionHomepageCardRenderer implements HomepageCardRenderer {
private final Context mContext;
private final ControllerRendererPool mControllerRendererPool;
public ConditionHomepageCardRenderer(Context context,
ControllerRendererPool controllerRendererPool) {
mContext = context;
mControllerRendererPool = controllerRendererPool;
}
@Override
public int getViewType() {
return R.layout.homepage_condition_tile;
}
@Override
public RecyclerView.ViewHolder createViewHolder(View view) {
return new ConditionalCardHolder(view);
}
@Override
public void bindView(RecyclerView.ViewHolder holder, HomepageCard homepageCard) {
final ConditionalCardHolder view = (ConditionalCardHolder) holder;
final ConditionCard card = (ConditionCard) homepageCard;
final MetricsFeatureProvider metricsFeatureProvider = FeatureFactory.getFactory(
mContext).getMetricsFeatureProvider();
metricsFeatureProvider.visible(mContext, MetricsProto.MetricsEvent.SETTINGS_HOMEPAGE,
card.getMetricsConstant());
initializePrimaryClick(view, card, metricsFeatureProvider);
initializeView(view, card);
initializeActionButton(view, card, metricsFeatureProvider);
}
private void initializePrimaryClick(ConditionalCardHolder view, ConditionCard card,
MetricsFeatureProvider metricsFeatureProvider) {
view.itemView.findViewById(R.id.content).setOnClickListener(
v -> {
metricsFeatureProvider.action(mContext,
MetricsProto.MetricsEvent.ACTION_SETTINGS_CONDITION_CLICK,
card.getMetricsConstant());
mControllerRendererPool.getController(mContext,
card.getCardType()).onPrimaryClick(card);
});
}
private void initializeView(ConditionalCardHolder view, ConditionCard card) {
view.icon.setImageDrawable(card.getIconDrawable());
view.title.setText(card.getTitleText());
view.summary.setText(card.getSummaryText());
setViewVisibility(view.itemView, R.id.divider, false);
}
private void initializeActionButton(ConditionalCardHolder view, ConditionCard card,
MetricsFeatureProvider metricsFeatureProvider) {
final CharSequence action = card.getActionText();
final boolean hasButtons = !TextUtils.isEmpty(action);
setViewVisibility(view.itemView, R.id.buttonBar, hasButtons);
final Button button = view.itemView.findViewById(R.id.first_action);
if (hasButtons) {
button.setVisibility(View.VISIBLE);
button.setText(action);
button.setOnClickListener(v -> {
final Context viewContext = v.getContext();
metricsFeatureProvider.action(
viewContext, MetricsProto.MetricsEvent.ACTION_SETTINGS_CONDITION_BUTTON,
card.getMetricsConstant());
mControllerRendererPool.getController(mContext, card.getCardType()).onActionClick(
card);
});
} else {
button.setVisibility(View.GONE);
}
}
private void setViewVisibility(View containerView, int viewId, boolean visible) {
View view = containerView.findViewById(viewId);
if (view != null) {
view.setVisibility(visible ? View.VISIBLE : View.GONE);
}
}
public static class ConditionalCardHolder extends RecyclerView.ViewHolder {
public final ImageView icon;
public final TextView title;
public final TextView summary;
public ConditionalCardHolder(View itemView) {
super(itemView);
icon = itemView.findViewById(android.R.id.icon);
title = itemView.findViewById(android.R.id.title);
summary = itemView.findViewById(android.R.id.summary);
}
}
}

View File

@@ -0,0 +1,138 @@
/*
* 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.conditional;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.graphics.drawable.Drawable;
import com.android.settings.R;
import com.android.settings.homepage.HomepageCardUpdateListener;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.util.ReflectionHelpers;
import java.util.ArrayList;
import java.util.List;
@RunWith(SettingsRobolectricTestRunner.class)
public class ConditionHomepageCardControllerTest {
@Mock
private ConditionManager mConditionManager;
@Mock
private HomepageCardUpdateListener mListener;
private Context mContext;
private ConditionHomepageCardController mController;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = spy(RuntimeEnvironment.application);
mController = spy(new ConditionHomepageCardController(mContext));
ReflectionHelpers.setField(mController, "mConditionManager", mConditionManager);
}
@Test
public void onStart_shouldStartMonitoring() {
mController.onStart();
verify(mConditionManager).startMonitoringStateChange();
}
@Test
public void onStop_shouldStopMonitoring() {
mController.onStop();
verify(mConditionManager).stopMonitoringStateChange();
}
@Test
public void onConditionsChanged_listenerIsSet_shouldUpdateData() {
final FakeConditionalCard fakeConditionalCard = new FakeConditionalCard(mContext);
final List<ConditionalCard> conditionalCards = new ArrayList<>();
conditionalCards.add(fakeConditionalCard);
when(mConditionManager.getDisplayableCards()).thenReturn(conditionalCards);
mController.setHomepageCardUpdateListener(mListener);
mController.onConditionsChanged();
verify(mController).onDataUpdated(any());
}
@Test
public void onConditionsChanged_listenerNotSet_shouldNotUpdateData() {
final FakeConditionalCard fakeConditionalCard = new FakeConditionalCard(mContext);
final List<ConditionalCard> conditionalCards = new ArrayList<>();
conditionalCards.add(fakeConditionalCard);
when(mConditionManager.getDisplayableCards()).thenReturn(conditionalCards);
mController.onConditionsChanged();
verify(mController, never()).onDataUpdated(any());
}
private class FakeConditionalCard implements ConditionalCard {
private final Context mContext;
public FakeConditionalCard(Context context) {
mContext = context;
}
@Override
public long getId() {
return 100;
}
@Override
public CharSequence getActionText() {
return "action_text_test";
}
@Override
public int getMetricsConstant() {
return 1;
}
@Override
public Drawable getIcon() {
return mContext.getDrawable(R.drawable.ic_do_not_disturb_on_24dp);
}
@Override
public CharSequence getTitle() {
return "title_text_test";
}
@Override
public CharSequence getSummary() {
return "summary_text_test";
}
}
}

View File

@@ -0,0 +1,112 @@
/*
* 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.conditional;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.android.settings.R;
import com.android.settings.homepage.ControllerRendererPool;
import com.android.settings.homepage.HomepageCard;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RuntimeEnvironment;
@RunWith(SettingsRobolectricTestRunner.class)
public class ConditionHomepageCardRendererTest {
@Mock
private ControllerRendererPool mControllerRendererPool;
@Mock
private ConditionHomepageCardController mController;
private Context mContext;
private ConditionHomepageCardRenderer mRenderer;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = spy(RuntimeEnvironment.application);
mRenderer = new ConditionHomepageCardRenderer(mContext, mControllerRendererPool);
}
@Test
public void bindView_shouldSetListener() {
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);
final View card = view.findViewById(R.id.content);
when(mControllerRendererPool.getController(mContext,
HomepageCard.CardType.CONDITIONAL)).thenReturn(mController);
mRenderer.bindView(viewHolder, getHomepageCard());
assertThat(card).isNotNull();
assertThat(card.hasOnClickListeners()).isTrue();
}
@Test
public void viewClick_shouldInvokeControllerPrimaryClick() {
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);
final View card = view.findViewById(R.id.content);
when(mControllerRendererPool.getController(mContext,
HomepageCard.CardType.CONDITIONAL)).thenReturn(mController);
mRenderer.bindView(viewHolder, getHomepageCard());
assertThat(card).isNotNull();
card.performClick();
verify(mController).onPrimaryClick(any(HomepageCard.class));
}
private HomepageCard getHomepageCard() {
ConditionCard conditionCard = ((ConditionCard.Builder) new ConditionCard.Builder()
.setConditionId(123)
.setMetricsConstant(1)
.setActionText("test_action")
.setName("test_name")
.setCardType(HomepageCard.CardType.CONDITIONAL)
.setTitleText("test_title")
.setSummaryText("test_summary")
.setIconDrawable(mContext.getDrawable(R.drawable.ic_do_not_disturb_on_24dp))
.setIsHalfWidth(true))
.build();
return conditionCard;
}
}