Elicit ItemsData class and add DiffUtil.Callback

In the previous version, when there is a minor change in data, we
will refresh the whole screen(and data) by invoking "recountItems"
and "notifyDataSetChanged", which did lots of unnecessary works.

In this new cl, I elicit ItemsData class, which encapsulates the list
data used in adapter. When data changed, I build another ItemsData and
use the DiffUtil.Callback to calculate diffs between ItemsDatas. In
this way we can only refresh the items that changed in adapter.

Since I cannot find usage of see_all.xml anymore, I delete the relevant
code as well as the resource files.

Bug: 30319913
Test: make RunSettingsRoboTests
Change-Id: I4f753a26f624affea6c6c35d49cfb9c43fb74fe6
This commit is contained in:
jackqdyulei
2016-10-05 18:05:52 -07:00
parent c33b78df9c
commit 0b818b79db
5 changed files with 1026 additions and 314 deletions

View File

@@ -20,6 +20,7 @@ import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.support.v7.util.DiffUtil;
import android.support.v7.widget.PopupMenu;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
@@ -34,7 +35,6 @@ import android.widget.ImageView;
import android.widget.TextView;
import com.android.internal.logging.MetricsProto.MetricsEvent;
import com.android.internal.util.ArrayUtils;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.core.instrumentation.MetricsFeatureProvider;
@@ -48,82 +48,92 @@ import java.util.ArrayList;
import java.util.List;
public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.DashboardItemHolder>
implements View.OnClickListener, SummaryLoader.SummaryConsumer {
implements SummaryLoader.SummaryConsumer {
public static final String TAG = "DashboardAdapter";
private static final String STATE_SUGGESTION_LIST = "suggestion_list";
private static final String STATE_CATEGORY_LIST = "category_list";
private static final String STATE_IS_SHOWING_ALL = "is_showing_all";
private static final String STATE_SUGGESTION_MODE = "suggestion_mode";
private static final int NS_SPACER = 0;
private static final int NS_SUGGESTION = 1000;
private static final int NS_ITEMS = 2000;
private static final int NS_CONDITION = 3000;
private static int SUGGESTION_MODE_DEFAULT = 0;
private static int SUGGESTION_MODE_COLLAPSED = 1;
private static int SUGGESTION_MODE_EXPANDED = 2;
private static final int DEFAULT_SUGGESTION_COUNT = 2;
private final List<Object> mItems = new ArrayList<>();
private final List<Integer> mTypes = new ArrayList<>();
private final List<Integer> mIds = new ArrayList<>();
private final IconCache mCache;
private final Context mContext;
private final MetricsFeatureProvider mMetricsFeatureProvider;
private List<DashboardCategory> mCategories;
private List<Condition> mConditions;
private List<Tile> mSuggestions;
private boolean mIsShowingAll;
// Used for counting items;
private int mId;
private int mSuggestionMode = SUGGESTION_MODE_DEFAULT;
private Condition mExpandedCondition = null;
private DashboardData mDashboardData;
private SuggestionParser mSuggestionParser;
private View.OnClickListener mTileClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
//TODO: get rid of setTag/getTag
final Tile tile = (Tile) mDashboardData.getItemEntityByPosition((int) v.getTag());
((SettingsActivity) mContext).openTile(tile);
}
};
private View.OnClickListener mConditionClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
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,
MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND,
expandedCondition.getMetricsConstant());
updateExpandedCondition(expandedCondition);
}
}
};
public DashboardAdapter(Context context, SuggestionParser parser,
MetricsFeatureProvider metricsFeatureProvider, Bundle savedInstanceState,
List<Condition> conditions) {
List<Tile> suggestions = null;
List<DashboardCategory> categories = null;
int suggestionMode = DashboardData.SUGGESTION_MODE_DEFAULT;
mContext = context;
mMetricsFeatureProvider = metricsFeatureProvider;
mCache = new IconCache(context);
mSuggestionParser = parser;
mConditions = conditions;
setHasStableIds(true);
boolean showAll = true;
if (savedInstanceState != null) {
mSuggestions = savedInstanceState.getParcelableArrayList(STATE_SUGGESTION_LIST);
mCategories = savedInstanceState.getParcelableArrayList(STATE_CATEGORY_LIST);
showAll = savedInstanceState.getBoolean(STATE_IS_SHOWING_ALL, true);
mSuggestionMode = savedInstanceState.getInt(
STATE_SUGGESTION_MODE, SUGGESTION_MODE_DEFAULT);
suggestions = savedInstanceState.getParcelableArrayList(STATE_SUGGESTION_LIST);
categories = savedInstanceState.getParcelableArrayList(STATE_CATEGORY_LIST);
suggestionMode = savedInstanceState.getInt(
STATE_SUGGESTION_MODE, DashboardData.SUGGESTION_MODE_DEFAULT);
}
setShowingAll(showAll);
mDashboardData = new DashboardData.Builder()
.setConditions(conditions)
.setSuggestions(suggestions)
.setCategories(categories)
.setSuggestionMode(suggestionMode)
.build();
}
public List<Tile> getSuggestions() {
return mSuggestions;
return mDashboardData.getSuggestions();
}
public void setCategoriesAndSuggestions(List<DashboardCategory> categories,
List<Tile> suggestions) {
mSuggestions = suggestions;
mCategories = categories;
// TODO: Better place for tinting?
TypedValue tintColor = new TypedValue();
mContext.getTheme().resolveAttribute(com.android.internal.R.attr.colorAccent,
tintColor, true);
for (int i = 0; i < categories.size(); i++) {
for (int j = 0; j < categories.get(i).tiles.size(); j++) {
Tile tile = categories.get(i).tiles.get(j);
final Tile tile = categories.get(i).tiles.get(j);
if (!mContext.getPackageName().equals(
tile.intent.getComponent().getPackageName())) {
@@ -133,85 +143,49 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
}
}
}
recountItems();
final DashboardData prevData = mDashboardData;
mDashboardData = new DashboardData.Builder(prevData)
.setSuggestions(suggestions)
.setCategories(categories)
.build();
notifyDashboardDataChanged(prevData);
}
public void setConditions(List<Condition> conditions) {
mConditions = conditions;
recountItems();
final DashboardData prevData = mDashboardData;
mDashboardData = new DashboardData.Builder(prevData)
.setConditions(conditions)
.build();
notifyDashboardDataChanged(prevData);
}
@Override
public void notifySummaryChanged(Tile tile) {
notifyDataSetChanged();
}
final int position = mDashboardData.getPositionByTile(tile);
if (position != DashboardData.POSITION_NOT_FOUND) {
final Tile targetTile = (Tile) mDashboardData.getItemEntityByPosition(position);
if (!TextUtils.equals(tile.summary, targetTile.summary)) {
public void setShowingAll(boolean showingAll) {
mIsShowingAll = showingAll;
recountItems();
}
private void recountItems() {
reset();
boolean hasConditions = false;
for (int i = 0; mConditions != null && i < mConditions.size(); i++) {
boolean shouldShow = mConditions.get(i).shouldShow();
hasConditions |= shouldShow;
countItem(mConditions.get(i), R.layout.condition_card, shouldShow, NS_CONDITION);
}
boolean hasSuggestions = mSuggestions != null && mSuggestions.size() != 0;
countItem(null, R.layout.dashboard_spacer, hasConditions && hasSuggestions, NS_SPACER);
countItem(null, R.layout.suggestion_header, hasSuggestions, NS_SPACER);
resetCount();
if (mSuggestions != null) {
int maxSuggestions = getDisplayableSuggestionCount();
for (int i = 0; i < mSuggestions.size(); i++) {
countItem(mSuggestions.get(i), R.layout.suggestion_tile, i < maxSuggestions,
NS_SUGGESTION);
// Since usually tile in parameter and tile in mCategories are same instance,
// which is hard to be detected by DiffUtil, so we notifyItemChanged directly.
notifyItemChanged(position);
}
}
resetCount();
for (int i = 0; mCategories != null && i < mCategories.size(); i++) {
DashboardCategory category = mCategories.get(i);
countItem(category, R.layout.dashboard_category, mIsShowingAll
&& !TextUtils.isEmpty(category.title), NS_ITEMS);
for (int j = 0; j < category.tiles.size(); j++) {
Tile tile = category.tiles.get(j);
countItem(tile, R.layout.dashboard_tile, mIsShowingAll
|| ArrayUtils.contains(DashboardSummary.INITIAL_ITEMS,
tile.intent.getComponent().getClassName()), NS_ITEMS);
}
}
// TODO: move this method to SuggestionParser or some other util class
public void disableSuggestion(Tile suggestion) {
if (mSuggestionParser == null) {
return;
}
notifyDataSetChanged();
}
private void resetCount() {
mId = 0;
}
private void reset() {
mItems.clear();
mTypes.clear();
mIds.clear();
mId = 0;
}
private void countItem(Object object, int type, boolean add, int nameSpace) {
if (add) {
mItems.add(object);
mTypes.add(type);
// TODO: Counting namespaces for handling of suggestions/conds appearing/disappearing.
mIds.add(mId + nameSpace);
if (mSuggestionParser.dismissSuggestion(suggestion)) {
mContext.getPackageManager().setComponentEnabledSetting(
suggestion.intent.getComponent(),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
mSuggestionParser.markCategoryDone(suggestion.category);
}
mId++;
}
private int getDisplayableSuggestionCount() {
final int suggestionSize = mSuggestions.size();
return mSuggestionMode == SUGGESTION_MODE_DEFAULT
? Math.min(DEFAULT_SUGGESTION_COUNT, suggestionSize)
: mSuggestionMode == SUGGESTION_MODE_EXPANDED
? suggestionSize : 0;
}
@Override
@@ -222,21 +196,24 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
@Override
public void onBindViewHolder(DashboardItemHolder holder, int position) {
switch (mTypes.get(position)) {
final int type = mDashboardData.getItemTypeByPosition(position);
switch (type) {
case R.layout.dashboard_category:
onBindCategory(holder, (DashboardCategory) mItems.get(position));
onBindCategory(holder,
(DashboardCategory) mDashboardData.getItemEntityByPosition(position));
break;
case R.layout.dashboard_tile:
final Tile tile = (Tile) mItems.get(position);
final Tile tile = (Tile) mDashboardData.getItemEntityByPosition(position);
onBindTile(holder, tile);
holder.itemView.setTag(tile);
holder.itemView.setOnClickListener(this);
holder.itemView.setTag(position);
holder.itemView.setOnClickListener(mTileClickListener);
break;
case R.layout.suggestion_header:
onBindSuggestionHeader(holder);
onBindSuggestionHeader(holder, (DashboardData.SuggestionHeaderData)
mDashboardData.getItemEntityByPosition(position));
break;
case R.layout.suggestion_tile:
final Tile suggestion = (Tile) mItems.get(position);
final Tile suggestion = (Tile) mDashboardData.getItemEntityByPosition(position);
onBindTile(holder, suggestion);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
@@ -255,12 +232,12 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
}
});
break;
case R.layout.see_all:
onBindSeeAll(holder);
break;
case R.layout.condition_card:
ConditionAdapterUtils.bindViews((Condition) mItems.get(position), holder,
mItems.get(position) == mExpandedCondition, this,
final boolean isExpanded = mDashboardData.getItemEntityByPosition(position)
== mDashboardData.getExpandedCondition();
ConditionAdapterUtils.bindViews(
(Condition) mDashboardData.getItemEntityByPosition(position),
holder, isExpanded, mConditionClickListener,
new View.OnClickListener() {
@Override
public void onClick(View v) {
@@ -271,45 +248,103 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
}
}
@Override
public long getItemId(int position) {
return mDashboardData.getItemIdByPosition(position);
}
@Override
public int getItemViewType(int position) {
return mDashboardData.getItemTypeByPosition(position);
}
@Override
public int getItemCount() {
return mDashboardData.size();
}
public void onExpandClick(View v) {
Condition expandedCondition = mDashboardData.getExpandedCondition();
if (v.getTag() == expandedCondition) {
mMetricsFeatureProvider.action(mContext,
MetricsEvent.ACTION_SETTINGS_CONDITION_COLLAPSE,
expandedCondition.getMetricsConstant());
expandedCondition = null;
} else {
expandedCondition = (Condition) v.getTag();
mMetricsFeatureProvider.action(mContext, MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND,
expandedCondition.getMetricsConstant());
}
updateExpandedCondition(expandedCondition);
}
public Object getItem(long itemId) {
return mDashboardData.getItemEntityById(itemId);
}
public static String getSuggestionIdentifier(Context context, Tile suggestion) {
String packageName = suggestion.intent.getComponent().getPackageName();
if (packageName.equals(context.getPackageName())) {
// Since Settings provides several suggestions, fill in the class instead of the
// package for these.
packageName = suggestion.intent.getComponent().getClassName();
}
return packageName;
}
private void notifyDashboardDataChanged(DashboardData prevData) {
if (prevData != null) {
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DashboardData
.ItemsDataDiffCallback(prevData.getItemList(), mDashboardData.getItemList()));
diffResult.dispatchUpdatesTo(this);
} else {
notifyDataSetChanged();
}
}
private void updateExpandedCondition(Condition condition) {
final DashboardData prevData = mDashboardData;
mDashboardData = new DashboardData.Builder(prevData)
.setExpandedCondition(condition)
.build();
notifyDashboardDataChanged(prevData);
}
private void showRemoveOption(View v, final Tile suggestion) {
PopupMenu popup = new PopupMenu(
final PopupMenu popup = new PopupMenu(
new ContextThemeWrapper(mContext, R.style.Theme_AppCompat_DayNight), v);
popup.getMenu().add(R.string.suggestion_remove).setOnMenuItemClickListener(
new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
mMetricsFeatureProvider.action(
mContext, MetricsEvent.ACTION_SETTINGS_DISMISS_SUGGESTION,
DashboardAdapter.getSuggestionIdentifier(mContext, suggestion));
disableSuggestion(suggestion);
mSuggestions.remove(suggestion);
recountItems();
return true;
}
});
@Override
public boolean onMenuItemClick(MenuItem item) {
mMetricsFeatureProvider.action(
mContext, MetricsEvent.ACTION_SETTINGS_DISMISS_SUGGESTION,
DashboardAdapter.getSuggestionIdentifier(mContext, suggestion));
disableSuggestion(suggestion);
List<Tile> suggestions = mDashboardData.getSuggestions();
suggestions.remove(suggestion);
DashboardData prevData = mDashboardData;
mDashboardData = new DashboardData.Builder(prevData)
.setSuggestions(suggestions)
.build();
notifyDashboardDataChanged(prevData);
return true;
}
});
popup.show();
}
public void disableSuggestion(Tile suggestion) {
if (mSuggestionParser == null) {
return;
}
if (mSuggestionParser.dismissSuggestion(suggestion)) {
mContext.getPackageManager().setComponentEnabledSetting(
suggestion.intent.getComponent(),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
mSuggestionParser.markCategoryDone(suggestion.category);
}
}
private void onBindSuggestionHeader(final DashboardItemHolder holder, DashboardData
.SuggestionHeaderData data) {
final boolean moreSuggestions = data.hasMoreSuggestions;
final int undisplayedSuggestionCount = data.undisplayedSuggestionCount;
private void onBindSuggestionHeader(final DashboardItemHolder holder) {
final boolean moreSuggestions = hasMoreSuggestions();
final int undisplayedSuggestionCount =
mSuggestions.size() - getDisplayableSuggestionCount();
holder.icon.setImageResource(moreSuggestions ? R.drawable.ic_expand_more
: R.drawable.ic_expand_less);
holder.title.setText(mContext.getString(R.string.suggestions_title, mSuggestions.size()));
holder.title.setText(mContext.getString(R.string.suggestions_title, data.suggestionSize));
String summaryContentDescription;
if (moreSuggestions) {
summaryContentDescription = mContext.getResources().getQuantityString(
@@ -329,22 +364,22 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (hasMoreSuggestions()) {
mSuggestionMode = SUGGESTION_MODE_EXPANDED;
final int suggestionMode;
if (moreSuggestions) {
suggestionMode = DashboardData.SUGGESTION_MODE_EXPANDED;
} else {
mSuggestionMode = SUGGESTION_MODE_COLLAPSED;
suggestionMode = DashboardData.SUGGESTION_MODE_COLLAPSED;
}
recountItems();
DashboardData prevData = mDashboardData;
mDashboardData = new DashboardData.Builder(prevData)
.setSuggestionMode(suggestionMode)
.build();
notifyDashboardDataChanged(prevData);
}
});
}
private boolean hasMoreSuggestions() {
return mSuggestionMode == SUGGESTION_MODE_COLLAPSED
|| (mSuggestionMode == SUGGESTION_MODE_DEFAULT
&& mSuggestions.size() > DEFAULT_SUGGESTION_COUNT);
}
private void onBindTile(DashboardItemHolder holder, Tile tile) {
holder.icon.setImageDrawable(mCache.getIcon(tile.icon));
holder.title.setText(tile.title);
@@ -360,98 +395,21 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
holder.title.setText(category.title);
}
private void onBindSeeAll(DashboardItemHolder holder) {
holder.title.setText(mIsShowingAll ? R.string.see_less
: R.string.see_all);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setShowingAll(!mIsShowingAll);
}
});
}
@Override
public long getItemId(int position) {
return mIds.get(position);
}
@Override
public int getItemViewType(int position) {
return mTypes.get(position);
}
@Override
public int getItemCount() {
return mIds.size();
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.dashboard_tile) {
((SettingsActivity) mContext).openTile((Tile) v.getTag());
return;
}
if (v.getTag() == mExpandedCondition) {
mMetricsFeatureProvider.action(mContext, MetricsEvent.ACTION_SETTINGS_CONDITION_CLICK,
mExpandedCondition.getMetricsConstant());
mExpandedCondition.onPrimaryClick();
} else {
mExpandedCondition = (Condition) v.getTag();
mMetricsFeatureProvider.action(mContext, MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND,
mExpandedCondition.getMetricsConstant());
notifyDataSetChanged();
}
}
public void onExpandClick(View v) {
if (v.getTag() == mExpandedCondition) {
mMetricsFeatureProvider.action(mContext,
MetricsEvent.ACTION_SETTINGS_CONDITION_COLLAPSE,
mExpandedCondition.getMetricsConstant());
mExpandedCondition = null;
} else {
mExpandedCondition = (Condition) v.getTag();
mMetricsFeatureProvider.action(mContext, MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND,
mExpandedCondition.getMetricsConstant());
}
notifyDataSetChanged();
}
public Object getItem(long itemId) {
for (int i = 0; i < mIds.size(); i++) {
if (mIds.get(i) == itemId) {
return mItems.get(i);
}
}
return null;
}
public static String getSuggestionIdentifier(Context context, Tile suggestion) {
String packageName = suggestion.intent.getComponent().getPackageName();
if (packageName.equals(context.getPackageName())) {
// Since Settings provides several suggestions, fill in the class instead of the
// package for these.
packageName = suggestion.intent.getComponent().getClassName();
}
return packageName;
}
void onSaveInstanceState(Bundle outState) {
if (mSuggestions != null) {
final List<Tile> suggestions = mDashboardData.getSuggestions();
final List<DashboardCategory> categories = mDashboardData.getCategories();
if (suggestions != null) {
outState.putParcelableArrayList(STATE_SUGGESTION_LIST,
new ArrayList<Tile>(mSuggestions));
new ArrayList<Tile>(suggestions));
}
if (mCategories != null) {
if (categories != null) {
outState.putParcelableArrayList(STATE_CATEGORY_LIST,
new ArrayList<DashboardCategory>(mCategories));
new ArrayList<DashboardCategory>(categories));
}
outState.putBoolean(STATE_IS_SHOWING_ALL, mIsShowingAll);
outState.putInt(STATE_SUGGESTION_MODE, mSuggestionMode);
outState.putInt(STATE_SUGGESTION_MODE, mDashboardData.getSuggestionMode());
}
private static class IconCache {
private final Context mContext;
private final ArrayMap<Icon, Drawable> mMap = new ArrayMap<>();

View File

@@ -0,0 +1,488 @@
/*
* 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.
*/
package com.android.settings.dashboard;
import android.annotation.IntDef;
import android.support.v7.util.DiffUtil;
import android.text.TextUtils;
import com.android.settings.dashboard.conditional.Condition;
import com.android.settingslib.drawer.DashboardCategory;
import com.android.settingslib.drawer.Tile;
import com.android.settings.R;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
/**
* Description about data list used in the DashboardAdapter. In the data list each item can be
* Condition, suggestion or category tile.
* <p>
* ItemsData has inner class Item, which represents the Item in data list.
*/
public class DashboardData {
public static final int SUGGESTION_MODE_DEFAULT = 0;
public static final int SUGGESTION_MODE_COLLAPSED = 1;
public static final int SUGGESTION_MODE_EXPANDED = 2;
public static final int POSITION_NOT_FOUND = -1;
public static final int DEFAULT_SUGGESTION_COUNT = 2;
// id namespace for different type of items.
private static final int NS_SPACER = 0;
private static final int NS_SUGGESTION = 1000;
private static final int NS_ITEMS = 2000;
private static final int NS_CONDITION = 3000;
private final List<Item> mItems;
private final List<DashboardCategory> mCategories;
private final List<Condition> mConditions;
private final List<Tile> mSuggestions;
private final int mSuggestionMode;
private final Condition mExpandedCondition;
private int mId;
private DashboardData(Builder builder) {
mCategories = builder.mCategories;
mConditions = builder.mConditions;
mSuggestions = builder.mSuggestions;
mSuggestionMode = builder.mSuggestionMode;
mExpandedCondition = builder.mExpandedCondition;
mItems = new ArrayList<>();
mId = 0;
buildItemsData();
}
public int getItemIdByPosition(int position) {
return mItems.get(position).id;
}
public int getItemTypeByPosition(int position) {
return mItems.get(position).type;
}
public Object getItemEntityByPosition(int position) {
return mItems.get(position).entity;
}
public List<Item> getItemList() {
return mItems;
}
public int size() {
return mItems.size();
}
public Object getItemEntityById(long id) {
for (final Item item : mItems) {
if (item.id == id) {
return item.entity;
}
}
return null;
}
public List<DashboardCategory> getCategories() {
return mCategories;
}
public List<Condition> getConditions() {
return mConditions;
}
public List<Tile> getSuggestions() {
return mSuggestions;
}
public int getSuggestionMode() {
return mSuggestionMode;
}
public Condition getExpandedCondition() {
return mExpandedCondition;
}
/**
* Find the position of the object in mItems list, using the equals method to compare
*
* @param entity the object that need to be found in list
* @return position of the object, return POSITION_NOT_FOUND if object isn't in the list
*/
public int getPositionByEntity(Object entity) {
if (entity == null) return POSITION_NOT_FOUND;
final int size = mItems.size();
for (int i = 0; i < size; i++) {
final Object item = mItems.get(i).entity;
if (entity.equals(item)) {
return i;
}
}
return POSITION_NOT_FOUND;
}
/**
* Find the position of the Tile object.
* <p>
* First, try to find the exact identical instance of the tile object, if not found,
* then try to find a tile has the same title.
*
* @param tile tile that need to be found
* @return position of the object, return INDEX_NOT_FOUND if object isn't in the list
*/
public int getPositionByTile(Tile tile) {
final int size = mItems.size();
for (int i = 0; i < size; i++) {
final Object entity = mItems.get(i).entity;
if (entity == tile) {
return i;
} else if (entity instanceof Tile && tile.title.equals(((Tile) entity).title)) {
return i;
}
}
return POSITION_NOT_FOUND;
}
/**
* Get the count of suggestions to display
*
* The displayable count mainly depends on the {@link #mSuggestionMode}
* and the size of suggestions list.
*
* When in default mode, displayable count couldn't larger than
* {@link #DEFAULT_SUGGESTION_COUNT}.
*
* When in expanded mode, display all the suggestions.
* @return the count of suggestions to display
*/
public int getDisplayableSuggestionCount() {
final int suggestionSize = mSuggestions.size();
return mSuggestionMode == SUGGESTION_MODE_DEFAULT
? Math.min(DEFAULT_SUGGESTION_COUNT, suggestionSize)
: mSuggestionMode == SUGGESTION_MODE_EXPANDED
? suggestionSize : 0;
}
public boolean hasMoreSuggestions() {
return mSuggestionMode == SUGGESTION_MODE_COLLAPSED
|| (mSuggestionMode == SUGGESTION_MODE_DEFAULT
&& mSuggestions.size() > DEFAULT_SUGGESTION_COUNT);
}
private void resetCount() {
mId = 0;
}
/**
* Count the item and add it into list when {@paramref add} is true.
*
* Note that {@link #mId} will increment automatically and the real
* id stored in {@link Item} is shifted by {@paramref nameSpace}. This is a
* simple way to keep the id stable.
*
* @param object maybe {@link Condition}, {@link Tile}, {@link DashboardCategory} or null
* @param type type of the item, and value is the layout id
* @param add flag about whether to add item into list
* @param nameSpace namespace based on the type
*/
private void countItem(Object object, int type, boolean add, int nameSpace) {
if (add) {
mItems.add(new Item(object, type, mId + nameSpace, object == mExpandedCondition));
}
mId++;
}
/**
* Build the mItems list using mConditions, mSuggestions, mCategories data
* and mIsShowingAll, mSuggestionMode flag.
*/
private void buildItemsData() {
boolean hasConditions = false;
for (int i = 0; mConditions != null && i < mConditions.size(); i++) {
boolean shouldShow = mConditions.get(i).shouldShow();
hasConditions |= shouldShow;
countItem(mConditions.get(i), R.layout.condition_card, shouldShow, NS_CONDITION);
}
resetCount();
final boolean hasSuggestions = mSuggestions != null && mSuggestions.size() != 0;
countItem(null, R.layout.dashboard_spacer, hasConditions && hasSuggestions, NS_SPACER);
countItem(buildSuggestionHeaderData(), R.layout.suggestion_header, hasSuggestions,
NS_SPACER);
resetCount();
if (mSuggestions != null) {
int maxSuggestions = getDisplayableSuggestionCount();
for (int i = 0; i < mSuggestions.size(); i++) {
countItem(mSuggestions.get(i), R.layout.suggestion_tile, i < maxSuggestions,
NS_SUGGESTION);
}
}
resetCount();
for (int i = 0; mCategories != null && i < mCategories.size(); i++) {
DashboardCategory category = mCategories.get(i);
countItem(category, R.layout.dashboard_category,
!TextUtils.isEmpty(category.title), NS_ITEMS);
for (int j = 0; j < category.tiles.size(); j++) {
Tile tile = category.tiles.get(j);
countItem(tile, R.layout.dashboard_tile, true, NS_ITEMS);
}
}
}
private SuggestionHeaderData buildSuggestionHeaderData() {
SuggestionHeaderData data;
if (mSuggestions == null) {
data = new SuggestionHeaderData();
} else {
final boolean hasMoreSuggestions = hasMoreSuggestions();
final int suggestionSize = mSuggestions.size();
final int undisplayedSuggestionCount = suggestionSize - getDisplayableSuggestionCount();
data = new SuggestionHeaderData(hasMoreSuggestions, suggestionSize,
undisplayedSuggestionCount);
}
return data;
}
/**
* Builder used to build the ItemsData
* <p>
* {@link #mExpandedCondition} and {@link #mSuggestionMode} have default value
* while others are not.
*/
public static class Builder {
private int mSuggestionMode = SUGGESTION_MODE_DEFAULT;
private Condition mExpandedCondition = null;
private List<DashboardCategory> mCategories;
private List<Condition> mConditions;
private List<Tile> mSuggestions;
public Builder() {
}
public Builder(DashboardData dashboardData) {
mCategories = dashboardData.mCategories;
mConditions = dashboardData.mConditions;
mSuggestions = dashboardData.mSuggestions;
mSuggestionMode = dashboardData.mSuggestionMode;
mExpandedCondition = dashboardData.mExpandedCondition;
}
public Builder setCategories(List<DashboardCategory> categories) {
this.mCategories = categories;
return this;
}
public Builder setConditions(List<Condition> conditions) {
this.mConditions = conditions;
return this;
}
public Builder setSuggestions(List<Tile> suggestions) {
this.mSuggestions = suggestions;
return this;
}
public Builder setSuggestionMode(int suggestionMode) {
this.mSuggestionMode = suggestionMode;
return this;
}
public Builder setExpandedCondition(Condition expandedCondition) {
this.mExpandedCondition = expandedCondition;
return this;
}
public DashboardData build() {
return new DashboardData(this);
}
}
/**
* A DiffCallback to calculate the difference between old and new Item
* List in DashboardData
*/
public static class ItemsDataDiffCallback extends DiffUtil.Callback {
final private List<Item> mOldItems;
final private List<Item> mNewItems;
public ItemsDataDiffCallback(List<Item> oldItems, List<Item> newItems) {
mOldItems = oldItems;
mNewItems = newItems;
}
@Override
public int getOldListSize() {
return mOldItems.size();
}
@Override
public int getNewListSize() {
return mNewItems.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return mOldItems.get(oldItemPosition).id == mNewItems.get(newItemPosition).id;
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return mOldItems.get(oldItemPosition).equals(mNewItems.get(newItemPosition));
}
}
/**
* An item contains the data needed in the DashboardData.
*/
private static class Item {
// valid types in field type
private static final int TYPE_DASHBOARD_CATEGORY = R.layout.dashboard_category;
private static final int TYPE_DASHBOARD_TILE = R.layout.dashboard_tile;
private static final int TYPE_SUGGESTION_HEADER = R.layout.suggestion_header;
private static final int TYPE_SUGGESTION_TILE = R.layout.suggestion_tile;
private static final int TYPE_CONDITION_CARD = R.layout.condition_card;
private static final int TYPE_DASHBOARD_SPACER = R.layout.dashboard_spacer;
@IntDef({TYPE_DASHBOARD_CATEGORY, TYPE_DASHBOARD_TILE, TYPE_SUGGESTION_HEADER,
TYPE_SUGGESTION_TILE, TYPE_CONDITION_CARD, TYPE_DASHBOARD_SPACER})
@Retention(RetentionPolicy.SOURCE)
public @interface ItemTypes{}
/**
* The main data object in item, usually is a {@link Tile}, {@link Condition} or
* {@link DashboardCategory} object. This object can also be null when the
* item is an divider line. Please refer to {@link #buildItemsData()} for
* detail usage of the Item.
*/
public final Object entity;
/**
* The type of item, value inside is the layout id(e.g. R.layout.dashboard_tile)
*/
public final @ItemTypes int type;
/**
* Id of this item, used in the {@link ItemsDataDiffCallback} to identify the same item.
*/
public final int id;
/**
* To store whether the condition is expanded, useless when {@link #type} is not
* {@link #TYPE_CONDITION_CARD}
*/
public final boolean conditionExpanded;
public Item(Object entity, @ItemTypes int type, int id, boolean conditionExpanded) {
this.entity = entity;
this.type = type;
this.id = id;
this.conditionExpanded = conditionExpanded;
}
/**
* Override it to make comparision in the {@link ItemsDataDiffCallback}
* @param obj object to compared with
* @return true if the same object or has equal value.
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Item)) {
return false;
}
final Item targetItem = (Item) obj;
if (type != targetItem.type || id != targetItem.id) {
return false;
}
switch (type) {
case TYPE_DASHBOARD_CATEGORY:
// Only check title for dashboard category
return TextUtils.equals(((DashboardCategory)entity).title,
((DashboardCategory) targetItem.entity).title);
case TYPE_DASHBOARD_TILE:
final Tile localTile = (Tile)entity;
final Tile targetTile = (Tile)targetItem.entity;
// Only check title and summary for dashboard tile
return TextUtils.equals(localTile.title, targetTile.title)
&& TextUtils.equals(localTile.summary, targetTile.summary);
case TYPE_CONDITION_CARD:
// First check conditionExpanded for quick return
if (conditionExpanded != targetItem.conditionExpanded) {
return false;
}
// After that, go to default to do final check
default:
return entity == null ? targetItem.entity == null
: entity.equals(targetItem.entity);
}
}
}
/**
* This class contains the data needed to build the header. The data can also be
* used to check the diff in DiffUtil.Callback
*/
public static class SuggestionHeaderData {
public final boolean hasMoreSuggestions;
public final int suggestionSize;
public final int undisplayedSuggestionCount;
public SuggestionHeaderData(boolean moreSuggestions, int suggestionSize, int
undisplayedSuggestionCount) {
this.hasMoreSuggestions = moreSuggestions;
this.suggestionSize = suggestionSize;
this.undisplayedSuggestionCount = undisplayedSuggestionCount;
}
public SuggestionHeaderData() {
hasMoreSuggestions = false;
suggestionSize = 0;
undisplayedSuggestionCount = 0;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof SuggestionHeaderData)) {
return false;
}
SuggestionHeaderData targetData = (SuggestionHeaderData) obj;
return hasMoreSuggestions == targetData.hasMoreSuggestions
&& suggestionSize == targetData.suggestionSize
&& undisplayedSuggestionCount == targetData.undisplayedSuggestionCount;
}
}
}