diff --git a/res/layout/see_all.xml b/res/layout/see_all.xml deleted file mode 100644 index 44b263ce7c3..00000000000 --- a/res/layout/see_all.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/res/values/strings.xml b/res/values/strings.xml index 8e6336f6954..a908c7af557 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -7281,11 +7281,6 @@ Tue 6:03PM - - See all - - See less - Disconnected diff --git a/src/com/android/settings/dashboard/DashboardAdapter.java b/src/com/android/settings/dashboard/DashboardAdapter.java index f501dfadf2a..9ea48b06525 100644 --- a/src/com/android/settings/dashboard/DashboardAdapter.java +++ b/src/com/android/settings/dashboard/DashboardAdapter.java @@ -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 - 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 mItems = new ArrayList<>(); - private final List mTypes = new ArrayList<>(); - private final List mIds = new ArrayList<>(); private final IconCache mCache; - private final Context mContext; private final MetricsFeatureProvider mMetricsFeatureProvider; - - private List mCategories; - private List mConditions; - private List 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 conditions) { + List suggestions = null; + List 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 getSuggestions() { - return mSuggestions; + return mDashboardData.getSuggestions(); } public void setCategoriesAndSuggestions(List categories, List 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 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 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 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 suggestions = mDashboardData.getSuggestions(); + final List categories = mDashboardData.getCategories(); + if (suggestions != null) { outState.putParcelableArrayList(STATE_SUGGESTION_LIST, - new ArrayList(mSuggestions)); + new ArrayList(suggestions)); } - if (mCategories != null) { + if (categories != null) { outState.putParcelableArrayList(STATE_CATEGORY_LIST, - new ArrayList(mCategories)); + new ArrayList(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 mMap = new ArrayMap<>(); diff --git a/src/com/android/settings/dashboard/DashboardData.java b/src/com/android/settings/dashboard/DashboardData.java new file mode 100644 index 00000000000..0fc994b268a --- /dev/null +++ b/src/com/android/settings/dashboard/DashboardData.java @@ -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. + *

+ * 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 mItems; + private final List mCategories; + private final List mConditions; + private final List 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 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 getCategories() { + return mCategories; + } + + public List getConditions() { + return mConditions; + } + + public List 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. + *

+ * 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 + *

+ * {@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 mCategories; + private List mConditions; + private List 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 categories) { + this.mCategories = categories; + return this; + } + + public Builder setConditions(List conditions) { + this.mConditions = conditions; + return this; + } + + public Builder setSuggestions(List 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 mOldItems; + final private List mNewItems; + + public ItemsDataDiffCallback(List oldItems, List 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; + } + } + +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/dashboard/DashboardDataTest.java b/tests/robotests/src/com/android/settings/dashboard/DashboardDataTest.java new file mode 100644 index 00000000000..52c2103cd9d --- /dev/null +++ b/tests/robotests/src/com/android/settings/dashboard/DashboardDataTest.java @@ -0,0 +1,336 @@ +/* + * 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.support.annotation.NonNull; +import android.support.v7.util.DiffUtil; +import android.support.v7.util.ListUpdateCallback; +import com.android.settings.TestConfig; +import com.android.settings.dashboard.conditional.AirplaneModeCondition; +import com.android.settings.dashboard.conditional.Condition; +import com.android.settingslib.drawer.DashboardCategory; +import com.android.settingslib.drawer.Tile; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +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.mock; +import static org.mockito.Mockito.when; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class DashboardDataTest { + private static final String TEST_SUGGESTION_TITLE = "Use fingerprint"; + private static final String TEST_CATEGORY_TILE_TITLE = "Display"; + + private DashboardData mDashboardDataWithOneConditions; + private DashboardData mDashboardDataWithTwoConditions; + private DashboardData mDashboardDataWithNoItems; + @Mock + private Tile mTestCategoryTile; + @Mock + private Tile mTestSuggestion; + @Mock + private DashboardCategory mDashboardCategory; + @Mock + private Condition mTestCondition; + @Mock + private Condition mSecondCondition; // condition used to test insert in DiffUtil + + @Before + public void SetUp() { + MockitoAnnotations.initMocks(this); + + // Build suggestions + final List suggestions = new ArrayList<>(); + mTestSuggestion.title = TEST_SUGGESTION_TITLE; + suggestions.add(mTestSuggestion); + + // Build oneItemConditions + final List oneItemConditions = new ArrayList<>(); + when(mTestCondition.shouldShow()).thenReturn(true); + oneItemConditions.add(mTestCondition); + + // Build twoItemConditions + final List twoItemsConditions = new ArrayList<>(); + when(mSecondCondition.shouldShow()).thenReturn(true); + twoItemsConditions.add(mTestCondition); + twoItemsConditions.add(mSecondCondition); + + // Build categories + final List categories = new ArrayList<>(); + mTestCategoryTile.title = TEST_CATEGORY_TILE_TITLE; + mDashboardCategory.title = "test"; + mDashboardCategory.tiles = new ArrayList<>(); + mDashboardCategory.tiles.add(mTestCategoryTile); + categories.add(mDashboardCategory); + + // Build DashboardData + mDashboardDataWithOneConditions = new DashboardData.Builder() + .setConditions(oneItemConditions) + .setCategories(categories) + .setSuggestions(suggestions) + .build(); + + mDashboardDataWithTwoConditions = new DashboardData.Builder() + .setConditions(twoItemsConditions) + .setCategories(categories) + .setSuggestions(suggestions) + .build(); + + mDashboardDataWithNoItems = new DashboardData.Builder() + .setConditions(null) + .setCategories(null) + .setSuggestions(null) + .build(); + } + + @Test + public void testBuildItemsData_containsAllData() { + final DashboardData.SuggestionHeaderData data = + new DashboardData.SuggestionHeaderData(false, 1, 0); + final Object[] expectedObjects = {mTestCondition, null, data, mTestSuggestion, + mDashboardCategory, mTestCategoryTile}; + final int expectedSize = expectedObjects.length; + + assertThat(mDashboardDataWithOneConditions.getItemList().size()) + .isEqualTo(expectedSize); + for (int i = 0; i < expectedSize; i++) { + if (mDashboardDataWithOneConditions.getItemEntityByPosition(i) + instanceof DashboardData.SuggestionHeaderData) { + // SuggestionHeaderData is created inside when build, we can only use isEqualTo + assertThat(mDashboardDataWithOneConditions.getItemEntityByPosition(i)) + .isEqualTo(expectedObjects[i]); + } else { + assertThat(mDashboardDataWithOneConditions.getItemEntityByPosition(i)) + .isSameAs(expectedObjects[i]); + } + } + } + + @Test + public void testGetPositionByEntity_selfInstance_returnPositionFound() { + final int position = mDashboardDataWithOneConditions + .getPositionByEntity(mTestCondition); + assertThat(position).isNotEqualTo(DashboardData.POSITION_NOT_FOUND); + } + + @Test + public void testGetPositionByEntity_notExisted_returnNotFound() { + final Condition condition = mock(AirplaneModeCondition.class); + final int position = mDashboardDataWithOneConditions.getPositionByEntity(condition); + assertThat(position).isEqualTo(DashboardData.POSITION_NOT_FOUND); + } + + @Test + public void testGetPositionByTile_selfInstance_returnPositionFound() { + final int position = mDashboardDataWithOneConditions + .getPositionByTile(mTestCategoryTile); + assertThat(position).isNotEqualTo(DashboardData.POSITION_NOT_FOUND); + } + + @Test + public void testGetPositionByTile_equalTitle_returnPositionFound() { + final Tile tile = mock(Tile.class); + tile.title = TEST_CATEGORY_TILE_TITLE; + final int position = mDashboardDataWithOneConditions.getPositionByTile(tile); + assertThat(position).isNotEqualTo(DashboardData.POSITION_NOT_FOUND); + } + + @Test + public void testGetPositionByTile_notExisted_returnNotFound() { + final Tile tile = mock(Tile.class); + tile.title = ""; + final int position = mDashboardDataWithOneConditions.getPositionByTile(tile); + assertThat(position).isEqualTo(DashboardData.POSITION_NOT_FOUND); + } + + @Test + public void testDiffUtil_DataEqual_noResultData() { + List testResultData = new ArrayList<>(); + testDiffUtil(mDashboardDataWithOneConditions, + mDashboardDataWithOneConditions, testResultData); + } + + @Test + public void testDiffUtil_InsertOneCondition_ResultDataOneInserted() { + //Build testResultData + final List testResultData = new ArrayList<>(); + testResultData.add(new ListUpdateResult.ResultData( + ListUpdateResult.ResultData.TYPE_OPERATION_INSERT, 1, 1)); + + testDiffUtil(mDashboardDataWithOneConditions, + mDashboardDataWithTwoConditions, testResultData); + } + + @Test + public void testDiffUtil_DeleteAllData_ResultDataOneDeleted() { + //Build testResultData + final List testResultData = new ArrayList<>(); + testResultData.add(new ListUpdateResult.ResultData( + ListUpdateResult.ResultData.TYPE_OPERATION_REMOVE, 0, 6)); + + testDiffUtil(mDashboardDataWithOneConditions, mDashboardDataWithNoItems, testResultData); + } + + /** + * Test when using the + * {@link com.android.settings.dashboard.DashboardData.ItemsDataDiffCallback} + * to transfer List from {@paramref baseDashboardData} to {@paramref diffDashboardData}, whether + * the transform data result is equals to {@paramref testResultData} + *

+ * The steps are described below: + * 1. Calculate a {@link android.support.v7.util.DiffUtil.DiffResult} from + * {@paramref baseDashboardData} to {@paramref diffDashboardData} + *

+ * 2. Dispatch the {@link android.support.v7.util.DiffUtil.DiffResult} calculated from step 1 + * into {@link ListUpdateResult} + *

+ * 3. Get result data(a.k.a. baseResultData) from {@link ListUpdateResult} and compare it to + * {@paramref testResultData} + *

+ * Because baseResultData and {@paramref testResultData} don't have sequence. When do the + * comparison, we will sort them first and then compare the inside data from them one by one. + * + * @param baseDashboardData + * @param diffDashboardData + * @param testResultData + */ + private void testDiffUtil(DashboardData baseDashboardData, DashboardData diffDashboardData, + List testResultData) { + final DiffUtil.DiffResult diffUtilResult = DiffUtil.calculateDiff( + new DashboardData.ItemsDataDiffCallback( + baseDashboardData.getItemList(), diffDashboardData.getItemList())); + + // Dispatch to listUpdateResult, then listUpdateResult will have result data + final ListUpdateResult listUpdateResult = new ListUpdateResult(); + diffUtilResult.dispatchUpdatesTo(listUpdateResult); + + final List baseResultData = listUpdateResult.getResultData(); + assertThat(testResultData.size()).isEqualTo(baseResultData.size()); + + // Sort them so we can compare them one by one using a for loop + Collections.sort(baseResultData); + Collections.sort(testResultData); + final int size = baseResultData.size(); + for (int i = 0; i < size; i++) { + // Refer to equals method in ResultData + assertThat(baseResultData.get(i)).isEqualTo(testResultData.get(i)); + } + } + + /** + * This class contains the result about how the changes made to convert one + * list to another list. It implements ListUpdateCallback to record the result data. + */ + private static class ListUpdateResult implements ListUpdateCallback { + final private List mResultData; + + public ListUpdateResult() { + mResultData = new ArrayList<>(); + } + + public List getResultData() { + return mResultData; + } + + @Override + public void onInserted(int position, int count) { + mResultData.add(new ResultData(ResultData.TYPE_OPERATION_INSERT, position, count)); + } + + @Override + public void onRemoved(int position, int count) { + mResultData.add(new ResultData(ResultData.TYPE_OPERATION_REMOVE, position, count)); + } + + @Override + public void onMoved(int fromPosition, int toPosition) { + mResultData.add( + new ResultData(ResultData.TYPE_OPERATION_MOVE, fromPosition, toPosition)); + } + + @Override + public void onChanged(int position, int count, Object payload) { + mResultData.add(new ResultData(ResultData.TYPE_OPERATION_CHANGE, position, count)); + } + + /** + * This class contains general type and field to record the operation data generated + * in {@link ListUpdateCallback}. Please refer to {@link ListUpdateCallback} for more info. + *

+ * The following are examples about the data stored in this class: + *

+ * "The data starts from position(arg1) with count number(arg2) is changed(operation)" + * or "The data is moved(operation) from position1(arg1) to position2(arg2)" + */ + private static class ResultData implements Comparable { + public static final int TYPE_OPERATION_INSERT = 0; + public static final int TYPE_OPERATION_REMOVE = 1; + public static final int TYPE_OPERATION_MOVE = 2; + public static final int TYPE_OPERATION_CHANGE = 3; + + public final int operation; + public final int arg1; + public final int arg2; + + public ResultData(int operation, int arg1, int arg2) { + this.operation = operation; + this.arg1 = arg1; + this.arg2 = arg2; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof ResultData)) { + return false; + } + + ResultData targetData = (ResultData) obj; + + return operation == targetData.operation && arg1 == targetData.arg1 + && arg2 == targetData.arg2; + } + + @Override + public int compareTo(@NonNull ResultData resultData) { + if (this.operation != resultData.operation) { + return operation - resultData.operation; + } + + if (arg1 != resultData.arg1) { + return arg1 - resultData.arg1; + } + + return arg2 - resultData.arg2; + } + } + } +}