Merge "Modifying SuggestionParser to support dismiss logic of smart suggestions."
This commit is contained in:
committed by
Android (Google) Code Review
commit
e622690efa
@@ -94,16 +94,27 @@ public class SuggestionParser {
|
||||
|
||||
private static final long MILLIS_IN_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
// Default dismiss control for smart suggestions.
|
||||
private static final String DEFAULT_SMART_DISMISS_CONTROL = "0,10";
|
||||
|
||||
private final Context mContext;
|
||||
private final List<SuggestionCategory> mSuggestionList;
|
||||
private final ArrayMap<Pair<String, String>, Tile> mAddCache = new ArrayMap<>();
|
||||
private final SharedPreferences mSharedPrefs;
|
||||
private final String mSmartDismissControl;
|
||||
|
||||
public SuggestionParser(Context context, SharedPreferences sharedPrefs, int orderXml) {
|
||||
|
||||
public SuggestionParser(
|
||||
Context context, SharedPreferences sharedPrefs, int orderXml, String smartDismissControl) {
|
||||
mContext = context;
|
||||
mSuggestionList = (List<SuggestionCategory>) new SuggestionOrderInflater(mContext)
|
||||
.parse(orderXml);
|
||||
mSharedPrefs = sharedPrefs;
|
||||
mSmartDismissControl = smartDismissControl;
|
||||
}
|
||||
|
||||
public SuggestionParser(Context context, SharedPreferences sharedPrefs, int orderXml) {
|
||||
this(context, sharedPrefs, orderXml, DEFAULT_SMART_DISMISS_CONTROL);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@@ -111,26 +122,35 @@ public class SuggestionParser {
|
||||
mContext = context;
|
||||
mSuggestionList = new ArrayList<SuggestionCategory>();
|
||||
mSharedPrefs = sharedPrefs;
|
||||
mSmartDismissControl = DEFAULT_SMART_DISMISS_CONTROL;
|
||||
Log.wtf(TAG, "Only use this constructor for testing");
|
||||
}
|
||||
|
||||
public List<Tile> getSuggestions() {
|
||||
return getSuggestions(false);
|
||||
}
|
||||
|
||||
public List<Tile> getSuggestions(boolean isSmartSuggestionEnabled) {
|
||||
List<Tile> suggestions = new ArrayList<>();
|
||||
final int N = mSuggestionList.size();
|
||||
for (int i = 0; i < N; i++) {
|
||||
readSuggestions(mSuggestionList.get(i), suggestions);
|
||||
readSuggestions(mSuggestionList.get(i), suggestions, isSmartSuggestionEnabled);
|
||||
}
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
public boolean dismissSuggestion(Tile suggestion) {
|
||||
return dismissSuggestion(suggestion, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismisses a suggestion, returns true if the suggestion has no more dismisses left and should
|
||||
* be disabled.
|
||||
*/
|
||||
public boolean dismissSuggestion(Tile suggestion) {
|
||||
public boolean dismissSuggestion(Tile suggestion, boolean isSmartSuggestionEnabled) {
|
||||
String keyBase = suggestion.intent.getComponent().flattenToShortString();
|
||||
int index = mSharedPrefs.getInt(keyBase + DISMISS_INDEX, 0);
|
||||
String dismissControl = suggestion.metaData.getString(META_DATA_DISMISS_CONTROL);
|
||||
String dismissControl = getDismissControl(suggestion, isSmartSuggestionEnabled);
|
||||
if (dismissControl == null || parseDismissString(dismissControl).length == index) {
|
||||
return true;
|
||||
}
|
||||
@@ -141,20 +161,23 @@ public class SuggestionParser {
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void filterSuggestions(List<Tile> suggestions, int countBefore) {
|
||||
public void filterSuggestions(
|
||||
List<Tile> suggestions, int countBefore, boolean isSmartSuggestionEnabled) {
|
||||
for (int i = countBefore; i < suggestions.size(); i++) {
|
||||
if (!isAvailable(suggestions.get(i)) ||
|
||||
!isSupported(suggestions.get(i)) ||
|
||||
!satisifesRequiredUserType(suggestions.get(i)) ||
|
||||
!satisfiesRequiredAccount(suggestions.get(i)) ||
|
||||
!satisfiesConnectivity(suggestions.get(i)) ||
|
||||
isDismissed(suggestions.get(i))) {
|
||||
isDismissed(suggestions.get(i), isSmartSuggestionEnabled)) {
|
||||
suggestions.remove(i--);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void readSuggestions(SuggestionCategory category, List<Tile> suggestions) {
|
||||
@VisibleForTesting
|
||||
void readSuggestions(
|
||||
SuggestionCategory category, List<Tile> suggestions, boolean isSmartSuggestionEnabled) {
|
||||
int countBefore = suggestions.size();
|
||||
Intent intent = new Intent(Intent.ACTION_MAIN);
|
||||
intent.addCategory(category.category);
|
||||
@@ -163,7 +186,7 @@ public class SuggestionParser {
|
||||
}
|
||||
TileUtils.getTilesForIntent(mContext, new UserHandle(UserHandle.myUserId()), intent,
|
||||
mAddCache, null, suggestions, true, false);
|
||||
filterSuggestions(suggestions, countBefore);
|
||||
filterSuggestions(suggestions, countBefore, isSmartSuggestionEnabled);
|
||||
if (!category.multiple && suggestions.size() > (countBefore + 1)) {
|
||||
// If there are too many, remove them all and only re-add the one with the highest
|
||||
// priority.
|
||||
@@ -288,12 +311,11 @@ public class SuggestionParser {
|
||||
Settings.Secure.putInt(mContext.getContentResolver(), name, 1);
|
||||
}
|
||||
|
||||
private boolean isDismissed(Tile suggestion) {
|
||||
Object dismissObj = suggestion.metaData.get(META_DATA_DISMISS_CONTROL);
|
||||
if (dismissObj == null) {
|
||||
private boolean isDismissed(Tile suggestion, boolean isSmartSuggestionEnabled) {
|
||||
String dismissControl = getDismissControl(suggestion, isSmartSuggestionEnabled);
|
||||
if (dismissControl == null) {
|
||||
return false;
|
||||
}
|
||||
String dismissControl = String.valueOf(dismissObj);
|
||||
String keyBase = suggestion.intent.getComponent().flattenToShortString();
|
||||
if (!mSharedPrefs.contains(keyBase + SETUP_TIME)) {
|
||||
mSharedPrefs.edit()
|
||||
@@ -333,7 +355,16 @@ public class SuggestionParser {
|
||||
return dismisses;
|
||||
}
|
||||
|
||||
private static class SuggestionCategory {
|
||||
private String getDismissControl(Tile suggestion, boolean isSmartSuggestionEnabled) {
|
||||
if (isSmartSuggestionEnabled) {
|
||||
return mSmartDismissControl;
|
||||
} else {
|
||||
return suggestion.metaData.getString(META_DATA_DISMISS_CONTROL);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static class SuggestionCategory {
|
||||
public String category;
|
||||
public String pkg;
|
||||
public boolean multiple;
|
||||
|
||||
@@ -43,6 +43,12 @@ LOCAL_AAPT_FLAGS := --auto-add-overlay \
|
||||
LOCAL_SRC_FILES := \
|
||||
$(call all-java-files-under, src)
|
||||
|
||||
LOCAL_JAR_EXCLUDE_FILES := none
|
||||
|
||||
LOCAL_RESOURCE_DIR := \
|
||||
$(LOCAL_PATH)/res
|
||||
|
||||
|
||||
include frameworks/base/packages/SettingsLib/common.mk
|
||||
|
||||
include $(BUILD_PACKAGE)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
|
||||
<optional-steps>
|
||||
<step category="com.android.settings.suggested.category.LOCK_SCREEN" />
|
||||
<step category="com.android.settings.suggested.category.EMAIL" />
|
||||
<step category="com.android.settings.suggested.category.PARTNER_ACCOUNT"
|
||||
multiple="true" />
|
||||
<step category="com.android.settings.suggested.category.HOTWORD" />
|
||||
<step category="com.android.settings.suggested.category.DEFAULT"
|
||||
multiple="true" />
|
||||
<step category="com.android.settings.suggested.category.SETTINGS_ONLY"
|
||||
multiple="true" />
|
||||
</optional-steps>
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.settingslib;
|
||||
|
||||
import org.junit.runners.model.InitializationError;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.manifest.AndroidManifest;
|
||||
import org.robolectric.res.Fs;
|
||||
import org.robolectric.res.ResourcePath;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SettingLibRobolectricTestRunner extends RobolectricTestRunner {
|
||||
|
||||
public SettingLibRobolectricTestRunner(Class<?> testClass) throws InitializationError {
|
||||
super(testClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AndroidManifest getAppManifest(Config config) {
|
||||
// Using the manifest file's relative path, we can figure out the application directory.
|
||||
final String appRoot = "frameworks/base/packages/SettingsLib";
|
||||
final String manifestPath = appRoot + "/AndroidManifest.xml";
|
||||
final String resDir = appRoot + "/tests/robotests/res";
|
||||
final String assetsDir = appRoot + config.assetDir();
|
||||
|
||||
final AndroidManifest manifest = new AndroidManifest(Fs.fileFromPath(manifestPath),
|
||||
Fs.fileFromPath(resDir), Fs.fileFromPath(assetsDir));
|
||||
|
||||
manifest.setPackageName("com.android.settingslib");
|
||||
return manifest;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* 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.settingslib;
|
||||
|
||||
import android.util.Log;
|
||||
import android.content.Context;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
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 com.android.settingslib.drawer.Tile;
|
||||
import com.android.settingslib.drawer.TileUtilsTest;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
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.spy;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
|
||||
@RunWith(SettingLibRobolectricTestRunner.class)
|
||||
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
|
||||
public class SuggestionParserTest {
|
||||
|
||||
@Mock
|
||||
private PackageManager mPackageManager;
|
||||
private Context mContext;
|
||||
private SuggestionParser mSuggestionParser;
|
||||
private SuggestionParser.SuggestionCategory mSuggestioCategory;
|
||||
private List<Tile> mSuggestionsBeforeDismiss;
|
||||
private List<Tile> mSuggestionsAfterDismiss;
|
||||
private SharedPreferences mPrefs;
|
||||
private Tile mSuggestion;
|
||||
private List<ResolveInfo> mInfo;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mContext = spy(RuntimeEnvironment.application);
|
||||
when(mContext.getPackageManager()).thenReturn(mPackageManager);
|
||||
mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext);
|
||||
mSuggestion = new Tile();
|
||||
mSuggestion.intent = new Intent("action");
|
||||
mSuggestion.intent.setComponent(new ComponentName("pkg", "cls"));
|
||||
mSuggestion.metaData = new Bundle();
|
||||
mSuggestionParser = new SuggestionParser(
|
||||
mContext, mPrefs, R.xml.suggestion_ordering, "0,0");
|
||||
mSuggestioCategory = new SuggestionParser.SuggestionCategory();
|
||||
mSuggestioCategory.category = "category1";
|
||||
mSuggestioCategory.multiple = true;
|
||||
mInfo = new ArrayList<>();
|
||||
ResolveInfo info1 = TileUtilsTest.newInfo(true, "category1");
|
||||
info1.activityInfo.packageName = "pkg";
|
||||
ResolveInfo info2 = TileUtilsTest.newInfo(true, "category1");
|
||||
info2.activityInfo.packageName = "pkg2";
|
||||
mInfo.add(info1);
|
||||
mInfo.add(info2);
|
||||
when(mPackageManager.queryIntentActivitiesAsUser(
|
||||
any(Intent.class), anyInt(), anyInt())).thenReturn(mInfo);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDismissSuggestion_withoutSmartSuggestion() {
|
||||
assertThat(mSuggestionParser.dismissSuggestion(mSuggestion, false)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDismissSuggestion_withSmartSuggestion() {
|
||||
assertThat(mSuggestionParser.dismissSuggestion(mSuggestion, true)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSuggestions_withoutSmartSuggestions() {
|
||||
readAndDismissSuggestion(false);
|
||||
mSuggestionParser.readSuggestions(mSuggestioCategory, mSuggestionsAfterDismiss, false);
|
||||
assertThat(mSuggestionsBeforeDismiss.size()).isEqualTo(2);
|
||||
assertThat(mSuggestionsAfterDismiss.size()).isEqualTo(1);
|
||||
assertThat(mSuggestionsBeforeDismiss.get(1)).isEqualTo(mSuggestionsAfterDismiss.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSuggestions_withSmartSuggestions() {
|
||||
readAndDismissSuggestion(true);
|
||||
assertThat(mSuggestionsBeforeDismiss.size()).isEqualTo(2);
|
||||
assertThat(mSuggestionsAfterDismiss.size()).isEqualTo(2);
|
||||
assertThat(mSuggestionsBeforeDismiss).isEqualTo(mSuggestionsAfterDismiss);
|
||||
}
|
||||
|
||||
private void readAndDismissSuggestion(boolean isSmartSuggestionEnabled) {
|
||||
mSuggestionsBeforeDismiss = new ArrayList<Tile>();
|
||||
mSuggestionsAfterDismiss = new ArrayList<Tile>();
|
||||
mSuggestionParser.readSuggestions(
|
||||
mSuggestioCategory, mSuggestionsBeforeDismiss, isSmartSuggestionEnabled);
|
||||
if (mSuggestionParser.dismissSuggestion(
|
||||
mSuggestionsBeforeDismiss.get(0), isSmartSuggestionEnabled)) {
|
||||
mInfo.remove(0);
|
||||
}
|
||||
mSuggestionParser.readSuggestions(
|
||||
mSuggestioCategory, mSuggestionsAfterDismiss, isSmartSuggestionEnabled);
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ import android.util.Pair;
|
||||
|
||||
import com.android.settingslib.SuggestionParser;
|
||||
import com.android.settingslib.TestConfig;
|
||||
import com.android.settingslib.drawer.TileUtilsTest;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
|
||||
import org.junit.Before;
|
||||
@@ -179,7 +180,7 @@ public class TileUtilsTest {
|
||||
|
||||
assertThat(outTiles.size()).isEqualTo(1);
|
||||
SuggestionParser parser = new SuggestionParser(mContext, null);
|
||||
parser.filterSuggestions(outTiles, 0);
|
||||
parser.filterSuggestions(outTiles, 0, false);
|
||||
assertThat(outTiles.size()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@@ -303,16 +304,16 @@ public class TileUtilsTest {
|
||||
assertThat(outTiles.get(0).summary).isEqualTo("dynamic-summary");
|
||||
}
|
||||
|
||||
private ResolveInfo newInfo(boolean systemApp, String category) {
|
||||
public static ResolveInfo newInfo(boolean systemApp, String category) {
|
||||
return newInfo(systemApp, category, null);
|
||||
}
|
||||
|
||||
private ResolveInfo newInfo(boolean systemApp, String category, String keyHint) {
|
||||
private static ResolveInfo newInfo(boolean systemApp, String category, String keyHint) {
|
||||
return newInfo(systemApp, category, keyHint, null, null);
|
||||
}
|
||||
|
||||
private ResolveInfo newInfo(boolean systemApp, String category, String keyHint, String iconUri,
|
||||
String summaryUri) {
|
||||
private static ResolveInfo newInfo(boolean systemApp, String category, String keyHint,
|
||||
String iconUri, String summaryUri) {
|
||||
ResolveInfo info = new ResolveInfo();
|
||||
info.system = systemApp;
|
||||
info.activityInfo = new ActivityInfo();
|
||||
|
||||
Reference in New Issue
Block a user