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.server.people.data.AppUsageStatsData;
30 import com.android.server.people.data.DataManager;
31 import com.android.server.people.data.Event;
32 
33 import java.time.Duration;
34 import java.util.ArrayList;
35 import java.util.Comparator;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.PriorityQueue;
39 import java.util.concurrent.TimeUnit;
40 import java.util.function.Function;
41 
42 /** Ranking scorer for Sharesheet targets. */
43 class SharesheetModelScorer {
44 
45     private static final String TAG = "SharesheetModelScorer";
46     private static final boolean DEBUG = false;
47     private static final Integer RECENCY_SCORE_COUNT = 6;
48     private static final float RECENCY_INITIAL_BASE_SCORE = 0.4F;
49     private static final float RECENCY_SCORE_INITIAL_DECAY = 0.05F;
50     private static final float RECENCY_SCORE_SUBSEQUENT_DECAY = 0.02F;
51     private static final long ONE_MONTH_WINDOW = TimeUnit.DAYS.toMillis(30);
52     private static final long FOREGROUND_APP_PROMO_TIME_WINDOW = TimeUnit.MINUTES.toMillis(10);
53     private static final float USAGE_STATS_CHOOSER_SCORE_INITIAL_DECAY = 0.9F;
54     private static final float FREQUENTLY_USED_APP_SCORE_INITIAL_DECAY = 0.3F;
55     @VisibleForTesting
56     static final float FOREGROUND_APP_WEIGHT = 0F;
57 
58     // Keep constructor private to avoid class being instantiated.
SharesheetModelScorer()59     private SharesheetModelScorer() {
60     }
61 
62     /**
63      * Computes each target's recency, frequency and frequency of the same {@code shareEventType}
64      * based on past sharing history. Update {@link ShareTargetPredictor.ShareTargetScore}.
65      */
computeScore(List<ShareTargetPredictor.ShareTarget> shareTargets, int shareEventType, long now)66     static void computeScore(List<ShareTargetPredictor.ShareTarget> shareTargets,
67             int shareEventType, long now) {
68         if (shareTargets.isEmpty()) {
69             return;
70         }
71         float totalFreqScore = 0f;
72         int freqScoreCount = 0;
73         float totalMimeFreqScore = 0f;
74         int mimeFreqScoreCount = 0;
75         // Top of this heap has lowest rank.
76         PriorityQueue<Pair<ShareTargetRankingScore, Range<Long>>> recencyMinHeap =
77                 new PriorityQueue<>(RECENCY_SCORE_COUNT,
78                         Comparator.comparingLong(p -> p.second.getUpper()));
79         List<ShareTargetRankingScore> scoreList = new ArrayList<>(shareTargets.size());
80         for (ShareTargetPredictor.ShareTarget target : shareTargets) {
81             ShareTargetRankingScore shareTargetScore = new ShareTargetRankingScore();
82             scoreList.add(shareTargetScore);
83             if (target.getEventHistory() == null) {
84                 continue;
85             }
86             // Counts frequency
87             List<Range<Long>> timeSlots = target.getEventHistory().getEventIndex(
88                     Event.SHARE_EVENT_TYPES).getActiveTimeSlots();
89             if (!timeSlots.isEmpty()) {
90                 for (Range<Long> timeSlot : timeSlots) {
91                     shareTargetScore.incrementFrequencyScore(
92                             getFreqDecayedOnElapsedTime(now - timeSlot.getLower()));
93                 }
94                 totalFreqScore += shareTargetScore.getFrequencyScore();
95                 freqScoreCount++;
96             }
97             // Counts frequency for sharing same mime type
98             List<Range<Long>> timeSlotsOfSameType = target.getEventHistory().getEventIndex(
99                     shareEventType).getActiveTimeSlots();
100             if (!timeSlotsOfSameType.isEmpty()) {
101                 for (Range<Long> timeSlot : timeSlotsOfSameType) {
102                     shareTargetScore.incrementMimeFrequencyScore(
103                             getFreqDecayedOnElapsedTime(now - timeSlot.getLower()));
104                 }
105                 totalMimeFreqScore += shareTargetScore.getMimeFrequencyScore();
106                 mimeFreqScoreCount++;
107             }
108             // Records most recent targets
109             Range<Long> mostRecentTimeSlot = target.getEventHistory().getEventIndex(
110                     Event.SHARE_EVENT_TYPES).getMostRecentActiveTimeSlot();
111             if (mostRecentTimeSlot == null) {
112                 continue;
113             }
114             if (recencyMinHeap.size() < RECENCY_SCORE_COUNT
115                     || mostRecentTimeSlot.getUpper() > recencyMinHeap.peek().second.getUpper()) {
116                 if (recencyMinHeap.size() == RECENCY_SCORE_COUNT) {
117                     recencyMinHeap.poll();
118                 }
119                 recencyMinHeap.offer(new Pair(shareTargetScore, mostRecentTimeSlot));
120             }
121         }
122         // Calculates recency score
123         while (!recencyMinHeap.isEmpty()) {
124             float recencyScore = RECENCY_INITIAL_BASE_SCORE;
125             if (recencyMinHeap.size() > 1) {
126                 recencyScore = RECENCY_INITIAL_BASE_SCORE - RECENCY_SCORE_INITIAL_DECAY
127                         - RECENCY_SCORE_SUBSEQUENT_DECAY * (recencyMinHeap.size() - 2);
128             }
129             recencyMinHeap.poll().first.setRecencyScore(recencyScore);
130         }
131 
132         Float avgFreq = freqScoreCount != 0 ? totalFreqScore / freqScoreCount : 0f;
133         Float avgMimeFreq = mimeFreqScoreCount != 0 ? totalMimeFreqScore / mimeFreqScoreCount : 0f;
134         for (int i = 0; i < scoreList.size(); i++) {
135             ShareTargetPredictor.ShareTarget target = shareTargets.get(i);
136             ShareTargetRankingScore targetScore = scoreList.get(i);
137             // Normalizes freq and mimeFreq score
138             targetScore.setFrequencyScore(normalizeFreqScore(
139                     avgFreq.equals(0f) ? 0f : targetScore.getFrequencyScore() / avgFreq));
140             targetScore.setMimeFrequencyScore(normalizeMimeFreqScore(avgMimeFreq.equals(0f) ? 0f
141                     : targetScore.getMimeFrequencyScore() / avgMimeFreq));
142             // Calculates total score
143             targetScore.setTotalScore(
144                     probOR(probOR(targetScore.getRecencyScore(), targetScore.getFrequencyScore()),
145                             targetScore.getMimeFrequencyScore()));
146             target.setScore(targetScore.getTotalScore());
147 
148             if (DEBUG) {
149                 Slog.d(TAG, String.format(
150                         "SharesheetModel: packageName: %s, className: %s, shortcutId: %s, "
151                                 + "recency:%.2f, freq_all:%.2f, freq_mime:%.2f, total:%.2f",
152                         target.getAppTarget().getPackageName(),
153                         target.getAppTarget().getClassName(),
154                         target.getAppTarget().getShortcutInfo() != null
155                                 ? target.getAppTarget().getShortcutInfo().getId() : null,
156                         targetScore.getRecencyScore(),
157                         targetScore.getFrequencyScore(),
158                         targetScore.getMimeFrequencyScore(),
159                         targetScore.getTotalScore()));
160             }
161         }
162     }
163 
164     /**
165      * Computes ranking score for app sharing. Update {@link ShareTargetPredictor.ShareTargetScore}.
166      */
computeScoreForAppShare(List<ShareTargetPredictor.ShareTarget> shareTargets, int shareEventType, int targetsLimit, long now, @NonNull DataManager dataManager, @UserIdInt int callingUserId, @Nullable String chooserActivity)167     static void computeScoreForAppShare(List<ShareTargetPredictor.ShareTarget> shareTargets,
168             int shareEventType, int targetsLimit, long now, @NonNull DataManager dataManager,
169             @UserIdInt int callingUserId, @Nullable String chooserActivity) {
170         computeScore(shareTargets, shareEventType, now);
171         postProcess(shareTargets, targetsLimit, dataManager, callingUserId, chooserActivity);
172     }
173 
postProcess(List<ShareTargetPredictor.ShareTarget> shareTargets, int targetsLimit, @NonNull DataManager dataManager, @UserIdInt int callingUserId, @Nullable String chooserActivity)174     private static void postProcess(List<ShareTargetPredictor.ShareTarget> shareTargets,
175             int targetsLimit, @NonNull DataManager dataManager, @UserIdInt int callingUserId,
176             @Nullable String chooserActivity) {
177         // Populates a map which key is package name and value is list of shareTargets descended
178         // on total score.
179         Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap = new ArrayMap<>();
180         for (ShareTargetPredictor.ShareTarget shareTarget : shareTargets) {
181             String packageName = shareTarget.getAppTarget().getPackageName();
182             shareTargetMap.computeIfAbsent(packageName, key -> new ArrayList<>());
183             List<ShareTargetPredictor.ShareTarget> targetsList = shareTargetMap.get(packageName);
184             int index = 0;
185             while (index < targetsList.size()) {
186                 if (shareTarget.getScore() > targetsList.get(index).getScore()) {
187                     break;
188                 }
189                 index++;
190             }
191             targetsList.add(index, shareTarget);
192         }
193         promoteForegroundApp(shareTargetMap, dataManager, callingUserId, chooserActivity);
194         promoteMostChosenAndFrequentlyUsedApps(shareTargetMap, targetsLimit, dataManager,
195                 callingUserId);
196     }
197 
198     /**
199      * Promotes frequently chosen sharing apps and frequently used sharing apps as per
200      * UsageStatsManager, if recommended apps based on sharing history have not reached the limit
201      * (e.g. user did not share any content in last couple weeks)
202      */
promoteMostChosenAndFrequentlyUsedApps( Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap, int targetsLimit, @NonNull DataManager dataManager, @UserIdInt int callingUserId)203     private static void promoteMostChosenAndFrequentlyUsedApps(
204             Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap, int targetsLimit,
205             @NonNull DataManager dataManager, @UserIdInt int callingUserId) {
206         int validPredictionNum = 0;
207         float minValidScore = 1f;
208         for (List<ShareTargetPredictor.ShareTarget> targets : shareTargetMap.values()) {
209             for (ShareTargetPredictor.ShareTarget target : targets) {
210                 if (target.getScore() > 0f) {
211                     validPredictionNum++;
212                     minValidScore = Math.min(target.getScore(), minValidScore);
213                 }
214             }
215         }
216         // Skips if recommended apps based on sharing history have already reached the limit.
217         if (validPredictionNum >= targetsLimit) {
218             return;
219         }
220         long now = System.currentTimeMillis();
221         Map<String, AppUsageStatsData> appStatsMap =
222                 dataManager.queryAppUsageStats(
223                         callingUserId, now - ONE_MONTH_WINDOW, now, shareTargetMap.keySet());
224         // Promotes frequently chosen sharing apps as per UsageStatsManager.
225         minValidScore = promoteApp(shareTargetMap, appStatsMap, AppUsageStatsData::getChosenCount,
226                 USAGE_STATS_CHOOSER_SCORE_INITIAL_DECAY * minValidScore, minValidScore);
227         // Promotes frequently used sharing apps as per UsageStatsManager.
228         promoteApp(shareTargetMap, appStatsMap, AppUsageStatsData::getLaunchCount,
229                 FREQUENTLY_USED_APP_SCORE_INITIAL_DECAY * minValidScore, minValidScore);
230     }
231 
promoteApp( Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap, Map<String, AppUsageStatsData> appStatsMap, Function<AppUsageStatsData, Integer> countFunc, float baseScore, float minValidScore)232     private static float promoteApp(
233             Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap,
234             Map<String, AppUsageStatsData> appStatsMap,
235             Function<AppUsageStatsData, Integer> countFunc, float baseScore, float minValidScore) {
236         int maxCount = 0;
237         for (AppUsageStatsData data : appStatsMap.values()) {
238             maxCount = Math.max(maxCount, countFunc.apply(data));
239         }
240         if (maxCount > 0) {
241             for (Map.Entry<String, AppUsageStatsData> entry : appStatsMap.entrySet()) {
242                 if (!shareTargetMap.containsKey(entry.getKey())) {
243                     continue;
244                 }
245                 ShareTargetPredictor.ShareTarget target = shareTargetMap.get(entry.getKey()).get(0);
246                 if (target.getScore() > 0f) {
247                     continue;
248                 }
249                 float curScore = baseScore * countFunc.apply(entry.getValue()) / maxCount;
250                 target.setScore(curScore);
251                 if (curScore > 0) {
252                     minValidScore = Math.min(minValidScore, curScore);
253                 }
254                 if (DEBUG) {
255                     Slog.d(TAG, String.format(
256                             "SharesheetModel: promote as per AppUsageStats packageName: %s, "
257                                     + "className: %s, total:%.2f",
258                             target.getAppTarget().getPackageName(),
259                             target.getAppTarget().getClassName(),
260                             target.getScore()));
261                 }
262             }
263         }
264         return minValidScore;
265     }
266 
267     /**
268      * Promotes the foreground app just prior to source sharing app. Share often occurs between
269      * two apps the user is switching.
270      */
promoteForegroundApp( Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap, @NonNull DataManager dataManager, @UserIdInt int callingUserId, @Nullable String chooserActivity)271     private static void promoteForegroundApp(
272             Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap,
273             @NonNull DataManager dataManager, @UserIdInt int callingUserId,
274             @Nullable String chooserActivity) {
275         String sharingForegroundApp = findSharingForegroundApp(shareTargetMap, dataManager,
276                 callingUserId, chooserActivity);
277         if (sharingForegroundApp != null) {
278             ShareTargetPredictor.ShareTarget target = shareTargetMap.get(sharingForegroundApp).get(
279                     0);
280             target.setScore(probOR(target.getScore(), FOREGROUND_APP_WEIGHT));
281             if (DEBUG) {
282                 Slog.d(TAG, String.format(
283                         "SharesheetModel: promoteForegroundApp packageName: %s, className: %s, "
284                                 + "total:%.2f",
285                         target.getAppTarget().getPackageName(),
286                         target.getAppTarget().getClassName(),
287                         target.getScore()));
288             }
289         }
290     }
291 
292     /**
293      * Find the foreground app just prior to source sharing app from usageStatsManager. Returns null
294      * if it is not available.
295      */
296     @Nullable
findSharingForegroundApp( Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap, @NonNull DataManager dataManager, @UserIdInt int callingUserId, @Nullable String chooserActivity)297     private static String findSharingForegroundApp(
298             Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap,
299             @NonNull DataManager dataManager, @UserIdInt int callingUserId,
300             @Nullable String chooserActivity) {
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 && chooserActivity != null
310                     && className.contains(chooserActivity))) {
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