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