1 /*
2  * Copyright (C) 2020 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.server.people.prediction;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.UserIdInt;
22 import android.app.usage.UsageEvents;
23 import android.util.ArrayMap;
24 import android.util.Pair;
25 import android.util.Range;
26 import android.util.Slog;
27 
28 import com.android.internal.annotations.VisibleForTesting;
29 import com.android.internal.app.ChooserActivity;
30 import com.android.server.people.data.AppUsageStatsData;
31 import com.android.server.people.data.DataManager;
32 import com.android.server.people.data.Event;
33 
34 import java.time.Duration;
35 import java.util.ArrayList;
36 import java.util.Comparator;
37 import java.util.List;
38 import java.util.Map;
39 import java.util.PriorityQueue;
40 import java.util.concurrent.TimeUnit;
41 import java.util.function.Function;
42 
43 /** Ranking scorer for Sharesheet targets. */
44 class SharesheetModelScorer {
45 
46     private static final String TAG = "SharesheetModelScorer";
47     private static final boolean DEBUG = false;
48     private static final Integer RECENCY_SCORE_COUNT = 6;
49     private static final float RECENCY_INITIAL_BASE_SCORE = 0.4F;
50     private static final float RECENCY_SCORE_INITIAL_DECAY = 0.05F;
51     private static final float RECENCY_SCORE_SUBSEQUENT_DECAY = 0.02F;
52     private static final long ONE_MONTH_WINDOW = TimeUnit.DAYS.toMillis(30);
53     private static final long FOREGROUND_APP_PROMO_TIME_WINDOW = TimeUnit.MINUTES.toMillis(10);
54     private static final float USAGE_STATS_CHOOSER_SCORE_INITIAL_DECAY = 0.9F;
55     private static final float FREQUENTLY_USED_APP_SCORE_INITIAL_DECAY = 0.3F;
56     @VisibleForTesting
57     static final float FOREGROUND_APP_WEIGHT = 0F;
58     @VisibleForTesting
59     static final String CHOOSER_ACTIVITY = ChooserActivity.class.getSimpleName();
60 
61     // Keep constructor private to avoid class being instantiated.
SharesheetModelScorer()62     private SharesheetModelScorer() {
63     }
64 
65     /**
66      * Computes each target's recency, frequency and frequency of the same {@code shareEventType}
67      * based on past sharing history. Update {@link ShareTargetPredictor.ShareTargetScore}.
68      */
computeScore(List<ShareTargetPredictor.ShareTarget> shareTargets, int shareEventType, long now)69     static void computeScore(List<ShareTargetPredictor.ShareTarget> shareTargets,
70             int shareEventType, long now) {
71         if (shareTargets.isEmpty()) {
72             return;
73         }
74         float totalFreqScore = 0f;
75         int freqScoreCount = 0;
76         float totalMimeFreqScore = 0f;
77         int mimeFreqScoreCount = 0;
78         // Top of this heap has lowest rank.
79         PriorityQueue<Pair<ShareTargetRankingScore, Range<Long>>> recencyMinHeap =
80                 new PriorityQueue<>(RECENCY_SCORE_COUNT,
81                         Comparator.comparingLong(p -> p.second.getUpper()));
82         List<ShareTargetRankingScore> scoreList = new ArrayList<>(shareTargets.size());
83         for (ShareTargetPredictor.ShareTarget target : shareTargets) {
84             ShareTargetRankingScore shareTargetScore = new ShareTargetRankingScore();
85             scoreList.add(shareTargetScore);
86             if (target.getEventHistory() == null) {
87                 continue;
88             }
89             // Counts frequency
90             List<Range<Long>> timeSlots = target.getEventHistory().getEventIndex(
91                     Event.SHARE_EVENT_TYPES).getActiveTimeSlots();
92             if (!timeSlots.isEmpty()) {
93                 for (Range<Long> timeSlot : timeSlots) {
94                     shareTargetScore.incrementFrequencyScore(
95                             getFreqDecayedOnElapsedTime(now - timeSlot.getLower()));
96                 }
97                 totalFreqScore += shareTargetScore.getFrequencyScore();
98                 freqScoreCount++;
99             }
100             // Counts frequency for sharing same mime type
101             List<Range<Long>> timeSlotsOfSameType = target.getEventHistory().getEventIndex(
102                     shareEventType).getActiveTimeSlots();
103             if (!timeSlotsOfSameType.isEmpty()) {
104                 for (Range<Long> timeSlot : timeSlotsOfSameType) {
105                     shareTargetScore.incrementMimeFrequencyScore(
106                             getFreqDecayedOnElapsedTime(now - timeSlot.getLower()));
107                 }
108                 totalMimeFreqScore += shareTargetScore.getMimeFrequencyScore();
109                 mimeFreqScoreCount++;
110             }
111             // Records most recent targets
112             Range<Long> mostRecentTimeSlot = target.getEventHistory().getEventIndex(
113                     Event.SHARE_EVENT_TYPES).getMostRecentActiveTimeSlot();
114             if (mostRecentTimeSlot == null) {
115                 continue;
116             }
117             if (recencyMinHeap.size() < RECENCY_SCORE_COUNT
118                     || mostRecentTimeSlot.getUpper() > recencyMinHeap.peek().second.getUpper()) {
119                 if (recencyMinHeap.size() == RECENCY_SCORE_COUNT) {
120                     recencyMinHeap.poll();
121                 }
122                 recencyMinHeap.offer(new Pair(shareTargetScore, mostRecentTimeSlot));
123             }
124         }
125         // Calculates recency score
126         while (!recencyMinHeap.isEmpty()) {
127             float recencyScore = RECENCY_INITIAL_BASE_SCORE;
128             if (recencyMinHeap.size() > 1) {
129                 recencyScore = RECENCY_INITIAL_BASE_SCORE - RECENCY_SCORE_INITIAL_DECAY
130                         - RECENCY_SCORE_SUBSEQUENT_DECAY * (recencyMinHeap.size() - 2);
131             }
132             recencyMinHeap.poll().first.setRecencyScore(recencyScore);
133         }
134 
135         Float avgFreq = freqScoreCount != 0 ? totalFreqScore / freqScoreCount : 0f;
136         Float avgMimeFreq = mimeFreqScoreCount != 0 ? totalMimeFreqScore / mimeFreqScoreCount : 0f;
137         for (int i = 0; i < scoreList.size(); i++) {
138             ShareTargetPredictor.ShareTarget target = shareTargets.get(i);
139             ShareTargetRankingScore targetScore = scoreList.get(i);
140             // Normalizes freq and mimeFreq score
141             targetScore.setFrequencyScore(normalizeFreqScore(
142                     avgFreq.equals(0f) ? 0f : targetScore.getFrequencyScore() / avgFreq));
143             targetScore.setMimeFrequencyScore(normalizeMimeFreqScore(avgMimeFreq.equals(0f) ? 0f
144                     : targetScore.getMimeFrequencyScore() / avgMimeFreq));
145             // Calculates total score
146             targetScore.setTotalScore(
147                     probOR(probOR(targetScore.getRecencyScore(), targetScore.getFrequencyScore()),
148                             targetScore.getMimeFrequencyScore()));
149             target.setScore(targetScore.getTotalScore());
150 
151             if (DEBUG) {
152                 Slog.d(TAG, String.format(
153                         "SharesheetModel: packageName: %s, className: %s, shortcutId: %s, "
154                                 + "recency:%.2f, freq_all:%.2f, freq_mime:%.2f, total:%.2f",
155                         target.getAppTarget().getPackageName(),
156                         target.getAppTarget().getClassName(),
157                         target.getAppTarget().getShortcutInfo() != null
158                                 ? target.getAppTarget().getShortcutInfo().getId() : null,
159                         targetScore.getRecencyScore(),
160                         targetScore.getFrequencyScore(),
161                         targetScore.getMimeFrequencyScore(),
162                         targetScore.getTotalScore()));
163             }
164         }
165     }
166 
167     /**
168      * Computes ranking score for app sharing. Update {@link ShareTargetPredictor.ShareTargetScore}.
169      */
computeScoreForAppShare(List<ShareTargetPredictor.ShareTarget> shareTargets, int shareEventType, int targetsLimit, long now, @NonNull DataManager dataManager, @UserIdInt int callingUserId)170     static void computeScoreForAppShare(List<ShareTargetPredictor.ShareTarget> shareTargets,
171             int shareEventType, int targetsLimit, long now, @NonNull DataManager dataManager,
172             @UserIdInt int callingUserId) {
173         computeScore(shareTargets, shareEventType, now);
174         postProcess(shareTargets, targetsLimit, dataManager, callingUserId);
175     }
176 
postProcess(List<ShareTargetPredictor.ShareTarget> shareTargets, int targetsLimit, @NonNull DataManager dataManager, @UserIdInt int callingUserId)177     private static void postProcess(List<ShareTargetPredictor.ShareTarget> shareTargets,
178             int targetsLimit, @NonNull DataManager dataManager, @UserIdInt int callingUserId) {
179         // Populates a map which key is package name and value is list of shareTargets descended
180         // on total score.
181         Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap = new ArrayMap<>();
182         for (ShareTargetPredictor.ShareTarget shareTarget : shareTargets) {
183             String packageName = shareTarget.getAppTarget().getPackageName();
184             shareTargetMap.computeIfAbsent(packageName, key -> new ArrayList<>());
185             List<ShareTargetPredictor.ShareTarget> targetsList = shareTargetMap.get(packageName);
186             int index = 0;
187             while (index < targetsList.size()) {
188                 if (shareTarget.getScore() > targetsList.get(index).getScore()) {
189                     break;
190                 }
191                 index++;
192             }
193             targetsList.add(index, shareTarget);
194         }
195         promoteForegroundApp(shareTargetMap, dataManager, callingUserId);
196         promoteMostChosenAndFrequentlyUsedApps(shareTargetMap, targetsLimit, dataManager,
197                 callingUserId);
198     }
199 
200     /**
201      * Promotes frequently chosen sharing apps and frequently used sharing apps as per
202      * UsageStatsManager, if recommended apps based on sharing history have not reached the limit
203      * (e.g. user did not share any content in last couple weeks)
204      */
promoteMostChosenAndFrequentlyUsedApps( Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap, int targetsLimit, @NonNull DataManager dataManager, @UserIdInt int callingUserId)205     private static void promoteMostChosenAndFrequentlyUsedApps(
206             Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap, int targetsLimit,
207             @NonNull DataManager dataManager, @UserIdInt int callingUserId) {
208         int validPredictionNum = 0;
209         float minValidScore = 1f;
210         for (List<ShareTargetPredictor.ShareTarget> targets : shareTargetMap.values()) {
211             for (ShareTargetPredictor.ShareTarget target : targets) {
212                 if (target.getScore() > 0f) {
213                     validPredictionNum++;
214                     minValidScore = Math.min(target.getScore(), minValidScore);
215                 }
216             }
217         }
218         // Skips if recommended apps based on sharing history have already reached the limit.
219         if (validPredictionNum >= targetsLimit) {
220             return;
221         }
222         long now = System.currentTimeMillis();
223         Map<String, AppUsageStatsData> appStatsMap =
224                 dataManager.queryAppUsageStats(
225                         callingUserId, now - ONE_MONTH_WINDOW, now, shareTargetMap.keySet());
226         // Promotes frequently chosen sharing apps as per UsageStatsManager.
227         minValidScore = promoteApp(shareTargetMap, appStatsMap, AppUsageStatsData::getChosenCount,
228                 USAGE_STATS_CHOOSER_SCORE_INITIAL_DECAY * minValidScore, minValidScore);
229         // Promotes frequently used sharing apps as per UsageStatsManager.
230         promoteApp(shareTargetMap, appStatsMap, AppUsageStatsData::getLaunchCount,
231                 FREQUENTLY_USED_APP_SCORE_INITIAL_DECAY * minValidScore, minValidScore);
232     }
233 
promoteApp( Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap, Map<String, AppUsageStatsData> appStatsMap, Function<AppUsageStatsData, Integer> countFunc, float baseScore, float minValidScore)234     private static float promoteApp(
235             Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap,
236             Map<String, AppUsageStatsData> appStatsMap,
237             Function<AppUsageStatsData, Integer> countFunc, float baseScore, float minValidScore) {
238         int maxCount = 0;
239         for (AppUsageStatsData data : appStatsMap.values()) {
240             maxCount = Math.max(maxCount, countFunc.apply(data));
241         }
242         if (maxCount > 0) {
243             for (Map.Entry<String, AppUsageStatsData> entry : appStatsMap.entrySet()) {
244                 if (!shareTargetMap.containsKey(entry.getKey())) {
245                     continue;
246                 }
247                 ShareTargetPredictor.ShareTarget target = shareTargetMap.get(entry.getKey()).get(0);
248                 if (target.getScore() > 0f) {
249                     continue;
250                 }
251                 float curScore = baseScore * countFunc.apply(entry.getValue()) / maxCount;
252                 target.setScore(curScore);
253                 if (curScore > 0) {
254                     minValidScore = Math.min(minValidScore, curScore);
255                 }
256                 if (DEBUG) {
257                     Slog.d(TAG, String.format(
258                             "SharesheetModel: promote as per AppUsageStats packageName: %s, "
259                                     + "className: %s, total:%.2f",
260                             target.getAppTarget().getPackageName(),
261                             target.getAppTarget().getClassName(),
262                             target.getScore()));
263                 }
264             }
265         }
266         return minValidScore;
267     }
268 
269     /**
270      * Promotes the foreground app just prior to source sharing app. Share often occurs between
271      * two apps the user is switching.
272      */
promoteForegroundApp( Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap, @NonNull DataManager dataManager, @UserIdInt int callingUserId)273     private static void promoteForegroundApp(
274             Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap,
275             @NonNull DataManager dataManager, @UserIdInt int callingUserId) {
276         String sharingForegroundApp = findSharingForegroundApp(shareTargetMap, dataManager,
277                 callingUserId);
278         if (sharingForegroundApp != null) {
279             ShareTargetPredictor.ShareTarget target = shareTargetMap.get(sharingForegroundApp).get(
280                     0);
281             target.setScore(probOR(target.getScore(), FOREGROUND_APP_WEIGHT));
282             if (DEBUG) {
283                 Slog.d(TAG, String.format(
284                         "SharesheetModel: promoteForegroundApp packageName: %s, className: %s, "
285                                 + "total:%.2f",
286                         target.getAppTarget().getPackageName(),
287                         target.getAppTarget().getClassName(),
288                         target.getScore()));
289             }
290         }
291     }
292 
293     /**
294      * Find the foreground app just prior to source sharing app from usageStatsManager. Returns null
295      * if it is not available.
296      */
297     @Nullable
findSharingForegroundApp( Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap, @NonNull DataManager dataManager, @UserIdInt int callingUserId)298     private static String findSharingForegroundApp(
299             Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap,
300             @NonNull DataManager dataManager, @UserIdInt int callingUserId) {
301         String sharingForegroundApp = null;
302         long now = System.currentTimeMillis();
303         List<UsageEvents.Event> events = dataManager.queryAppMovingToForegroundEvents(
304                 callingUserId, now - FOREGROUND_APP_PROMO_TIME_WINDOW, now);
305         String sourceApp = null;
306         for (int i = events.size() - 1; i >= 0; i--) {
307             String className = events.get(i).getClassName();
308             String packageName = events.get(i).getPackageName();
309             if (packageName == null || (className != null && className.contains(CHOOSER_ACTIVITY))
310                     || packageName.contains(CHOOSER_ACTIVITY)) {
311                 continue;
312             }
313             if (sourceApp == null) {
314                 sourceApp = packageName;
315             } else if (!packageName.equals(sourceApp) && shareTargetMap.containsKey(packageName)) {
316                 sharingForegroundApp = packageName;
317                 break;
318             }
319         }
320         return sharingForegroundApp;
321     }
322 
323     /**
324      * Probabilistic OR (also known as the algebraic sum). If a <= 1 and b <= 1, the result will be
325      * <= 1.0.
326      */
probOR(float a, float b)327     private static float probOR(float a, float b) {
328         return 1f - (1f - a) * (1f - b);
329     }
330 
331     /** Counts frequency of share targets. Decays frequency for old shares. */
getFreqDecayedOnElapsedTime(long elapsedTimeMillis)332     private static float getFreqDecayedOnElapsedTime(long elapsedTimeMillis) {
333         Duration duration = Duration.ofMillis(elapsedTimeMillis);
334         if (duration.compareTo(Duration.ofDays(1)) <= 0) {
335             return 1.0f;
336         } else if (duration.compareTo(Duration.ofDays(3)) <= 0) {
337             return 0.9f;
338         } else if (duration.compareTo(Duration.ofDays(7)) <= 0) {
339             return 0.8f;
340         } else if (duration.compareTo(Duration.ofDays(14)) <= 0) {
341             return 0.7f;
342         } else {
343             return 0.6f;
344         }
345     }
346 
347     /** Normalizes frequency score. */
normalizeFreqScore(double freqRatio)348     private static float normalizeFreqScore(double freqRatio) {
349         if (freqRatio >= 2.5) {
350             return 0.2f;
351         } else if (freqRatio >= 1.5) {
352             return 0.15f;
353         } else if (freqRatio >= 1.0) {
354             return 0.1f;
355         } else if (freqRatio >= 0.75) {
356             return 0.05f;
357         } else {
358             return 0f;
359         }
360     }
361 
362     /** Normalizes mimetype-specific frequency score. */
normalizeMimeFreqScore(double freqRatio)363     private static float normalizeMimeFreqScore(double freqRatio) {
364         if (freqRatio >= 2.0) {
365             return 0.2f;
366         } else if (freqRatio >= 1.2) {
367             return 0.15f;
368         } else if (freqRatio > 0.0) {
369             return 0.1f;
370         } else {
371             return 0f;
372         }
373     }
374 
375     private static class ShareTargetRankingScore {
376 
377         private float mRecencyScore = 0f;
378         private float mFrequencyScore = 0f;
379         private float mMimeFrequencyScore = 0f;
380         private float mTotalScore = 0f;
381 
getTotalScore()382         float getTotalScore() {
383             return mTotalScore;
384         }
385 
setTotalScore(float totalScore)386         void setTotalScore(float totalScore) {
387             mTotalScore = totalScore;
388         }
389 
getRecencyScore()390         float getRecencyScore() {
391             return mRecencyScore;
392         }
393 
setRecencyScore(float recencyScore)394         void setRecencyScore(float recencyScore) {
395             mRecencyScore = recencyScore;
396         }
397 
getFrequencyScore()398         float getFrequencyScore() {
399             return mFrequencyScore;
400         }
401 
setFrequencyScore(float frequencyScore)402         void setFrequencyScore(float frequencyScore) {
403             mFrequencyScore = frequencyScore;
404         }
405 
incrementFrequencyScore(float incremental)406         void incrementFrequencyScore(float incremental) {
407             mFrequencyScore += incremental;
408         }
409 
getMimeFrequencyScore()410         float getMimeFrequencyScore() {
411             return mMimeFrequencyScore;
412         }
413 
setMimeFrequencyScore(float mimeFrequencyScore)414         void setMimeFrequencyScore(float mimeFrequencyScore) {
415             mMimeFrequencyScore = mimeFrequencyScore;
416         }
417 
incrementMimeFrequencyScore(float incremental)418         void incrementMimeFrequencyScore(float incremental) {
419             mMimeFrequencyScore += incremental;
420         }
421     }
422 }
423