1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.intelligence.suggestions.ranking;
18 
19 import android.content.Context;
20 import android.service.settings.suggestions.Suggestion;
21 import androidx.annotation.VisibleForTesting;
22 
23 import com.android.settings.intelligence.overlay.FeatureFactory;
24 
25 import java.util.ArrayList;
26 import java.util.Collections;
27 import java.util.Comparator;
28 import java.util.HashMap;
29 import java.util.List;
30 import java.util.Map;
31 
32 /**
33  * Copied from packages/apps/Settings/src/.../dashboard/suggestions/SuggestionRanker
34  */
35 public class SuggestionRanker {
36 
37     private static final String TAG = "SuggestionRanker";
38 
39     // The following coefficients form a linear model, which mixes the features to obtain a
40     // relevance metric for ranking the suggestion items. This model is learned with off-line data
41     // by training a binary classifier to detect the clicked items. The higher the obtained
42     // relevance metric, the higher chance of getting clicked.
43     private static final Map<String, Double> WEIGHTS = Map.of(
44             SuggestionFeaturizer.FEATURE_IS_SHOWN, 5.05140842519,
45             SuggestionFeaturizer.FEATURE_IS_DISMISSED, 2.29641455171,
46             SuggestionFeaturizer.FEATURE_IS_CLICKED, -2.98812233623,
47             SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_SHOWN, 5.02807250202,
48             SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_DISMISSED, 2.49589700842,
49             SuggestionFeaturizer.FEATURE_TIME_FROM_LAST_CLICKED, -4.3377039948,
50             SuggestionFeaturizer.FEATURE_SHOWN_COUNT, -2.35993512546);
51 
52     private final long mMaxSuggestionsDisplayCount;
53     private final SuggestionFeaturizer mSuggestionFeaturizer;
54     private final Map<Suggestion, Double> mRelevanceMetrics;
55 
56     Comparator<Suggestion> suggestionComparator = new Comparator<Suggestion>() {
57         @Override
58         public int compare(Suggestion suggestion1, Suggestion suggestion2) {
59             return mRelevanceMetrics.get(suggestion1) < mRelevanceMetrics.get(suggestion2) ? 1 : -1;
60         }
61     };
62 
SuggestionRanker(Context context, SuggestionFeaturizer suggestionFeaturizer)63     public SuggestionRanker(Context context, SuggestionFeaturizer suggestionFeaturizer) {
64         mSuggestionFeaturizer = suggestionFeaturizer;
65         mRelevanceMetrics = new HashMap<>();
66         mMaxSuggestionsDisplayCount = FeatureFactory.get(context).experimentFeatureProvider()
67                 .getMaxSuggestionDisplayCount(context);
68     }
69 
70     /**
71      * Filter out suggestions that are not relevant at the moment, and rank the rest.
72      *
73      * @return a list of suggestion ranked by relevance.
74      */
rankRelevantSuggestions(List<Suggestion> suggestions)75     public List<Suggestion> rankRelevantSuggestions(List<Suggestion> suggestions) {
76         mRelevanceMetrics.clear();
77         Map<String, Map<String, Double>> features = mSuggestionFeaturizer.featurize(suggestions);
78         for (Suggestion suggestion : suggestions) {
79             mRelevanceMetrics.put(suggestion, getRelevanceMetric(features.get(suggestion.getId())));
80         }
81         final List<Suggestion> rankedSuggestions = new ArrayList<>();
82         rankedSuggestions.addAll(suggestions);
83         Collections.sort(rankedSuggestions, suggestionComparator);
84 
85         if (rankedSuggestions.size() < mMaxSuggestionsDisplayCount) {
86             return rankedSuggestions;
87         } else {
88             final List<Suggestion> relevantSuggestions = new ArrayList<>();
89             for (int i = 0; i < mMaxSuggestionsDisplayCount; i++) {
90                 relevantSuggestions.add(rankedSuggestions.get(i));
91             }
92             return relevantSuggestions;
93         }
94     }
95 
96     @VisibleForTesting
getRelevanceMetric(Map<String, Double> features)97     double getRelevanceMetric(Map<String, Double> features) {
98         double sum = 0;
99         if (features == null) {
100             return sum;
101         }
102         for (String feature : WEIGHTS.keySet()) {
103             sum += WEIGHTS.get(feature) * features.get(feature);
104         }
105         return sum;
106     }
107 }