Swipe to dismiss suggestions

- Move dismiss suggestion logic into feature provider
- In DashboardData, use hashcode as suggestion's stable id. This is much
  more likely to provide a truely stable id for each suggestion card.
  Eventually I want to use hash for all tiles to provide stable id.
- Add a SuggestionDismissionController to handle swipe to dismiss
  callbacks

Change-Id: If3770f07a90c5469a0b86fc28f3eb5e4c17227cd
Fix: 35159816
Test: make RunSettingsRoboTests
This commit is contained in:
Fan Zhang
2017-02-22 16:41:38 -08:00
parent 5e2545c3e6
commit a5b620e738
11 changed files with 398 additions and 100 deletions

View File

@@ -55,11 +55,4 @@
</RelativeLayout> </RelativeLayout>
<ImageView
android:id="@+id/overflow"
style="?android:attr/actionOverflowButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"/>
</LinearLayout> </LinearLayout>

View File

@@ -30,7 +30,6 @@ import android.content.pm.IPackageDataObserver;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo; import android.content.pm.ProviderInfo;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment;
import android.os.Handler; import android.os.Handler;
import android.os.Message; import android.os.Message;
import android.os.RemoteException; import android.os.RemoteException;
@@ -39,7 +38,6 @@ import android.os.storage.StorageManager;
import android.os.storage.VolumeInfo; import android.os.storage.VolumeInfo;
import android.support.v7.preference.Preference; import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceCategory; import android.support.v7.preference.PreferenceCategory;
import android.text.format.Formatter;
import android.util.Log; import android.util.Log;
import android.util.MutableInt; import android.util.MutableInt;
import android.view.View; import android.view.View;
@@ -51,8 +49,6 @@ import com.android.settings.R;
import com.android.settings.Utils; import com.android.settings.Utils;
import com.android.settings.deviceinfo.StorageWizardMoveConfirm; import com.android.settings.deviceinfo.StorageWizardMoveConfirm;
import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.applications.ApplicationsState.AppEntry;
import com.android.settingslib.applications.ApplicationsState.Callbacks; import com.android.settingslib.applications.ApplicationsState.Callbacks;
import com.android.settingslib.applications.StorageStatsSource; import com.android.settingslib.applications.StorageStatsSource;
import com.android.settingslib.applications.StorageStatsSource.AppStorageStats; import com.android.settingslib.applications.StorageStatsSource.AppStorageStats;

View File

@@ -17,21 +17,17 @@ package com.android.settings.dashboard;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon; import android.graphics.drawable.Icon;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.VisibleForTesting; import android.support.annotation.VisibleForTesting;
import android.support.v7.util.DiffUtil; import android.support.v7.util.DiffUtil;
import android.support.v7.widget.PopupMenu;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.ArrayMap; import android.util.ArrayMap;
import android.util.Log; import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView; import android.widget.ImageView;
@@ -43,18 +39,17 @@ import com.android.settings.SettingsActivity;
import com.android.settings.core.instrumentation.MetricsFeatureProvider; import com.android.settings.core.instrumentation.MetricsFeatureProvider;
import com.android.settings.dashboard.conditional.Condition; import com.android.settings.dashboard.conditional.Condition;
import com.android.settings.dashboard.conditional.ConditionAdapterUtils; import com.android.settings.dashboard.conditional.ConditionAdapterUtils;
import com.android.settings.dashboard.suggestions.SuggestionDismissController;
import com.android.settings.overlay.FeatureFactory; import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.SuggestionParser; import com.android.settingslib.SuggestionParser;
import com.android.settingslib.drawer.DashboardCategory; import com.android.settingslib.drawer.DashboardCategory;
import com.android.settingslib.drawer.Tile; import com.android.settingslib.drawer.Tile;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.DashboardItemHolder> public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.DashboardItemHolder>
implements SummaryLoader.SummaryConsumer { implements SummaryLoader.SummaryConsumer, SuggestionDismissController.Callback {
public static final String TAG = "DashboardAdapter"; public static final String TAG = "DashboardAdapter";
private static final String STATE_SUGGESTION_LIST = "suggestion_list"; private static final String STATE_SUGGESTION_LIST = "suggestion_list";
private static final String STATE_CATEGORY_LIST = "category_list"; private static final String STATE_CATEGORY_LIST = "category_list";
@@ -66,7 +61,6 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
private final MetricsFeatureProvider mMetricsFeatureProvider; private final MetricsFeatureProvider mMetricsFeatureProvider;
private final DashboardFeatureProvider mDashboardFeatureProvider; private final DashboardFeatureProvider mDashboardFeatureProvider;
private final ArrayList<String> mSuggestionsShownLogged; private final ArrayList<String> mSuggestionsShownLogged;
private SuggestionParser mSuggestionParser;
private boolean mFirstFrameDrawn; private boolean mFirstFrameDrawn;
@VisibleForTesting @VisibleForTesting
@@ -115,7 +109,6 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
mDashboardFeatureProvider = FeatureFactory.getFactory(context) mDashboardFeatureProvider = FeatureFactory.getFactory(context)
.getDashboardFeatureProvider(context); .getDashboardFeatureProvider(context);
mCache = new IconCache(context); mCache = new IconCache(context);
mSuggestionParser = parser;
setHasStableIds(true); setHasStableIds(true);
@@ -218,22 +211,6 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
} }
} }
// TODO: move this method to SuggestionParser or some other util class
public void disableSuggestion(Tile suggestion) {
if (mSuggestionParser == null) {
return;
}
boolean isSmartSuggestionEnabled = FeatureFactory.getFactory(mContext)
.getSuggestionFeatureProvider(mContext).isSmartSuggestionEnabled(mContext);
if (mSuggestionParser.dismissSuggestion(suggestion, isSmartSuggestionEnabled)) {
mContext.getPackageManager().setComponentEnabledSetting(
suggestion.intent.getComponent(),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
mSuggestionParser.markCategoryDone(suggestion.category);
}
}
@Override @Override
public DashboardItemHolder onCreateViewHolder(ViewGroup parent, int viewType) { public DashboardItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new DashboardItemHolder(LayoutInflater.from(parent.getContext()).inflate( return new DashboardItemHolder(LayoutInflater.from(parent.getContext()).inflate(
@@ -277,13 +254,6 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
((SettingsActivity) mContext).startSuggestion(suggestion.intent); ((SettingsActivity) mContext).startSuggestion(suggestion.intent);
} }
}); });
holder.itemView.findViewById(R.id.overflow).setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View v) {
showRemoveOption(v, suggestion);
}
});
break; break;
case R.layout.condition_card: case R.layout.condition_card:
final boolean isExpanded = mDashboardData.getItemEntityByPosition(position) final boolean isExpanded = mDashboardData.getItemEntityByPosition(position)
@@ -379,30 +349,24 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
notifyDashboardDataChanged(prevData); notifyDashboardDataChanged(prevData);
} }
private void showRemoveOption(View v, final Tile suggestion) { @Override
final PopupMenu popup = new PopupMenu( public Tile getSuggestionForPosition(int position) {
new ContextThemeWrapper(mContext, R.style.Theme_AppCompat_DayNight), v); return (Tile) mDashboardData.getItemEntityByPosition(position);
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);
List<Tile> suggestions = mDashboardData.getSuggestions();
suggestions.remove(suggestion);
DashboardData prevData = mDashboardData; @Override
mDashboardData = new DashboardData.Builder(prevData) public void onSuggestionDismissed(Tile suggestion) {
.setSuggestions(suggestions) final List<Tile> suggestions = mDashboardData.getSuggestions();
.build(); if (suggestions == null) {
notifyDashboardDataChanged(prevData); return;
}
suggestions.remove(suggestion);
return true; final DashboardData prevData = mDashboardData;
} mDashboardData = new DashboardData.Builder(prevData)
}); .setSuggestions(suggestions)
popup.show(); .build();
notifyDashboardDataChanged(prevData);
} }
@VisibleForTesting @VisibleForTesting
@@ -513,9 +477,9 @@ public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.Dash
public DashboardItemHolder(View itemView) { public DashboardItemHolder(View itemView) {
super(itemView); super(itemView);
icon = (ImageView) itemView.findViewById(android.R.id.icon); icon = itemView.findViewById(android.R.id.icon);
title = (TextView) itemView.findViewById(android.R.id.title); title = itemView.findViewById(android.R.id.title);
summary = (TextView) itemView.findViewById(android.R.id.summary); summary = itemView.findViewById(android.R.id.summary);
} }
} }
} }

View File

@@ -19,15 +19,17 @@ import android.annotation.IntDef;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v7.util.DiffUtil; import android.support.v7.util.DiffUtil;
import android.text.TextUtils; import android.text.TextUtils;
import com.android.settings.R;
import com.android.settings.dashboard.conditional.Condition; import com.android.settings.dashboard.conditional.Condition;
import com.android.settingslib.drawer.DashboardCategory; import com.android.settingslib.drawer.DashboardCategory;
import com.android.settingslib.drawer.Tile; import com.android.settingslib.drawer.Tile;
import com.android.settings.R;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
/** /**
* Description about data list used in the DashboardAdapter. In the data list each item can be * Description about data list used in the DashboardAdapter. In the data list each item can be
@@ -44,7 +46,6 @@ public class DashboardData {
// id namespace for different type of items. // id namespace for different type of items.
private static final int NS_SPACER = 0; 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_ITEMS = 2000;
private static final int NS_CONDITION = 3000; private static final int NS_CONDITION = 3000;
@@ -171,6 +172,7 @@ public class DashboardData {
* {@link #DEFAULT_SUGGESTION_COUNT}. * {@link #DEFAULT_SUGGESTION_COUNT}.
* *
* When in expanded mode, display all the suggestions. * When in expanded mode, display all the suggestions.
*
* @return the count of suggestions to display * @return the count of suggestions to display
*/ */
public int getDisplayableSuggestionCount() { public int getDisplayableSuggestionCount() {
@@ -178,7 +180,7 @@ public class DashboardData {
return mSuggestionMode == SUGGESTION_MODE_DEFAULT return mSuggestionMode == SUGGESTION_MODE_DEFAULT
? Math.min(DEFAULT_SUGGESTION_COUNT, suggestionSize) ? Math.min(DEFAULT_SUGGESTION_COUNT, suggestionSize)
: mSuggestionMode == SUGGESTION_MODE_EXPANDED : mSuggestionMode == SUGGESTION_MODE_EXPANDED
? suggestionSize : 0; ? suggestionSize : 0;
} }
public boolean hasMoreSuggestions() { public boolean hasMoreSuggestions() {
@@ -198,9 +200,9 @@ public class DashboardData {
* id stored in {@link Item} is shifted by {@paramref nameSpace}. This is a * id stored in {@link Item} is shifted by {@paramref nameSpace}. This is a
* simple way to keep the id stable. * simple way to keep the id stable.
* *
* @param object maybe {@link Condition}, {@link Tile}, {@link DashboardCategory} or null * @param object maybe {@link Condition}, {@link Tile}, {@link DashboardCategory} or null
* @param type type of the item, and value is the layout id * @param type type of the item, and value is the layout id
* @param add flag about whether to add item into list * @param add flag about whether to add item into list
* @param nameSpace namespace based on the type * @param nameSpace namespace based on the type
*/ */
private void countItem(Object object, int type, boolean add, int nameSpace) { private void countItem(Object object, int type, boolean add, int nameSpace) {
@@ -210,6 +212,17 @@ public class DashboardData {
mId++; mId++;
} }
/**
* A special count item method for just suggestions. Id is calculated using suggestion hash
* instead of the position of suggestion in list. This is a more stable id than countItem.
*/
private void countSuggestion(Tile tile, boolean add) {
if (add) {
mItems.add(new Item(tile, R.layout.suggestion_tile, Objects.hash(tile.title), false));
}
mId++;
}
/** /**
* Build the mItems list using mConditions, mSuggestions, mCategories data * Build the mItems list using mConditions, mSuggestions, mCategories data
* and mIsShowingAll, mSuggestionMode flag. * and mIsShowingAll, mSuggestionMode flag.
@@ -232,8 +245,7 @@ public class DashboardData {
if (mSuggestions != null) { if (mSuggestions != null) {
int maxSuggestions = getDisplayableSuggestionCount(); int maxSuggestions = getDisplayableSuggestionCount();
for (int i = 0; i < mSuggestions.size(); i++) { for (int i = 0; i < mSuggestions.size(); i++) {
countItem(mSuggestions.get(i), R.layout.suggestion_tile, i < maxSuggestions, countSuggestion(mSuggestions.get(i), i < maxSuggestions);
NS_SUGGESTION);
} }
} }
resetCount(); resetCount();
@@ -277,7 +289,6 @@ public class DashboardData {
private List<Condition> mConditions; private List<Condition> mConditions;
private List<Tile> mSuggestions; private List<Tile> mSuggestions;
public Builder() { public Builder() {
} }
@@ -359,7 +370,7 @@ public class DashboardData {
return "condition"; // return anything but null to mark the payload return "condition"; // return anything but null to mark the payload
} }
return null; return null;
} }
} }
/** /**
@@ -433,11 +444,11 @@ public class DashboardData {
switch (type) { switch (type) {
case TYPE_DASHBOARD_CATEGORY: case TYPE_DASHBOARD_CATEGORY:
// Only check title for dashboard category // Only check title for dashboard category
return TextUtils.equals(((DashboardCategory)entity).title, return TextUtils.equals(((DashboardCategory) entity).title,
((DashboardCategory) targetItem.entity).title); ((DashboardCategory) targetItem.entity).title);
case TYPE_DASHBOARD_TILE: case TYPE_DASHBOARD_TILE:
final Tile localTile = (Tile)entity; final Tile localTile = (Tile) entity;
final Tile targetTile = (Tile)targetItem.entity; final Tile targetTile = (Tile) targetItem.entity;
// Only check title and summary for dashboard tile // Only check title and summary for dashboard tile
return TextUtils.equals(localTile.title, targetTile.title) return TextUtils.equals(localTile.title, targetTile.title)

View File

@@ -35,6 +35,7 @@ import com.android.settings.dashboard.conditional.Condition;
import com.android.settings.dashboard.conditional.ConditionAdapterUtils; import com.android.settings.dashboard.conditional.ConditionAdapterUtils;
import com.android.settings.dashboard.conditional.ConditionManager; import com.android.settings.dashboard.conditional.ConditionManager;
import com.android.settings.dashboard.conditional.FocusRecyclerView; import com.android.settings.dashboard.conditional.FocusRecyclerView;
import com.android.settings.dashboard.suggestions.SuggestionDismissController;
import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider; import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider;
import com.android.settings.overlay.FeatureFactory; import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.SuggestionParser; import com.android.settingslib.SuggestionParser;
@@ -70,6 +71,7 @@ public class DashboardSummary extends InstrumentedFragment
private DashboardFeatureProvider mDashboardFeatureProvider; private DashboardFeatureProvider mDashboardFeatureProvider;
private SuggestionFeatureProvider mSuggestionFeatureProvider; private SuggestionFeatureProvider mSuggestionFeatureProvider;
private boolean isOnCategoriesChangedCalled; private boolean isOnCategoriesChangedCalled;
private SuggestionDismissController mSuggestionDismissHandler;
@Override @Override
public int getMetricsCategory() { public int getMetricsCategory() {
@@ -119,8 +121,7 @@ public class DashboardSummary extends InstrumentedFragment
} }
} }
if (DEBUG_TIMING) { if (DEBUG_TIMING) {
Log.d(TAG, "onResume took " + (System.currentTimeMillis() - startTime) Log.d(TAG, "onResume took " + (System.currentTimeMillis() - startTime) + " ms");
+ " ms");
} }
} }
@@ -177,7 +178,7 @@ public class DashboardSummary extends InstrumentedFragment
@Override @Override
public void onViewCreated(View view, Bundle bundle) { public void onViewCreated(View view, Bundle bundle) {
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
mDashboard = (FocusRecyclerView) view.findViewById(R.id.dashboard_container); mDashboard = view.findViewById(R.id.dashboard_container);
mLayoutManager = new LinearLayoutManager(getContext()); mLayoutManager = new LinearLayoutManager(getContext());
mLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); mLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
if (bundle != null) { if (bundle != null) {
@@ -192,6 +193,8 @@ public class DashboardSummary extends InstrumentedFragment
mAdapter = new DashboardAdapter(getContext(), mSuggestionParser, mMetricsFeatureProvider, mAdapter = new DashboardAdapter(getContext(), mSuggestionParser, mMetricsFeatureProvider,
bundle, mConditionManager.getConditions()); bundle, mConditionManager.getConditions());
mDashboard.setAdapter(mAdapter); mDashboard.setAdapter(mAdapter);
mSuggestionDismissHandler = new SuggestionDismissController(
getContext(), mDashboard, mSuggestionParser, mAdapter);
mDashboard.setItemAnimator(new DashboardItemAnimator()); mDashboard.setItemAnimator(new DashboardItemAnimator());
mSummaryLoader.setSummaryConsumer(mAdapter); mSummaryLoader.setSummaryConsumer(mAdapter);
ConditionAdapterUtils.addDismiss(mDashboard); ConditionAdapterUtils.addDismiss(mDashboard);
@@ -243,7 +246,7 @@ public class DashboardSummary extends InstrumentedFragment
List<String> suggestionIds = new ArrayList<>(suggestions.size()); List<String> suggestionIds = new ArrayList<>(suggestions.size());
for (Tile suggestion : suggestions) { for (Tile suggestion : suggestions) {
suggestionIds.add( suggestionIds.add(
DashboardAdapter.getSuggestionIdentifier(context, suggestion)); DashboardAdapter.getSuggestionIdentifier(context, suggestion));
} }
// TODO: create a Suggestion class to maintain the id and other info // TODO: create a Suggestion class to maintain the id and other info
mSuggestionFeatureProvider.rankSuggestions(suggestions, suggestionIds); mSuggestionFeatureProvider.rankSuggestions(suggestions, suggestionIds);
@@ -251,7 +254,8 @@ public class DashboardSummary extends InstrumentedFragment
for (int i = 0; i < suggestions.size(); i++) { for (int i = 0; i < suggestions.size(); i++) {
Tile suggestion = suggestions.get(i); Tile suggestion = suggestions.get(i);
if (mSuggestionsChecks.isSuggestionComplete(suggestion)) { if (mSuggestionsChecks.isSuggestionComplete(suggestion)) {
mAdapter.disableSuggestion(suggestion); mSuggestionFeatureProvider.dismissSuggestion(
context, mSuggestionParser, suggestion);
suggestions.remove(i--); suggestions.remove(i--);
} }
} }
@@ -277,7 +281,7 @@ public class DashboardSummary extends InstrumentedFragment
// API that takes a single category. // API that takes a single category.
List<DashboardCategory> categories = new ArrayList<>(); List<DashboardCategory> categories = new ArrayList<>();
categories.add(mDashboardFeatureProvider.getTilesForCategory( categories.add(mDashboardFeatureProvider.getTilesForCategory(
CategoryKey.CATEGORY_HOMEPAGE)); CategoryKey.CATEGORY_HOMEPAGE));
if (suggestions != null) { if (suggestions != null) {
mAdapter.setCategoriesAndSuggestions(categories, suggestions); mAdapter.setCategoriesAndSuggestions(categories, suggestions);
} else { } else {

View File

@@ -15,16 +15,11 @@
*/ */
package com.android.settings.dashboard.conditional; package com.android.settings.dashboard.conditional;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Context; import android.content.Context;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper; import android.support.v7.widget.helper.ItemTouchHelper;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewGroup.LayoutParams;
import android.widget.Button; import android.widget.Button;
import android.widget.ImageView; import android.widget.ImageView;
@@ -126,12 +121,6 @@ public class ConditionAdapterUtils {
} }
} }
private static void setHeight(View detailGroup, int height) {
final LayoutParams params = detailGroup.getLayoutParams();
params.height = height;
detailGroup.setLayoutParams(params);
}
private static void setViewVisibility(View containerView, int viewId, boolean visible) { private static void setViewVisibility(View containerView, int viewId, boolean visible) {
View view = containerView.findViewById(viewId); View view = containerView.findViewById(viewId);
if (view != null) { if (view != null) {

View File

@@ -0,0 +1,84 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.dashboard.suggestions;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.SuggestionParser;
import com.android.settingslib.drawer.Tile;
public class SuggestionDismissController extends ItemTouchHelper.SimpleCallback {
public interface Callback {
/**
* Returns suggestion tile data from the callback
*/
Tile getSuggestionForPosition(int position);
/**
* Called when a suggestion is dismissed.
*/
void onSuggestionDismissed(Tile suggestion);
}
private final Context mContext;
private final SuggestionFeatureProvider mSuggestionFeatureProvider;
private final SuggestionParser mSuggestionParser;
private final Callback mCallback;
public SuggestionDismissController(Context context, RecyclerView recyclerView,
SuggestionParser parser, Callback callback) {
super(0, ItemTouchHelper.START | ItemTouchHelper.END);
mContext = context;
mSuggestionParser = parser;
mSuggestionFeatureProvider = FeatureFactory.getFactory(context)
.getSuggestionFeatureProvider(context);
mCallback = callback;
final ItemTouchHelper itemTouchHelper = new ItemTouchHelper(this);
itemTouchHelper.attachToRecyclerView(recyclerView);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {
return true;
}
@Override
public int getSwipeDirs(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
if (viewHolder.getItemViewType() == R.layout.suggestion_tile) {
// Only return swipe direction for suggestion tiles. All other types are not swipeable.
return super.getSwipeDirs(recyclerView, viewHolder);
}
return 0;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
if (mCallback == null) {
return;
}
final Tile suggestion = mCallback.getSuggestionForPosition(viewHolder.getAdapterPosition());
mSuggestionFeatureProvider.dismissSuggestion(mContext, mSuggestionParser, suggestion);
mCallback.onSuggestionDismissed(suggestion);
}
}

View File

@@ -18,6 +18,7 @@ package com.android.settings.dashboard.suggestions;
import android.content.Context; import android.content.Context;
import com.android.settingslib.SuggestionParser;
import com.android.settingslib.drawer.Tile; import com.android.settingslib.drawer.Tile;
import java.util.List; import java.util.List;
@@ -38,8 +39,14 @@ public interface SuggestionFeatureProvider {
/** /**
* Ranks the list of suggestions in place. * Ranks the list of suggestions in place.
* @param suggestions: List of suggestion Tiles *
* @param suggestionIds: List of suggestion ids corresponding to the suggestion tiles. * @param suggestions List of suggestion Tiles
* @param suggestionIds List of suggestion ids corresponding to the suggestion tiles.
*/ */
void rankSuggestions(final List<Tile> suggestions, List<String> suggestionIds); void rankSuggestions(final List<Tile> suggestions, List<String> suggestionIds);
/**
* Dismisses a suggestion.
*/
void dismissSuggestion(Context context, SuggestionParser parser, Tile suggestion);
} }

View File

@@ -17,7 +17,13 @@
package com.android.settings.dashboard.suggestions; package com.android.settings.dashboard.suggestions;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager;
import com.android.internal.logging.nano.MetricsProto;
import com.android.settings.core.instrumentation.MetricsFeatureProvider;
import com.android.settings.dashboard.DashboardAdapter;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.SuggestionParser;
import com.android.settingslib.drawer.Tile; import com.android.settingslib.drawer.Tile;
import java.util.List; import java.util.List;
@@ -25,6 +31,7 @@ import java.util.List;
public class SuggestionFeatureProviderImpl implements SuggestionFeatureProvider { public class SuggestionFeatureProviderImpl implements SuggestionFeatureProvider {
private final SuggestionRanker mSuggestionRanker; private final SuggestionRanker mSuggestionRanker;
private final MetricsFeatureProvider mMetricsFeatureProvider;
@Override @Override
public boolean isSmartSuggestionEnabled(Context context) { public boolean isSmartSuggestionEnabled(Context context) {
@@ -45,6 +52,8 @@ public class SuggestionFeatureProviderImpl implements SuggestionFeatureProvider
public SuggestionFeatureProviderImpl(Context context) { public SuggestionFeatureProviderImpl(Context context) {
mSuggestionRanker = new SuggestionRanker( mSuggestionRanker = new SuggestionRanker(
new SuggestionFeaturizer(new EventStore(context.getApplicationContext()))); new SuggestionFeaturizer(new EventStore(context.getApplicationContext())));
mMetricsFeatureProvider = FeatureFactory.getFactory(context)
.getMetricsFeatureProvider();
} }
@Override @Override
@@ -52,4 +61,24 @@ public class SuggestionFeatureProviderImpl implements SuggestionFeatureProvider
mSuggestionRanker.rankSuggestions(suggestions, suggestionIds); mSuggestionRanker.rankSuggestions(suggestions, suggestionIds);
} }
@Override
public void dismissSuggestion(Context context, SuggestionParser parser, Tile suggestion) {
if (parser == null || suggestion == null) {
return;
}
mMetricsFeatureProvider.action(
context, MetricsProto.MetricsEvent.ACTION_SETTINGS_DISMISS_SUGGESTION,
DashboardAdapter.getSuggestionIdentifier(context, suggestion));
final boolean isSmartSuggestionEnabled = isSmartSuggestionEnabled(context);
if (!parser.dismissSuggestion(suggestion, isSmartSuggestionEnabled)) {
return;
}
context.getPackageManager().setComponentEnabledSetting(
suggestion.intent.getComponent(),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
parser.markCategoryDone(suggestion.category);
}
} }

View File

@@ -0,0 +1,107 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.dashboard.suggestions;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import com.android.settings.R;
import com.android.settings.SettingsRobolectricTestRunner;
import com.android.settings.TestConfig;
import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settingslib.SuggestionParser;
import com.android.settingslib.drawer.Tile;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
public class SuggestionDismissControllerTest {
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private Context mContext;
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private RecyclerView mRecyclerView;
@Mock
private SuggestionParser mSuggestionParser;
@Mock
private SuggestionDismissController.Callback mCallback;
private FakeFeatureFactory mFactory;
private SuggestionDismissController mController;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
FakeFeatureFactory.setupForTest(mContext);
mFactory = (FakeFeatureFactory) FakeFeatureFactory.getFactory(mContext);
when(mRecyclerView.getResources().getDimension(anyInt())).thenReturn(50F);
mController = new SuggestionDismissController(mContext, mRecyclerView,
mSuggestionParser, mCallback);
}
@Test
public void onMove_alwaysReturnTrue() {
assertThat(mController.onMove(null, null, null)).isTrue();
}
@Test
public void getSwipeDirs_isSuggestionTile_shouldReturnDirection() {
final RecyclerView.ViewHolder vh = mock(RecyclerView.ViewHolder.class);
when(vh.getItemViewType()).thenReturn(R.layout.suggestion_tile);
assertThat(mController.getSwipeDirs(mRecyclerView, vh))
.isEqualTo(ItemTouchHelper.START | ItemTouchHelper.END);
}
@Test
public void getSwipeDirs_isNotSuggestionTile_shouldReturn0() {
final RecyclerView.ViewHolder vh = mock(RecyclerView.ViewHolder.class);
when(vh.getItemViewType()).thenReturn(R.layout.condition_card);
assertThat(mController.getSwipeDirs(mRecyclerView, vh))
.isEqualTo(0);
}
@Test
public void onSwiped_shouldTriggerDismissSuggestion() {
final RecyclerView.ViewHolder vh = mock(RecyclerView.ViewHolder.class);
mController.onSwiped(vh, ItemTouchHelper.START);
verify(mFactory.suggestionsFeatureProvider).dismissSuggestion(
eq(mContext), eq(mSuggestionParser), any(Tile.class));
verify(mCallback).onSuggestionDismissed(any(Tile.class));
}
}

View File

@@ -0,0 +1,114 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.dashboard.suggestions;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import com.android.internal.logging.nano.MetricsProto;
import com.android.settings.SettingsRobolectricTestRunner;
import com.android.settings.TestConfig;
import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settingslib.SuggestionParser;
import com.android.settingslib.drawer.Tile;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
@RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
public class SuggestionFeatureProviderImplTest {
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private Context mContext;
@Mock
private SuggestionParser mSuggestionParser;
@Mock
private Tile mSuggestion;
private FakeFeatureFactory mFactory;
private SuggestionFeatureProviderImpl mProvider;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
FakeFeatureFactory.setupForTest(mContext);
mFactory = (FakeFeatureFactory) FakeFeatureFactory.getFactory(mContext);
when(mContext.getApplicationContext()).thenReturn(RuntimeEnvironment.application);
mSuggestion.intent = new Intent().setClassName("pkg", "cls");
mSuggestion.category = "category";
mProvider = new SuggestionFeatureProviderImpl(mContext);
}
@Test
public void dismissSuggestion_noParserOrSuggestion_noop() {
mProvider.dismissSuggestion(mContext, null, null);
mProvider.dismissSuggestion(mContext, mSuggestionParser, null);
mProvider.dismissSuggestion(mContext, null, mSuggestion);
verifyZeroInteractions(mFactory.metricsFeatureProvider);
}
@Test
public void dismissSuggestion_hasMoreDismissCount_shouldNotDisableComponent() {
when(mSuggestionParser.dismissSuggestion(any(Tile.class), anyBoolean()))
.thenReturn(false);
mProvider.dismissSuggestion(mContext, mSuggestionParser, mSuggestion);
verify(mFactory.metricsFeatureProvider).action(
eq(mContext),
eq(MetricsProto.MetricsEvent.ACTION_SETTINGS_DISMISS_SUGGESTION),
anyString());
verify(mContext, never()).getPackageManager();
}
@Test
public void dismissSuggestion_hasNoMoreDismissCount_shouldDisableComponent() {
when(mSuggestionParser.dismissSuggestion(any(Tile.class), anyBoolean()))
.thenReturn(true);
mProvider.dismissSuggestion(mContext, mSuggestionParser, mSuggestion);
verify(mFactory.metricsFeatureProvider).action(
eq(mContext),
eq(MetricsProto.MetricsEvent.ACTION_SETTINGS_DISMISS_SUGGESTION),
anyString());
verify(mContext.getPackageManager())
.setComponentEnabledSetting(mSuggestion.intent.getComponent(),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
verify(mSuggestionParser).markCategoryDone(mSuggestion.category);
}
}