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