Combine settings suggestion and condition.

- Add a flag in dashboard feature provider to specify whether to use the
combined UI for suggestions and conditions.
- Move Conditions below Suggestions.
- Add dashboard entity for condition and suggestion container, and
  wrap the condition and suggestion list inside the container. The
  container itself will be a single dashboard item, and within it will
  be the list of suggestion or condition.
- Add suggestion/condition header that will show the combined info for
  the conditions and suggestion data, and have the expand button to
  control expanding both the suggestion and condition list.
- Change the individual condition card to be always expanded, and
remove the logic to collapse/expand individual condition card.
- Remove the divider between the action button and condition detail
  within each condition card.
- Add suggestion/condition footer for collapsing the whole suggestion and
  condition list.

Bug: 37645754
Test: make RunSettingsRoboTests
Change-Id: I86df75f7e4551778f79d730851c03121fd0dcbdf
This commit is contained in:
Doris Ling
2017-05-11 16:27:54 -07:00
parent 52c031edae
commit 83a6621b38
23 changed files with 1476 additions and 110 deletions

View File

@@ -20,11 +20,13 @@ import android.annotation.ColorInt;
import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.support.annotation.VisibleForTesting;
import android.support.v7.util.DiffUtil;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.util.ArrayMap;
@@ -33,19 +35,28 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.settings.R;
import com.android.settings.R.id;
import com.android.settings.SettingsActivity;
import com.android.settings.core.instrumentation.MetricsFeatureProvider;
import com.android.settings.dashboard.DashboardData.SuggestionConditionHeaderData;
import com.android.settings.dashboard.conditional.Condition;
import com.android.settings.dashboard.conditional.ConditionAdapter;
import com.android.settings.dashboard.conditional.ConditionAdapterUtils;
import com.android.settings.dashboard.conditional.FocusRecyclerView;
import com.android.settings.dashboard.suggestions.SuggestionAdapter;
import com.android.settings.dashboard.suggestions.SuggestionDismissController;
import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.Utils;
import com.android.settingslib.drawer.DashboardCategory;
import com.android.settingslib.drawer.Tile;
import com.android.settingslib.suggestions.SuggestionParser;
import java.util.ArrayList;
import java.util.List;
@@ -57,6 +68,7 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
private static final String STATE_SUGGESTION_MODE = "suggestion_mode";
private static final String STATE_SUGGESTIONS_SHOWN_LOGGED = "suggestions_shown_logged";
private static final int DONT_SET_BACKGROUND_ATTR = -1;
private static final String STATE_SUGGESTION_CONDITION_MODE = "suggestion_condition_mode";
private final IconCache mCache;
private final Context mContext;
@@ -65,6 +77,12 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
private final SuggestionFeatureProvider mSuggestionFeatureProvider;
private final ArrayList<String> mSuggestionsShownLogged;
private boolean mFirstFrameDrawn;
private boolean mCombineSuggestionAndCondition;
private RecyclerView mRecyclerView;
private SuggestionParser mSuggestionParser;
private SuggestionAdapter mSuggestionAdapter;
private SuggestionDismissController mSuggestionDismissHandler;
private SuggestionDismissController.Callback mCallback;
@VisibleForTesting
DashboardData mDashboardData;
@@ -81,43 +99,65 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
@Override
public void onClick(View v) {
Condition expandedCondition = mDashboardData.getExpandedCondition();
//TODO: get rid of setTag/getTag
if (v.getTag() == expandedCondition) {
if (mCombineSuggestionAndCondition) {
Condition condition = (Condition) v.getTag();
//TODO: get rid of setTag/getTag
mMetricsFeatureProvider.action(mContext,
MetricsEvent.ACTION_SETTINGS_CONDITION_CLICK,
condition.getMetricsConstant());
condition.onPrimaryClick();
} else {
Condition expandedCondition = mDashboardData.getExpandedCondition();
//TODO: get rid of setTag/getTag
if (v.getTag() == expandedCondition) {
mMetricsFeatureProvider.action(mContext,
MetricsEvent.ACTION_SETTINGS_CONDITION_CLICK,
expandedCondition.getMetricsConstant());
expandedCondition.onPrimaryClick();
} else {
expandedCondition = (Condition) v.getTag();
mMetricsFeatureProvider.action(mContext,
expandedCondition.onPrimaryClick();
} else {
expandedCondition = (Condition) v.getTag();
mMetricsFeatureProvider.action(mContext,
MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND,
expandedCondition.getMetricsConstant());
updateExpandedCondition(expandedCondition);
updateExpandedCondition(expandedCondition);
}
}
}
};
@Deprecated
public DashboardAdapter(Context context, Bundle savedInstanceState,
List<Condition> conditions) {
List<Condition> conditions) {
this(context, savedInstanceState, conditions, null, null);
}
public DashboardAdapter(Context context, Bundle savedInstanceState,
List<Condition> conditions, SuggestionParser suggestionParser,
SuggestionDismissController.Callback callback) {
List<Tile> suggestions = null;
List<DashboardCategory> categories = null;
int suggestionMode = DashboardData.SUGGESTION_MODE_DEFAULT;
int suggestionConditionMode = DashboardData.HEADER_MODE_DEFAULT;
mContext = context;
final FeatureFactory factory = FeatureFactory.getFactory(context);
mMetricsFeatureProvider = factory.getMetricsFeatureProvider();
mDashboardFeatureProvider = factory.getDashboardFeatureProvider(context);
mSuggestionFeatureProvider = factory.getSuggestionFeatureProvider(context);
mCombineSuggestionAndCondition = mDashboardFeatureProvider.combineSuggestionAndCondition();
mCache = new IconCache(context);
mSuggestionParser = suggestionParser;
mCallback = callback;
setHasStableIds(true);
if (savedInstanceState != null) {
suggestions = savedInstanceState.getParcelableArrayList(STATE_SUGGESTION_LIST);
categories = savedInstanceState.getParcelableArrayList(STATE_CATEGORY_LIST);
suggestionConditionMode = savedInstanceState.getInt(
STATE_SUGGESTION_CONDITION_MODE, suggestionConditionMode);
suggestionMode = savedInstanceState.getInt(
STATE_SUGGESTION_MODE, DashboardData.SUGGESTION_MODE_DEFAULT);
mSuggestionsShownLogged = savedInstanceState.getStringArrayList(
@@ -131,6 +171,8 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
.setSuggestions(suggestions)
.setCategories(categories)
.setSuggestionMode(suggestionMode)
.setCombineSuggestionAndCondition(mCombineSuggestionAndCondition)
.setSuggestionConditionMode(suggestionConditionMode)
.build();
}
@@ -167,14 +209,24 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
.build();
notifyDashboardDataChanged(prevData);
List<Tile> shownSuggestions = null;
switch (mDashboardData.getSuggestionMode()) {
case DashboardData.SUGGESTION_MODE_DEFAULT:
if (mCombineSuggestionAndCondition) {
final int mode = mDashboardData.getSuggestionConditionMode();
if (mode == DashboardData.HEADER_MODE_DEFAULT) {
shownSuggestions = suggestions.subList(0,
Math.min(suggestions.size(), DashboardData.DEFAULT_SUGGESTION_COUNT));
break;
case DashboardData.SUGGESTION_MODE_EXPANDED:
Math.min(suggestions.size(), DashboardData.DEFAULT_SUGGESTION_COUNT));
} else if (mode != DashboardData.HEADER_MODE_COLLAPSED) {
shownSuggestions = suggestions;
break;
}
} else {
switch (mDashboardData.getSuggestionMode()) {
case DashboardData.SUGGESTION_MODE_DEFAULT:
shownSuggestions = suggestions.subList(0,
Math.min(suggestions.size(), DashboardData.DEFAULT_SUGGESTION_COUNT));
break;
case DashboardData.SUGGESTION_MODE_EXPANDED:
shownSuggestions = suggestions;
break;
}
}
if (shownSuggestions != null) {
for (Tile suggestion : shownSuggestions) {
@@ -199,10 +251,16 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
public void setConditions(List<Condition> conditions) {
final DashboardData prevData = mDashboardData;
Log.d(TAG, "adapter setConditions called");
mDashboardData = new DashboardData.Builder(prevData)
if (mCombineSuggestionAndCondition) {
mDashboardData = new DashboardData.Builder(prevData)
.setConditions(conditions)
.build();
} else {
mDashboardData = new DashboardData.Builder(prevData)
.setConditions(conditions)
.setExpandedCondition(null)
.build();
}
notifyDashboardDataChanged(prevData);
}
@@ -218,8 +276,14 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
@Override
public DashboardItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new DashboardItemHolder(LayoutInflater.from(parent.getContext()).inflate(
viewType, parent, false));
final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
if (viewType == R.layout.suggestion_condition_header) {
return new SuggestionAndConditionHeaderHolder(view);
}
if (viewType == R.layout.suggestion_condition_container) {
return new SuggestionAndConditionContainerHolder(view);
}
return new DashboardItemHolder(view);
}
@Override
@@ -277,6 +341,43 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
(Condition) mDashboardData.getItemEntityByPosition(position),
holder, isExpanded, mConditionClickListener, v -> onExpandClick(v));
break;
case R.layout.suggestion_condition_container:
onBindConditionAndSuggestion(
(SuggestionAndConditionContainerHolder) holder, position);
break;
case R.layout.suggestion_condition_header:
/* There are 2 different headers for the suggestions/conditions section. To minimize
visual animation when expanding and collapsing the suggestions/conditions, we are
using the same layout to represent the 2 headers:
1. Suggestion header - when there is any suggestion shown, the suggestion header
will be the first item on the section. It only has the text "Suggestion", and
do nothing when clicked. This header will not be shown when the section is
collapsed, in which case, the SuggestionCondition header will be
shown instead to show the summary info.
2. SuggestionCondition header - the header that shows the summary info for the
suggestion/condition that is currently hidden. It has the expand button to
expand the section. */
if (mDashboardData.getDisplayableSuggestionCount() > 0 && position == 1
&& mDashboardData.getSuggestionConditionMode()
!= DashboardData.HEADER_MODE_COLLAPSED) {
onBindSuggestionHeader((SuggestionAndConditionHeaderHolder) holder);
} else {
onBindSuggestionConditionHeader((SuggestionAndConditionHeaderHolder) holder,
(SuggestionConditionHeaderData)
mDashboardData.getItemEntityByPosition(position));
}
break;
case R.layout.suggestion_condition_footer:
holder.itemView.setOnClickListener(v -> {
mMetricsFeatureProvider.action(mContext,
MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND, false);
DashboardData prevData = mDashboardData;
mDashboardData = new DashboardData.Builder(prevData).setSuggestionConditionMode(
DashboardData.HEADER_MODE_COLLAPSED).build();
notifyDashboardDataChanged(prevData);
mRecyclerView.scrollToPosition(1);
});
break;
}
}
@@ -295,6 +396,14 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
return mDashboardData.size();
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
// save the view so that we can scroll it when expanding/collapsing the suggestion and
// conditions.
mRecyclerView = recyclerView;
}
public void onPause() {
if (mDashboardData.getSuggestions() == null) {
return;
@@ -310,6 +419,9 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
mSuggestionsShownLogged.clear();
}
// condition card is always expanded in new suggestion/condition UI.
// TODO: Remove when completely move to new suggestion/condition UI
@Deprecated
public void onExpandClick(View v) {
Condition expandedCondition = mDashboardData.getExpandedCondition();
if (v.getTag() == expandedCondition) {
@@ -330,6 +442,13 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
return mDashboardData.getItemEntityById(itemId);
}
public Tile getSuggestion(int position) {
if (mCombineSuggestionAndCondition) {
return mSuggestionAdapter.getSuggestion(position);
}
return (Tile) getItem(getItemId(position));
}
private void notifyDashboardDataChanged(DashboardData prevData) {
if (mFirstFrameDrawn && prevData != null) {
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DashboardData
@@ -405,17 +524,7 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
final int suggestionMode;
if (moreSuggestions) {
suggestionMode = DashboardData.SUGGESTION_MODE_EXPANDED;
for (Tile suggestion : mDashboardData.getSuggestions()) {
final String suggestionId = mSuggestionFeatureProvider.getSuggestionIdentifier(
mContext, suggestion);
if (!mSuggestionsShownLogged.contains(suggestionId)) {
mMetricsFeatureProvider.action(
mContext, MetricsEvent.ACTION_SHOW_SETTINGS_SUGGESTION,
suggestionId);
mSuggestionsShownLogged.add(suggestionId);
}
}
logSuggestions();
} else {
suggestionMode = DashboardData.SUGGESTION_MODE_COLLAPSED;
}
@@ -428,6 +537,130 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
});
}
private void logSuggestions() {
for (Tile suggestion : mDashboardData.getSuggestions()) {
final String suggestionId = mSuggestionFeatureProvider.getSuggestionIdentifier(
mContext, suggestion);
if (!mSuggestionsShownLogged.contains(suggestionId)) {
mMetricsFeatureProvider.action(
mContext, MetricsEvent.ACTION_SHOW_SETTINGS_SUGGESTION,
suggestionId);
mSuggestionsShownLogged.add(suggestionId);
}
}
}
private void onBindSuggestionHeader(final SuggestionAndConditionHeaderHolder holder) {
holder.title.setText(R.string.suggestions_title);
holder.title.setTextColor(Color.BLACK);
holder.icon.setVisibility(View.INVISIBLE);
holder.icons.removeAllViews();
holder.icons.setVisibility(View.INVISIBLE);
holder.summary.setVisibility(View.INVISIBLE);
holder.expandIndicator.setVisibility(View.INVISIBLE);
holder.itemView.setOnClickListener(null);
}
private void onBindSuggestionConditionHeader(final SuggestionAndConditionHeaderHolder holder,
SuggestionConditionHeaderData data) {
final int curMode = mDashboardData.getSuggestionConditionMode();
final int nextMode = data.hiddenSuggestionCount > 0 && data.conditionCount > 0
&& curMode != DashboardData.HEADER_MODE_SUGGESTION_EXPANDED
? DashboardData.HEADER_MODE_SUGGESTION_EXPANDED
: DashboardData.HEADER_MODE_FULLY_EXPANDED;
final boolean moreSuggestions = data.hiddenSuggestionCount > 0;
final boolean hasConditions = data.conditionCount > 0;
if (data.conditionCount > 0) {
holder.icon.setImageIcon(data.conditionIcons.get(0));
holder.icon.setVisibility(View.VISIBLE);
if (data.conditionCount == 1) {
holder.title.setText(data.title);
holder.title.setTextColor(Utils.getColorAccent(mContext));
holder.icons.setVisibility(View.INVISIBLE);
} else {
holder.title.setText(null);
updateConditionIcons(data.conditionIcons, holder.icons);
holder.icons.setVisibility(View.VISIBLE);
}
} else {
holder.icon.setVisibility(View.INVISIBLE);
holder.icons.setVisibility(View.INVISIBLE);
}
if (data.hiddenSuggestionCount > 0) {
holder.summary.setTextColor(Color.BLACK);
if (curMode == DashboardData.HEADER_MODE_COLLAPSED) {
if (data.conditionCount > 0) {
holder.summary.setText(mContext.getResources().getQuantityString(
R.plurals.suggestions_collapsed_summary,
data.hiddenSuggestionCount, data.hiddenSuggestionCount));
} else {
holder.title.setText(mContext.getResources().getQuantityString(
R.plurals.suggestions_collapsed_title,
data.hiddenSuggestionCount, data.hiddenSuggestionCount));
holder.title.setTextColor(Color.BLACK);
holder.summary.setText(null);
}
} else if (curMode == DashboardData.HEADER_MODE_DEFAULT) {
if (data.conditionCount > 0) {
holder.summary.setText(mContext.getString(
R.string.suggestions_summary, data.hiddenSuggestionCount));
} else {
holder.title.setText(mContext.getString(
R.string.suggestions_more_title, data.hiddenSuggestionCount));
holder.title.setTextColor(Color.BLACK);
holder.summary.setText(null);
}
}
} else if (data.conditionCount > 1) {
holder.summary.setTextColor(Utils.getColorAccent(mContext));
holder.summary.setText(
mContext.getString(R.string.condition_summary, data.conditionCount));
} else {
holder.summary.setText(null);
}
holder.summary.setVisibility(View.VISIBLE);
holder.expandIndicator.setVisibility(View.VISIBLE);
holder.itemView.setOnClickListener(v -> {
if (moreSuggestions ) {
logSuggestions();
} else if (hasConditions) {
mMetricsFeatureProvider.action(mContext,
MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND, true);
}
DashboardData prevData = mDashboardData;
final boolean wasCollapsed = curMode == DashboardData.HEADER_MODE_COLLAPSED;
mDashboardData = new DashboardData.Builder(prevData)
.setSuggestionConditionMode(nextMode).build();
notifyDashboardDataChanged(prevData);
if (wasCollapsed) {
mRecyclerView.scrollToPosition(1);
}
});
}
private void onBindConditionAndSuggestion(final SuggestionAndConditionContainerHolder holder,
int position) {
RecyclerView.Adapter<DashboardItemHolder> adapter;
// If there is suggestions to show, it will be at position 2 (position 0 = header spacer,
// position 1 is suggestion header.
if (position == 2 && mDashboardData.getSuggestions() != null) {
mSuggestionAdapter = new SuggestionAdapter(mContext, (List<Tile>)
mDashboardData.getItemEntityByPosition(position), mSuggestionsShownLogged);
adapter = mSuggestionAdapter;
mSuggestionDismissHandler = new SuggestionDismissController(mContext,
holder.data, mSuggestionParser, mCallback);
} else {
ConditionAdapterUtils.addDismiss(holder.data);
adapter = new ConditionAdapter(mContext,
(List<Condition>) mDashboardData.getItemEntityByPosition(position),
mDashboardData.getSuggestionConditionMode());
}
holder.data.setLayoutManager(new LinearLayoutManager(mContext));
holder.data.setAdapter(adapter);
}
private void onBindTile(DashboardItemHolder holder, Tile tile) {
if (tile.remoteViews != null) {
final ViewGroup itemView = (ViewGroup) holder.itemView;
@@ -460,9 +693,27 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
}
outState.putInt(STATE_SUGGESTION_MODE, mDashboardData.getSuggestionMode());
outState.putStringArrayList(STATE_SUGGESTIONS_SHOWN_LOGGED, mSuggestionsShownLogged);
outState.putInt(STATE_SUGGESTION_CONDITION_MODE,
mDashboardData.getSuggestionConditionMode());
}
private static class IconCache {
private void updateConditionIcons(List<Icon> icons, ViewGroup parent) {
if (icons == null || icons.size() < 2) {
parent.setVisibility(View.INVISIBLE);
return;
}
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
parent.removeAllViews();
for (int i = 1, size = icons.size(); i < size; i++) {
ImageView icon = (ImageView) inflater.inflate(
R.layout.condition_header_icon, parent, false);
icon.setImageIcon(icons.get(i));
parent.addView(icon);
}
parent.setVisibility(View.VISIBLE);
}
public static class IconCache {
private final Context mContext;
private final ArrayMap<Icon, Drawable> mMap = new ArrayMap<>();
@@ -492,4 +743,25 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
summary = itemView.findViewById(android.R.id.summary);
}
}
public static class SuggestionAndConditionHeaderHolder extends DashboardItemHolder {
public final LinearLayout icons;
public final ImageView expandIndicator;
public SuggestionAndConditionHeaderHolder(View itemView) {
super(itemView);
icons = itemView.findViewById(id.additional_icons);
expandIndicator = itemView.findViewById(id.expand_indicator);
}
}
public static class SuggestionAndConditionContainerHolder extends DashboardItemHolder {
public final RecyclerView data;
public SuggestionAndConditionContainerHolder(View itemView) {
super(itemView);
data = itemView.findViewById(id.data);
}
}
}