diff --git a/src/com/android/settings/core/instrumentation/MetricsFeatureProviderImpl.java b/src/com/android/settings/core/instrumentation/MetricsFeatureProviderImpl.java index b74c8863085..091eb54f2d9 100644 --- a/src/com/android/settings/core/instrumentation/MetricsFeatureProviderImpl.java +++ b/src/com/android/settings/core/instrumentation/MetricsFeatureProviderImpl.java @@ -34,6 +34,7 @@ public class MetricsFeatureProviderImpl implements MetricsFeatureProvider { protected void installLogWriters() { mLoggerWriters.add(new EventLogWriter()); + mLoggerWriters.add(new SettingSuggestionsLogWriter()); } @Override diff --git a/src/com/android/settings/core/instrumentation/SettingSuggestionsLogWriter.java b/src/com/android/settings/core/instrumentation/SettingSuggestionsLogWriter.java new file mode 100644 index 00000000000..4c277fb9da3 --- /dev/null +++ b/src/com/android/settings/core/instrumentation/SettingSuggestionsLogWriter.java @@ -0,0 +1,77 @@ +/* + * 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.core.instrumentation; + +import android.content.Context; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.internal.logging.MetricsLogger; +import com.android.settings.suggestions.EventStore; + +/** + * {@link LogWriter} that writes setting suggestion related logs. + */ +public class SettingSuggestionsLogWriter implements LogWriter { + + private EventStore mEventStore; + + @Override + public void visible(Context context, int category) { + } + + @Override + public void hidden(Context context, int category) { + } + + @Override + public void action(Context context, int category) { + } + + @Override + public void action(Context context, int category, int value) { + } + + @Override + public void action(Context context, int category, boolean value) { + } + + @Override + public void action(Context context, int category, String pkg) { + if (mEventStore == null) { + mEventStore = new EventStore(context); + } + switch (category) { + case MetricsEvent.ACTION_SHOW_SETTINGS_SUGGESTION: + mEventStore.writeEvent(pkg, EventStore.EVENT_SHOWN); + break; + case MetricsEvent.ACTION_SETTINGS_DISMISS_SUGGESTION: + mEventStore.writeEvent(pkg, EventStore.EVENT_DISMISSED); + break; + case MetricsEvent.ACTION_SETTINGS_SUGGESTION: + mEventStore.writeEvent(pkg, EventStore.EVENT_CLICKED); + break; + } + } + + @Override + public void count(Context context, String name, int value) { + } + + @Override + public void histogram(Context context, String name, int bucket) { + } + +} diff --git a/src/com/android/settings/dashboard/DashboardSummary.java b/src/com/android/settings/dashboard/DashboardSummary.java index 9d64d4c8ade..fddf2d77586 100644 --- a/src/com/android/settings/dashboard/DashboardSummary.java +++ b/src/com/android/settings/dashboard/DashboardSummary.java @@ -36,6 +36,9 @@ import com.android.settings.dashboard.conditional.ConditionAdapterUtils; import com.android.settings.dashboard.conditional.ConditionManager; import com.android.settings.dashboard.conditional.FocusRecyclerView; import com.android.settings.overlay.FeatureFactory; +import com.android.settings.suggestions.EventStore; +import com.android.settings.suggestions.SuggestionFeaturizer; +import com.android.settings.suggestions.SuggestionRanker; import com.android.settingslib.SuggestionParser; import com.android.settingslib.drawer.CategoryKey; import com.android.settingslib.drawer.DashboardCategory; @@ -66,11 +69,13 @@ public class DashboardSummary extends InstrumentedFragment private SummaryLoader mSummaryLoader; private ConditionManager mConditionManager; private SuggestionParser mSuggestionParser; + private SuggestionRanker mSuggestionRanker; private LinearLayoutManager mLayoutManager; private SuggestionsChecks mSuggestionsChecks; private ArrayList mSuggestionsShownLogged; private ArrayList mSuggestionsHiddenLogged; private DashboardFeatureProvider mDashboardFeatureProvider; + private SuggestionFeatureProvider mSuggestionFeatureProvider; @Override public int getMetricsCategory() { @@ -84,6 +89,8 @@ public class DashboardSummary extends InstrumentedFragment final Activity activity = getActivity(); mDashboardFeatureProvider = FeatureFactory.getFactory(activity) .getDashboardFeatureProvider(activity); + mSuggestionFeatureProvider = FeatureFactory.getFactory(activity) + .getSuggestionFeatureProvider(); if (mDashboardFeatureProvider.isEnabled()) { mSummaryLoader = new SummaryLoader(activity, CategoryKey.CATEGORY_HOMEPAGE); @@ -95,6 +102,8 @@ public class DashboardSummary extends InstrumentedFragment mConditionManager = ConditionManager.get(activity, false); mSuggestionParser = new SuggestionParser(activity, activity.getSharedPreferences(SUGGESTIONS, 0), R.xml.suggestion_ordering); + mSuggestionRanker = new SuggestionRanker( + new SuggestionFeaturizer(new EventStore(activity))); mSuggestionsChecks = new SuggestionsChecks(getContext()); if (savedInstanceState == null) { mSuggestionsShownLogged = new ArrayList<>(); @@ -260,6 +269,16 @@ public class DashboardSummary extends InstrumentedFragment protected List doInBackground(Void... params) { final Context context = getContext(); List suggestions = mSuggestionParser.getSuggestions(); + if (mSuggestionFeatureProvider.isSmartSuggestionEnabled(context)) { + List suggestionIds = new ArrayList<>(suggestions.size()); + for (Tile suggestion : suggestions) { + suggestionIds.add( + DashboardAdapter.getSuggestionIdentifier(context, suggestion)); + } + // TODO: create a Suggestion class to maintain the id and other info + mSuggestionRanker.rank(suggestions, suggestionIds); + // TODO: consider showing only top-k (e.g., top-3) + } for (int i = 0; i < suggestions.size(); i++) { Tile suggestion = suggestions.get(i); if (mSuggestionsChecks.isSuggestionComplete(suggestion)) { diff --git a/src/com/android/settings/suggestions/EventStore.java b/src/com/android/settings/suggestions/EventStore.java new file mode 100644 index 00000000000..bf94f48bdda --- /dev/null +++ b/src/com/android/settings/suggestions/EventStore.java @@ -0,0 +1,102 @@ +/* + * 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.suggestions; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * Stores suggestion related statistics. + */ +public class EventStore { + + public static final String TAG = "SuggestionEventStore"; + + public static final String EVENT_SHOWN = "shown"; + public static final String EVENT_DISMISSED = "dismissed"; + public static final String EVENT_CLICKED = "clicked"; + public static final String METRIC_LAST_EVENT_TIME = "last_event_time"; + public static final String METRIC_COUNT = "count"; + + private static final Set EVENTS = new HashSet( + Arrays.asList(new String[] {EVENT_SHOWN, EVENT_DISMISSED, EVENT_CLICKED})); + private static final Set METRICS = new HashSet( + Arrays.asList(new String[] {METRIC_LAST_EVENT_TIME, METRIC_COUNT})); + + private final SharedPreferences mSharedPrefs; + + public EventStore(Context context) { + mSharedPrefs = context.getSharedPreferences(TAG, Context.MODE_PRIVATE); + } + + /** + * Writes individual log events. + * @param pkgName: Package for which this event is reported. + * @param eventType: Type of event (one of {@link #EVENTS}). + */ + public void writeEvent(String pkgName, String eventType) { + if (!EVENTS.contains(eventType)) { + Log.w(TAG, "Reported event type " + eventType + " is not a valid type!"); + return; + } + final String lastTimePrefKey = getPrefKey(pkgName, eventType, METRIC_LAST_EVENT_TIME); + final String countPrefKey = getPrefKey(pkgName, eventType, METRIC_COUNT); + writePref(lastTimePrefKey, System.currentTimeMillis()); + writePref(countPrefKey, readPref(countPrefKey, (long) 0) + 1); + } + + /** + * Reads metric of the the reported events (e.g., counts). + * @param pkgName: Package for which this metric is queried. + * @param eventType: Type of event (one of {@link #EVENTS}). + * @param metricType: Type of the queried metric (one of {@link #METRICS}). + * @return the corresponding metric. + */ + public long readMetric(String pkgName, String eventType, String metricType) { + if (!EVENTS.contains(eventType)) { + Log.w(TAG, "Reported event type " + eventType + " is not a valid event!"); + return 0; + } else if (!METRICS.contains(metricType)) { + Log.w(TAG, "Required stat type + " + metricType + " is not a valid stat!"); + return 0; + } + return readPref(getPrefKey(pkgName, eventType, metricType), (long) 0); + } + + private void writePref(String prefKey, long value) { + mSharedPrefs.edit().putLong(prefKey, value).commit(); + } + + private long readPref(String prefKey, Long defaultValue) { + return mSharedPrefs.getLong(prefKey, defaultValue); + } + + private String getPrefKey(String pkgName, String eventType, String statType) { + return new StringBuilder() + .append("setting_suggestion_") + .append(pkgName) + .append("_") + .append(eventType) + .append("_") + .append(statType) + .toString(); + } +} diff --git a/src/com/android/settings/suggestions/SuggestionFeaturizer.java b/src/com/android/settings/suggestions/SuggestionFeaturizer.java new file mode 100644 index 00000000000..eb43272003d --- /dev/null +++ b/src/com/android/settings/suggestions/SuggestionFeaturizer.java @@ -0,0 +1,106 @@ +/* + * 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.suggestions; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Creates a set of interaction features (i.e., metrics) to represent each setting suggestion. These + * features currently include normalized time from previous events (shown, dismissed and clicked) + * for any particular suggestion and also counts of these events. These features are used as signals + * to find the best ranking for suggestion items. + */ +public class SuggestionFeaturizer { + + // Key of the features used for ranking. + public static final String FEATURE_IS_SHOWN = "is_shown"; + public static final String FEATURE_IS_DISMISSED = "is_dismissed"; + public static final String FEATURE_IS_CLICKED = "is_clicked"; + public static final String FEATURE_TIME_FROM_LAST_SHOWN = "time_from_last_shown"; + public static final String FEATURE_TIME_FROM_LAST_DISMISSED = "time_from_last_dismissed"; + public static final String FEATURE_TIME_FROM_LAST_CLICKED = "time_from_last_clicked"; + public static final String FEATURE_SHOWN_COUNT = "shown_count"; + public static final String FEATURE_DISMISSED_COUNT = "dismissed_count"; + public static final String FEATURE_CLICKED_COUNT = "clicked_count"; + + // The following numbers are estimated from histograms. + public static final double TIME_NORMALIZATION_FACTOR = 2e10; + public static final double COUNT_NORMALIZATION_FACTOR = 500; + + private final EventStore mEventStore; + + /** + * Constructor + * + * @param eventStore An instance of {@code EventStore} which maintains the recorded suggestion + * events. + */ + public SuggestionFeaturizer(EventStore eventStore) { + mEventStore = eventStore; + } + + /** + * Extracts the features for each package name. + * + * @param pkgNames: List of package names for which features are queried. + * @return A Map containing the features, keyed by the package names. Each map value contains + * another map with key-value pairs of the features. + */ + public Map> featurize(List pkgNames) { + Map> features = new HashMap<>(); + Long curTimeMs = System.currentTimeMillis(); + for (String pkgName : pkgNames) { + Map featureMap = new HashMap<>(); + features.put(pkgName, featureMap); + Long lastShownTime = mEventStore + .readMetric(pkgName, EventStore.EVENT_SHOWN, EventStore.METRIC_LAST_EVENT_TIME); + Long lastDismissedTime = mEventStore.readMetric(pkgName, EventStore.EVENT_DISMISSED, + EventStore.METRIC_LAST_EVENT_TIME); + Long lastClickedTime = mEventStore.readMetric(pkgName, EventStore.EVENT_CLICKED, + EventStore.METRIC_LAST_EVENT_TIME); + featureMap.put(FEATURE_IS_SHOWN, booleanToDouble(lastShownTime > 0)); + featureMap.put(FEATURE_IS_DISMISSED, booleanToDouble(lastDismissedTime > 0)); + featureMap.put(FEATURE_IS_CLICKED, booleanToDouble(lastClickedTime > 0)); + featureMap.put(FEATURE_TIME_FROM_LAST_SHOWN, + normalizedTimeDiff(curTimeMs, lastShownTime)); + featureMap.put(FEATURE_TIME_FROM_LAST_DISMISSED, + normalizedTimeDiff(curTimeMs, lastDismissedTime)); + featureMap.put(FEATURE_TIME_FROM_LAST_CLICKED, + normalizedTimeDiff(curTimeMs, lastClickedTime)); + featureMap.put(FEATURE_SHOWN_COUNT, normalizedCount(mEventStore + .readMetric(pkgName, EventStore.EVENT_SHOWN, EventStore.METRIC_COUNT))); + featureMap.put(FEATURE_DISMISSED_COUNT, normalizedCount(mEventStore + .readMetric(pkgName, EventStore.EVENT_DISMISSED, EventStore.METRIC_COUNT))); + featureMap.put(FEATURE_CLICKED_COUNT, normalizedCount(mEventStore + .readMetric(pkgName, EventStore.EVENT_CLICKED, EventStore.METRIC_COUNT))); + } + return features; + } + + private static double booleanToDouble(boolean bool) { + return bool ? 1 : 0; + } + + private static double normalizedTimeDiff(long curTimeMs, long preTimeMs) { + return Math.min(1, (curTimeMs - preTimeMs) / TIME_NORMALIZATION_FACTOR); + } + + private static double normalizedCount(long count) { + return Math.min(1, count / COUNT_NORMALIZATION_FACTOR); + } +} diff --git a/src/com/android/settings/suggestions/SuggestionRanker.java b/src/com/android/settings/suggestions/SuggestionRanker.java new file mode 100644 index 00000000000..d0b914088f7 --- /dev/null +++ b/src/com/android/settings/suggestions/SuggestionRanker.java @@ -0,0 +1,83 @@ +/* + * 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.suggestions; + +import com.android.settingslib.drawer.Tile; + +import android.support.annotation.VisibleForTesting; + +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class SuggestionRanker { + + private static final String TAG = "SuggestionRanker"; + + // The following coefficients form a linear model, which mixes the features to obtain a + // relevance metric for ranking the suggestion items. This model is learned with off-line data + // by training a binary classifier to detect the clicked items. The higher the obtained + // relevance metric, the higher chance of getting clicked. + private static final Map WEIGHTS = new HashMap() {{ + put(SuggestionFeaturizer.FEATURE_IS_SHOWN, 4.07506758256); + put(SuggestionFeaturizer.FEATURE_IS_DISMISSED, 2.11535473578); + put(SuggestionFeaturizer.FEATURE_IS_CLICKED, 1.21885461304); + put(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_SHOWN, 3.18832024515); + put(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_DISMISSED, 1.09902706645); + put(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_CLICKED, 0.262631082877); + put(SuggestionFeaturizer.FEATURE_SHOWN_COUNT, -0.918484103748 * 240); + }}; + + private final SuggestionFeaturizer mSuggestionFeaturizer; + + private final Map relevanceMetrics; + + Comparator suggestionComparator = new Comparator() { + @Override + public int compare(Tile suggestion1, Tile suggestion2) { + return relevanceMetrics.get(suggestion1) < relevanceMetrics.get(suggestion2) ? 1 : -1; + } + }; + + public SuggestionRanker(SuggestionFeaturizer suggestionFeaturizer) { + mSuggestionFeaturizer = suggestionFeaturizer; + relevanceMetrics = new HashMap(); + } + + public void rank(final List suggestions, List suggestionIds) { + relevanceMetrics.clear(); + Map> features = mSuggestionFeaturizer.featurize(suggestionIds); + for (int i = 0; i < suggestionIds.size(); i++) { + relevanceMetrics.put(suggestions.get(i), + getRelevanceMetric(features.get(suggestionIds.get(i)))); + } + Collections.sort(suggestions, suggestionComparator); + } + + @VisibleForTesting + double getRelevanceMetric(Map features) { + double sum = 0; + if (features == null) { + return sum; + } + for (String feature : WEIGHTS.keySet()) { + sum += WEIGHTS.get(feature) * features.get(feature); + } + return sum; + } +} diff --git a/tests/robotests/src/com/android/settings/suggestions/EventStoreTest.java b/tests/robotests/src/com/android/settings/suggestions/EventStoreTest.java new file mode 100644 index 00000000000..9db11b8e375 --- /dev/null +++ b/tests/robotests/src/com/android/settings/suggestions/EventStoreTest.java @@ -0,0 +1,72 @@ +/* + * 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.suggestions; + +import com.android.settings.SettingsRobolectricTestRunner; +import com.android.settings.TestConfig; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +import static com.google.common.truth.Truth.assertThat; + +import org.robolectric.RuntimeEnvironment; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class EventStoreTest { + + private EventStore mEventStore; + + @Before + public void setUp() { + mEventStore = new EventStore(RuntimeEnvironment.application); + } + + @Test + public void testWriteRead() { + mEventStore.writeEvent("pkg", EventStore.EVENT_SHOWN); + long timeMs = System.currentTimeMillis(); + assertThat(mEventStore.readMetric("pkg", EventStore.EVENT_SHOWN, EventStore.METRIC_COUNT)) + .isEqualTo(1); + assertThat(Math.abs(timeMs - mEventStore + .readMetric("pkg", EventStore.EVENT_SHOWN, EventStore.METRIC_LAST_EVENT_TIME)) < 10000) + .isTrue(); + } + + @Test + public void testWriteRead_shouldHaveLatestValues() { + mEventStore.writeEvent("pkg", EventStore.EVENT_DISMISSED); + mEventStore.writeEvent("pkg", EventStore.EVENT_DISMISSED); + assertThat( + mEventStore.readMetric("pkg", EventStore.EVENT_DISMISSED, EventStore.METRIC_COUNT)) + .isEqualTo(2); + } + + @Test + public void testWriteRead_shouldReturnDefaultIfNotAvailable() { + assertThat(mEventStore.readMetric("pkg", EventStore.EVENT_SHOWN, EventStore.METRIC_COUNT)) + .isEqualTo(0); + assertThat( + mEventStore + .readMetric("pkg", EventStore.EVENT_SHOWN, EventStore.METRIC_LAST_EVENT_TIME)) + .isEqualTo(0); + } + +} diff --git a/tests/robotests/src/com/android/settings/suggestions/SuggestionFeaturizerTest.java b/tests/robotests/src/com/android/settings/suggestions/SuggestionFeaturizerTest.java new file mode 100644 index 00000000000..3c5bd4f70ca --- /dev/null +++ b/tests/robotests/src/com/android/settings/suggestions/SuggestionFeaturizerTest.java @@ -0,0 +1,109 @@ +/* + * 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.suggestions; + +import java.util.Arrays; +import java.util.Map; + +import com.android.settings.SettingsRobolectricTestRunner; +import com.android.settings.TestConfig; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +import static com.google.common.truth.Truth.assertThat; + +import org.robolectric.RuntimeEnvironment; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class SuggestionFeaturizerTest { + + private EventStore mEventStore; + private SuggestionFeaturizer mSuggestionFeaturizer; + + @Before + public void setUp() { + mEventStore = new EventStore(RuntimeEnvironment.application); + mSuggestionFeaturizer = new SuggestionFeaturizer(mEventStore); + } + + @Test + public void testFeaturize_singlePackage() { + mEventStore.writeEvent("pkg", EventStore.EVENT_DISMISSED); + mEventStore.writeEvent("pkg", EventStore.EVENT_SHOWN); + mEventStore.writeEvent("pkg", EventStore.EVENT_SHOWN); + Map features = mSuggestionFeaturizer.featurize(Arrays.asList("pkg")) + .get("pkg"); + assertThat(features.get(SuggestionFeaturizer.FEATURE_IS_SHOWN)).isEqualTo(1.0); + assertThat(features.get(SuggestionFeaturizer.FEATURE_IS_DISMISSED)).isEqualTo(1.0); + assertThat(features.get(SuggestionFeaturizer.FEATURE_IS_CLICKED)).isEqualTo(0.0); + + assertThat(features.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_SHOWN)).isLessThan(1.0); + assertThat(features.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_DISMISSED)) + .isLessThan(1.0); + assertThat(features.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_CLICKED)) + .isEqualTo(1.0); + assertThat(features.get(SuggestionFeaturizer.FEATURE_SHOWN_COUNT)) + .isEqualTo(2.0 / SuggestionFeaturizer.COUNT_NORMALIZATION_FACTOR); + assertThat(features.get(SuggestionFeaturizer.FEATURE_DISMISSED_COUNT)) + .isEqualTo(1.0 / SuggestionFeaturizer.COUNT_NORMALIZATION_FACTOR); + assertThat(features.get(SuggestionFeaturizer.FEATURE_CLICKED_COUNT)).isEqualTo(0.0); + } + + @Test + public void testFeaturize_multiplePackages() { + mEventStore.writeEvent("pkg1", EventStore.EVENT_DISMISSED); + mEventStore.writeEvent("pkg2", EventStore.EVENT_SHOWN); + mEventStore.writeEvent("pkg1", EventStore.EVENT_SHOWN); + Map> features = mSuggestionFeaturizer + .featurize(Arrays.asList("pkg1", "pkg2")); + Map features1 = features.get("pkg1"); + Map features2 = features.get("pkg2"); + + assertThat(features1.get(SuggestionFeaturizer.FEATURE_IS_SHOWN)).isEqualTo(1.0); + assertThat(features1.get(SuggestionFeaturizer.FEATURE_IS_DISMISSED)).isEqualTo(1.0); + assertThat(features1.get(SuggestionFeaturizer.FEATURE_IS_CLICKED)).isEqualTo(0.0); + assertThat(features1.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_SHOWN)) + .isLessThan(1.0); + assertThat(features1.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_DISMISSED)) + .isLessThan(1.0); + assertThat(features1.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_CLICKED)) + .isEqualTo(1.0); + assertThat(features1.get(SuggestionFeaturizer.FEATURE_SHOWN_COUNT)) + .isEqualTo(1.0 / SuggestionFeaturizer.COUNT_NORMALIZATION_FACTOR); + assertThat(features1.get(SuggestionFeaturizer.FEATURE_DISMISSED_COUNT)) + .isEqualTo(1.0 / SuggestionFeaturizer.COUNT_NORMALIZATION_FACTOR); + assertThat(features1.get(SuggestionFeaturizer.FEATURE_CLICKED_COUNT)).isEqualTo(0.0); + + assertThat(features2.get(SuggestionFeaturizer.FEATURE_IS_SHOWN)).isEqualTo(1.0); + assertThat(features2.get(SuggestionFeaturizer.FEATURE_IS_DISMISSED)).isEqualTo(0.0); + assertThat(features2.get(SuggestionFeaturizer.FEATURE_IS_CLICKED)).isEqualTo(0.0); + assertThat(features2.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_SHOWN)) + .isLessThan(1.0); + assertThat(features2.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_DISMISSED)) + .isEqualTo(1.0); + assertThat(features2.get(SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_CLICKED)) + .isEqualTo(1.0); + assertThat(features2.get(SuggestionFeaturizer.FEATURE_SHOWN_COUNT)) + .isEqualTo(1.0 / SuggestionFeaturizer.COUNT_NORMALIZATION_FACTOR); + assertThat(features2.get(SuggestionFeaturizer.FEATURE_DISMISSED_COUNT)).isEqualTo(0.0); + assertThat(features2.get(SuggestionFeaturizer.FEATURE_CLICKED_COUNT)).isEqualTo(0.0); + } +} diff --git a/tests/robotests/src/com/android/settings/suggestions/SuggestionRankerTest.java b/tests/robotests/src/com/android/settings/suggestions/SuggestionRankerTest.java new file mode 100644 index 00000000000..e1807a89a02 --- /dev/null +++ b/tests/robotests/src/com/android/settings/suggestions/SuggestionRankerTest.java @@ -0,0 +1,94 @@ +/* + * 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.suggestions; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.android.settings.SettingsRobolectricTestRunner; +import com.android.settings.TestConfig; +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.annotation.Config; + +import static com.google.common.truth.Truth.assertThat; + +import org.robolectric.RuntimeEnvironment; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.same; +import static org.mockito.Mockito.spy; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class SuggestionRankerTest { + + @Mock + private SuggestionRanker mSuggestionRanker; + @Mock + private SuggestionFeaturizer mSuggestionFeaturizer; + private Map> mFeatures; + private List mPkgNames; + private List mSuggestions; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mPkgNames = Arrays.asList("pkg1", "pkg2", "pkg3"); + mFeatures = new HashMap>(); + mFeatures.put("pkg1", new HashMap()); + mFeatures.put("pkg2", new HashMap()); + mFeatures.put("pkg3", new HashMap()); + mSuggestions = new ArrayList() { + { + add(new Tile()); + add(new Tile()); + add(new Tile()); + } + }; + mSuggestionFeaturizer = mock(SuggestionFeaturizer.class); + mSuggestionRanker = new SuggestionRanker(mSuggestionFeaturizer); + when(mSuggestionFeaturizer.featurize(mPkgNames)).thenReturn(mFeatures); + mSuggestionRanker = spy(mSuggestionRanker); + when(mSuggestionRanker.getRelevanceMetric(same(mFeatures.get("pkg1")))).thenReturn(0.9); + when(mSuggestionRanker.getRelevanceMetric(same(mFeatures.get("pkg2")))).thenReturn(0.1); + when(mSuggestionRanker.getRelevanceMetric(same(mFeatures.get("pkg3")))).thenReturn(0.5); + } + + @Test + public void testRank() { + List expectedOrderdList = new ArrayList() { + { + add(mSuggestions.get(0)); // relevance = 0.9 + add(mSuggestions.get(2)); // relevance = 0.5 + add(mSuggestions.get(1)); // relevance = 0.1 + } + }; + mSuggestionRanker.rank(mSuggestions, mPkgNames); + assertThat(mSuggestions).isEqualTo(expectedOrderdList); + } +} +