diff options
7 files changed, 1024 insertions, 42 deletions
diff --git a/services/people/java/com/android/server/people/data/DataManager.java b/services/people/java/com/android/server/people/data/DataManager.java index ae8d5743668a..136ee91dd685 100644 --- a/services/people/java/com/android/server/people/data/DataManager.java +++ b/services/people/java/com/android/server/people/data/DataManager.java @@ -26,6 +26,7 @@ import android.app.NotificationManager; import android.app.Person; import android.app.prediction.AppTarget; import android.app.prediction.AppTargetEvent; +import android.app.usage.UsageEvents; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; @@ -70,6 +71,7 @@ import com.android.server.notification.NotificationManagerInternal; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -237,6 +239,27 @@ public class DataManager { eventHistory.addEvent(new Event(System.currentTimeMillis(), eventType)); } + /** + * Queries events for moving app to foreground between {@code startTime} and {@code endTime}. + */ + @NonNull + public List<UsageEvents.Event> queryAppMovingToForegroundEvents(@UserIdInt int callingUserId, + long startTime, long endTime) { + return UsageStatsQueryHelper.queryAppMovingToForegroundEvents(callingUserId, startTime, + endTime); + } + + /** + * Queries launch counts of apps within {@code packageNameFilter} between {@code startTime} + * and {@code endTime}. + */ + @NonNull + public Map<String, Integer> queryAppLaunchCount(@UserIdInt int callingUserId, long startTime, + long endTime, Set<String> packageNameFilter) { + return UsageStatsQueryHelper.queryAppLaunchCount(callingUserId, startTime, endTime, + packageNameFilter); + } + /** Prunes the data for the specified user. */ public void pruneDataForUser(@UserIdInt int userId, @NonNull CancellationSignal signal) { UserData userData = getUnlockedUserData(userId); @@ -382,7 +405,13 @@ public class DataManager { } } - private int mimeTypeToShareEventType(String mimeType) { + /** + * Converts {@code mimeType} to {@link Event.EventType}. + */ + public int mimeTypeToShareEventType(String mimeType) { + if (mimeType == null) { + return Event.TYPE_SHARE_OTHER; + } if (mimeType.startsWith("text/")) { return Event.TYPE_SHARE_TEXT; } else if (mimeType.startsWith("image/")) { diff --git a/services/people/java/com/android/server/people/data/UsageStatsQueryHelper.java b/services/people/java/com/android/server/people/data/UsageStatsQueryHelper.java index 72f1abb70e34..6e6fea93c803 100644 --- a/services/people/java/com/android/server/people/data/UsageStatsQueryHelper.java +++ b/services/people/java/com/android/server/people/data/UsageStatsQueryHelper.java @@ -19,6 +19,8 @@ package com.android.server.people.data; import android.annotation.NonNull; import android.annotation.UserIdInt; import android.app.usage.UsageEvents; +import android.app.usage.UsageStats; +import android.app.usage.UsageStatsManager; import android.app.usage.UsageStatsManagerInternal; import android.content.ComponentName; import android.content.LocusId; @@ -27,7 +29,10 @@ import android.util.ArrayMap; import com.android.server.LocalServices; +import java.util.ArrayList; +import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Function; /** A helper class that queries {@link UsageStatsManagerInternal}. */ @@ -46,7 +51,7 @@ class UsageStatsQueryHelper { */ UsageStatsQueryHelper(@UserIdInt int userId, Function<String, PackageData> packageDataGetter) { - mUsageStatsManagerInternal = LocalServices.getService(UsageStatsManagerInternal.class); + mUsageStatsManagerInternal = getUsageStatsManagerInternal(); mUserId = userId; mPackageDataGetter = packageDataGetter; } @@ -106,6 +111,53 @@ class UsageStatsQueryHelper { return mLastEventTimestamp; } + /** + * Queries {@link UsageStatsManagerInternal} events for moving app to foreground between + * {@code startTime} and {@code endTime}. + * + * @return a list containing events moving app to foreground. + */ + static List<UsageEvents.Event> queryAppMovingToForegroundEvents(@UserIdInt int userId, + long startTime, long endTime) { + List<UsageEvents.Event> res = new ArrayList<>(); + UsageEvents usageEvents = getUsageStatsManagerInternal().queryEventsForUser(userId, + startTime, endTime, + UsageEvents.HIDE_SHORTCUT_EVENTS | UsageEvents.HIDE_LOCUS_EVENTS); + if (usageEvents == null) { + return res; + } + while (usageEvents.hasNextEvent()) { + UsageEvents.Event e = new UsageEvents.Event(); + usageEvents.getNextEvent(e); + if (e.getEventType() == UsageEvents.Event.ACTIVITY_RESUMED) { + res.add(e); + } + } + return res; + } + + /** + * Queries {@link UsageStatsManagerInternal} for launch count of apps within {@code + * packageNameFilter} between {@code startTime} and {@code endTime}.obfuscateInstantApps + * + * @return a map which keys are package names and values are app launch counts. + */ + static Map<String, Integer> queryAppLaunchCount(@UserIdInt int userId, long startTime, + long endTime, Set<String> packageNameFilter) { + List<UsageStats> stats = getUsageStatsManagerInternal().queryUsageStatsForUser(userId, + UsageStatsManager.INTERVAL_BEST, startTime, endTime, + /* obfuscateInstantApps= */ false); + Map<String, Integer> aggregatedStats = new ArrayMap<>(); + for (UsageStats stat : stats) { + String packageName = stat.getPackageName(); + if (packageNameFilter.contains(packageName)) { + aggregatedStats.put(packageName, + aggregatedStats.getOrDefault(packageName, 0) + stat.getAppLaunchCount()); + } + } + return aggregatedStats; + } + private void onInAppConversationEnded(@NonNull PackageData packageData, @NonNull UsageEvents.Event endEvent) { ComponentName activityName = @@ -138,4 +190,8 @@ class UsageStatsQueryHelper { EventStore.CATEGORY_LOCUS_ID_BASED, locusId.getId()); eventHistory.addEvent(event); } + + private static UsageStatsManagerInternal getUsageStatsManagerInternal() { + return LocalServices.getService(UsageStatsManagerInternal.class); + } } diff --git a/services/people/java/com/android/server/people/prediction/ShareTargetPredictor.java b/services/people/java/com/android/server/people/prediction/ShareTargetPredictor.java index 8e5d75be12b7..d09d0b379769 100644 --- a/services/people/java/com/android/server/people/prediction/ShareTargetPredictor.java +++ b/services/people/java/com/android/server/people/prediction/ShareTargetPredictor.java @@ -27,13 +27,11 @@ import android.app.prediction.AppTargetId; import android.content.IntentFilter; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager.ShareShortcutInfo; -import android.util.Range; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.ChooserActivity; import com.android.server.people.data.ConversationInfo; import com.android.server.people.data.DataManager; -import com.android.server.people.data.Event; import com.android.server.people.data.EventHistory; import com.android.server.people.data.PackageData; @@ -42,6 +40,9 @@ import java.util.Collections; import java.util.List; import java.util.function.Consumer; +/** + * Predictor that predicts the {@link AppTarget} the user is most likely to open on share sheet. + */ class ShareTargetPredictor extends AppTargetPredictor { private final IntentFilter mIntentFilter; @@ -66,7 +67,9 @@ class ShareTargetPredictor extends AppTargetPredictor { @Override void predictTargets() { List<ShareTarget> shareTargets = getDirectShareTargets(); - rankTargets(shareTargets); + SharesheetModelScorer.computeScore(shareTargets, getShareEventType(mIntentFilter), + System.currentTimeMillis()); + Collections.sort(shareTargets, (t1, t2) -> -Float.compare(t1.getScore(), t2.getScore())); List<AppTarget> res = new ArrayList<>(); for (int i = 0; i < Math.min(getPredictionContext().getPredictedTargetCount(), shareTargets.size()); i++) { @@ -80,36 +83,16 @@ class ShareTargetPredictor extends AppTargetPredictor { @Override void sortTargets(List<AppTarget> targets, Consumer<List<AppTarget>> callback) { List<ShareTarget> shareTargets = getAppShareTargets(targets); - rankTargets(shareTargets); + SharesheetModelScorer.computeScoreForAppShare(shareTargets, + getShareEventType(mIntentFilter), getPredictionContext().getPredictedTargetCount(), + System.currentTimeMillis(), getDataManager(), + mCallingUserId); + Collections.sort(shareTargets, (t1, t2) -> -Float.compare(t1.getScore(), t2.getScore())); List<AppTarget> appTargetList = new ArrayList<>(); shareTargets.forEach(t -> appTargetList.add(t.getAppTarget())); callback.accept(appTargetList); } - private void rankTargets(List<ShareTarget> shareTargets) { - // Rank targets based on recency of sharing history only for the moment. - // TODO: Take more factors into ranking, e.g. frequency, mime type, foreground app. - Collections.sort(shareTargets, (t1, t2) -> { - if (t1.getEventHistory() == null) { - return 1; - } - if (t2.getEventHistory() == null) { - return -1; - } - Range<Long> timeSlot1 = t1.getEventHistory().getEventIndex( - Event.SHARE_EVENT_TYPES).getMostRecentActiveTimeSlot(); - Range<Long> timeSlot2 = t2.getEventHistory().getEventIndex( - Event.SHARE_EVENT_TYPES).getMostRecentActiveTimeSlot(); - if (timeSlot1 == null) { - return 1; - } else if (timeSlot2 == null) { - return -1; - } else { - return -Long.compare(timeSlot1.getUpper(), timeSlot2.getUpper()); - } - }); - } - private List<ShareTarget> getDirectShareTargets() { List<ShareTarget> shareTargets = new ArrayList<>(); List<ShareShortcutInfo> shareShortcuts = @@ -153,6 +136,11 @@ class ShareTargetPredictor extends AppTargetPredictor { return shareTargets; } + private int getShareEventType(IntentFilter intentFilter) { + String mimeType = intentFilter != null ? intentFilter.getDataType(0) : null; + return getDataManager().mimeTypeToShareEventType(mimeType); + } + @VisibleForTesting static class ShareTarget { @@ -162,13 +150,16 @@ class ShareTargetPredictor extends AppTargetPredictor { private final EventHistory mEventHistory; @Nullable private final ConversationInfo mConversationInfo; + private float mScore; - private ShareTarget(@NonNull AppTarget appTarget, + @VisibleForTesting + ShareTarget(@NonNull AppTarget appTarget, @Nullable EventHistory eventHistory, @Nullable ConversationInfo conversationInfo) { mAppTarget = appTarget; mEventHistory = eventHistory; mConversationInfo = conversationInfo; + mScore = 0f; } @NonNull @@ -188,5 +179,15 @@ class ShareTargetPredictor extends AppTargetPredictor { ConversationInfo getConversationInfo() { return mConversationInfo; } + + @VisibleForTesting + float getScore() { + return mScore; + } + + @VisibleForTesting + void setScore(float score) { + mScore = score; + } } } diff --git a/services/people/java/com/android/server/people/prediction/SharesheetModelScorer.java b/services/people/java/com/android/server/people/prediction/SharesheetModelScorer.java new file mode 100644 index 000000000000..0ac5724210da --- /dev/null +++ b/services/people/java/com/android/server/people/prediction/SharesheetModelScorer.java @@ -0,0 +1,406 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.people.prediction; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.app.usage.UsageEvents; +import android.util.ArrayMap; +import android.util.Pair; +import android.util.Range; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.app.ChooserActivity; +import com.android.server.people.data.DataManager; +import com.android.server.people.data.Event; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.PriorityQueue; +import java.util.concurrent.TimeUnit; + +/** Ranking scorer for Sharesheet targets. */ +class SharesheetModelScorer { + + private static final String TAG = "SharesheetModelScorer"; + private static final boolean DEBUG = false; + private static final Integer RECENCY_SCORE_COUNT = 6; + private static final float RECENCY_INITIAL_BASE_SCORE = 0.4F; + private static final float RECENCY_SCORE_INITIAL_DECAY = 0.05F; + private static final float RECENCY_SCORE_SUBSEQUENT_DECAY = 0.02F; + private static final long ONE_MONTH_WINDOW = TimeUnit.DAYS.toMillis(30); + private static final long FOREGROUND_APP_PROMO_TIME_WINDOW = TimeUnit.MINUTES.toMillis(10); + private static final float FREQUENTLY_USED_APP_SCORE_DECAY = 0.9F; + @VisibleForTesting + static final float FOREGROUND_APP_WEIGHT = 0F; + @VisibleForTesting + static final String CHOOSER_ACTIVITY = ChooserActivity.class.getSimpleName(); + + // Keep constructor private to avoid class being instantiated. + private SharesheetModelScorer() { + } + + /** + * Computes each target's recency, frequency and frequency of the same {@code shareEventType} + * based on past sharing history. Update {@link ShareTargetPredictor.ShareTargetScore}. + */ + static void computeScore(List<ShareTargetPredictor.ShareTarget> shareTargets, + int shareEventType, long now) { + if (shareTargets.isEmpty()) { + return; + } + float totalFreqScore = 0f; + int freqScoreCount = 0; + float totalMimeFreqScore = 0f; + int mimeFreqScoreCount = 0; + // Top of this heap has lowest rank. + PriorityQueue<Pair<ShareTargetRankingScore, Range<Long>>> recencyMinHeap = + new PriorityQueue<>(RECENCY_SCORE_COUNT, + Comparator.comparingLong(p -> p.second.getUpper())); + List<ShareTargetRankingScore> scoreList = new ArrayList<>(shareTargets.size()); + for (ShareTargetPredictor.ShareTarget target : shareTargets) { + ShareTargetRankingScore shareTargetScore = new ShareTargetRankingScore(); + scoreList.add(shareTargetScore); + if (target.getEventHistory() == null) { + continue; + } + // Counts frequency + List<Range<Long>> timeSlots = target.getEventHistory().getEventIndex( + Event.SHARE_EVENT_TYPES).getActiveTimeSlots(); + if (!timeSlots.isEmpty()) { + for (Range<Long> timeSlot : timeSlots) { + shareTargetScore.incrementFrequencyScore( + getFreqDecayedOnElapsedTime(now - timeSlot.getLower())); + } + totalFreqScore += shareTargetScore.getFrequencyScore(); + freqScoreCount++; + } + // Counts frequency for sharing same mime type + List<Range<Long>> timeSlotsOfSameType = target.getEventHistory().getEventIndex( + shareEventType).getActiveTimeSlots(); + if (!timeSlotsOfSameType.isEmpty()) { + for (Range<Long> timeSlot : timeSlotsOfSameType) { + shareTargetScore.incrementMimeFrequencyScore( + getFreqDecayedOnElapsedTime(now - timeSlot.getLower())); + } + totalMimeFreqScore += shareTargetScore.getMimeFrequencyScore(); + mimeFreqScoreCount++; + } + // Records most recent targets + Range<Long> mostRecentTimeSlot = target.getEventHistory().getEventIndex( + Event.SHARE_EVENT_TYPES).getMostRecentActiveTimeSlot(); + if (mostRecentTimeSlot == null) { + continue; + } + if (recencyMinHeap.size() < RECENCY_SCORE_COUNT + || mostRecentTimeSlot.getUpper() > recencyMinHeap.peek().second.getUpper()) { + if (recencyMinHeap.size() == RECENCY_SCORE_COUNT) { + recencyMinHeap.poll(); + } + recencyMinHeap.offer(new Pair(shareTargetScore, mostRecentTimeSlot)); + } + } + // Calculates recency score + while (!recencyMinHeap.isEmpty()) { + float recencyScore = RECENCY_INITIAL_BASE_SCORE; + if (recencyMinHeap.size() > 1) { + recencyScore = RECENCY_INITIAL_BASE_SCORE - RECENCY_SCORE_INITIAL_DECAY + - RECENCY_SCORE_SUBSEQUENT_DECAY * (recencyMinHeap.size() - 2); + } + recencyMinHeap.poll().first.setRecencyScore(recencyScore); + } + + Float avgFreq = freqScoreCount != 0 ? totalFreqScore / freqScoreCount : 0f; + Float avgMimeFreq = mimeFreqScoreCount != 0 ? totalMimeFreqScore / mimeFreqScoreCount : 0f; + for (int i = 0; i < scoreList.size(); i++) { + ShareTargetPredictor.ShareTarget target = shareTargets.get(i); + ShareTargetRankingScore targetScore = scoreList.get(i); + // Normalizes freq and mimeFreq score + targetScore.setFrequencyScore(normalizeFreqScore( + avgFreq.equals(0f) ? 0f : targetScore.getFrequencyScore() / avgFreq)); + targetScore.setMimeFrequencyScore(normalizeMimeFreqScore(avgMimeFreq.equals(0f) ? 0f + : targetScore.getMimeFrequencyScore() / avgMimeFreq)); + // Calculates total score + targetScore.setTotalScore( + probOR(probOR(targetScore.getRecencyScore(), targetScore.getFrequencyScore()), + targetScore.getMimeFrequencyScore())); + target.setScore(targetScore.getTotalScore()); + + if (DEBUG) { + Slog.d(TAG, String.format( + "SharesheetModel: packageName: %s, className: %s, shortcutId: %s, " + + "recency:%.2f, freq_all:%.2f, freq_mime:%.2f, total:%.2f", + target.getAppTarget().getPackageName(), + target.getAppTarget().getClassName(), + target.getAppTarget().getShortcutInfo() != null + ? target.getAppTarget().getShortcutInfo().getId() : null, + targetScore.getRecencyScore(), + targetScore.getFrequencyScore(), + targetScore.getMimeFrequencyScore(), + targetScore.getTotalScore())); + } + } + } + + /** + * Computes ranking score for app sharing. Update {@link ShareTargetPredictor.ShareTargetScore}. + */ + static void computeScoreForAppShare(List<ShareTargetPredictor.ShareTarget> shareTargets, + int shareEventType, int targetsLimit, long now, @NonNull DataManager dataManager, + @UserIdInt int callingUserId) { + computeScore(shareTargets, shareEventType, now); + postProcess(shareTargets, targetsLimit, dataManager, callingUserId); + } + + private static void postProcess(List<ShareTargetPredictor.ShareTarget> shareTargets, + int targetsLimit, @NonNull DataManager dataManager, @UserIdInt int callingUserId) { + // Populates a map which key is package name and value is list of shareTargets descended + // on total score. + Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap = new ArrayMap<>(); + for (ShareTargetPredictor.ShareTarget shareTarget : shareTargets) { + String packageName = shareTarget.getAppTarget().getPackageName(); + shareTargetMap.computeIfAbsent(packageName, key -> new ArrayList<>()); + List<ShareTargetPredictor.ShareTarget> targetsList = shareTargetMap.get(packageName); + int index = 0; + while (index < targetsList.size()) { + if (shareTarget.getScore() > targetsList.get(index).getScore()) { + break; + } + index++; + } + targetsList.add(index, shareTarget); + } + promoteForegroundApp(shareTargetMap, dataManager, callingUserId); + promoteFrequentlyUsedApps(shareTargetMap, targetsLimit, dataManager, callingUserId); + } + + /** + * Promotes frequently used sharing apps, if recommended apps based on sharing history have not + * reached the limit (e.g. user did not share any content in last couple weeks) + */ + private static void promoteFrequentlyUsedApps( + Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap, int targetsLimit, + @NonNull DataManager dataManager, @UserIdInt int callingUserId) { + int validPredictionNum = 0; + float minValidScore = 1f; + for (List<ShareTargetPredictor.ShareTarget> targets : shareTargetMap.values()) { + for (ShareTargetPredictor.ShareTarget target : targets) { + if (target.getScore() > 0f) { + validPredictionNum++; + minValidScore = Math.min(target.getScore(), minValidScore); + } + } + } + // Skips if recommended apps based on sharing history have already reached the limit. + if (validPredictionNum >= targetsLimit) { + return; + } + long now = System.currentTimeMillis(); + Map<String, Integer> appLaunchCountsMap = dataManager.queryAppLaunchCount( + callingUserId, now - ONE_MONTH_WINDOW, now, shareTargetMap.keySet()); + List<Pair<String, Integer>> appLaunchCounts = new ArrayList<>(); + for (Map.Entry<String, Integer> entry : appLaunchCountsMap.entrySet()) { + if (entry.getValue() > 0) { + appLaunchCounts.add(new Pair(entry.getKey(), entry.getValue())); + } + } + Collections.sort(appLaunchCounts, (p1, p2) -> -Integer.compare(p1.second, p2.second)); + for (Pair<String, Integer> entry : appLaunchCounts) { + if (!shareTargetMap.containsKey(entry.first)) { + continue; + } + ShareTargetPredictor.ShareTarget target = shareTargetMap.get(entry.first).get(0); + if (target.getScore() > 0f) { + continue; + } + minValidScore *= FREQUENTLY_USED_APP_SCORE_DECAY; + target.setScore(minValidScore); + if (DEBUG) { + Slog.d(TAG, String.format( + "SharesheetModel: promoteFrequentUsedApps packageName: %s, className: %s," + + " total:%.2f", + target.getAppTarget().getPackageName(), + target.getAppTarget().getClassName(), + target.getScore())); + } + validPredictionNum++; + if (validPredictionNum == targetsLimit) { + return; + } + } + } + + /** + * Promotes the foreground app just prior to source sharing app. Share often occurs between + * two apps the user is switching. + */ + private static void promoteForegroundApp( + Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap, + @NonNull DataManager dataManager, @UserIdInt int callingUserId) { + String sharingForegroundApp = findSharingForegroundApp(shareTargetMap, dataManager, + callingUserId); + if (sharingForegroundApp != null) { + ShareTargetPredictor.ShareTarget target = shareTargetMap.get(sharingForegroundApp).get( + 0); + target.setScore(probOR(target.getScore(), FOREGROUND_APP_WEIGHT)); + if (DEBUG) { + Slog.d(TAG, String.format( + "SharesheetModel: promoteForegroundApp packageName: %s, className: %s, " + + "total:%.2f", + target.getAppTarget().getPackageName(), + target.getAppTarget().getClassName(), + target.getScore())); + } + } + } + + /** + * Find the foreground app just prior to source sharing app from usageStatsManager. Returns null + * if it is not available. + */ + @Nullable + private static String findSharingForegroundApp( + Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap, + @NonNull DataManager dataManager, @UserIdInt int callingUserId) { + String sharingForegroundApp = null; + long now = System.currentTimeMillis(); + List<UsageEvents.Event> events = dataManager.queryAppMovingToForegroundEvents( + callingUserId, now - FOREGROUND_APP_PROMO_TIME_WINDOW, now); + String sourceApp = null; + for (int i = events.size() - 1; i >= 0; i--) { + String className = events.get(i).getClassName(); + String packageName = events.get(i).getPackageName(); + if (packageName == null || (className != null && className.contains(CHOOSER_ACTIVITY)) + || packageName.contains(CHOOSER_ACTIVITY)) { + continue; + } + if (sourceApp == null) { + sourceApp = packageName; + } else if (!packageName.equals(sourceApp) && shareTargetMap.containsKey(packageName)) { + sharingForegroundApp = packageName; + break; + } + } + return sharingForegroundApp; + } + + /** + * Probabilistic OR (also known as the algebraic sum). If a <= 1 and b <= 1, the result will be + * <= 1.0. + */ + private static float probOR(float a, float b) { + return 1f - (1f - a) * (1f - b); + } + + /** Counts frequency of share targets. Decays frequency for old shares. */ + private static float getFreqDecayedOnElapsedTime(long elapsedTimeMillis) { + Duration duration = Duration.ofMillis(elapsedTimeMillis); + if (duration.compareTo(Duration.ofDays(1)) <= 0) { + return 1.0f; + } else if (duration.compareTo(Duration.ofDays(3)) <= 0) { + return 0.9f; + } else if (duration.compareTo(Duration.ofDays(7)) <= 0) { + return 0.8f; + } else if (duration.compareTo(Duration.ofDays(14)) <= 0) { + return 0.7f; + } else { + return 0.6f; + } + } + + /** Normalizes frequency score. */ + private static float normalizeFreqScore(double freqRatio) { + if (freqRatio >= 2.5) { + return 0.2f; + } else if (freqRatio >= 1.5) { + return 0.15f; + } else if (freqRatio >= 1.0) { + return 0.1f; + } else if (freqRatio >= 0.75) { + return 0.05f; + } else { + return 0f; + } + } + + /** Normalizes mimetype-specific frequency score. */ + private static float normalizeMimeFreqScore(double freqRatio) { + if (freqRatio >= 2.0) { + return 0.2f; + } else if (freqRatio >= 1.2) { + return 0.15f; + } else if (freqRatio > 0.0) { + return 0.1f; + } else { + return 0f; + } + } + + private static class ShareTargetRankingScore { + + private float mRecencyScore = 0f; + private float mFrequencyScore = 0f; + private float mMimeFrequencyScore = 0f; + private float mTotalScore = 0f; + + float getTotalScore() { + return mTotalScore; + } + + void setTotalScore(float totalScore) { + mTotalScore = totalScore; + } + + float getRecencyScore() { + return mRecencyScore; + } + + void setRecencyScore(float recencyScore) { + mRecencyScore = recencyScore; + } + + float getFrequencyScore() { + return mFrequencyScore; + } + + void setFrequencyScore(float frequencyScore) { + mFrequencyScore = frequencyScore; + } + + void incrementFrequencyScore(float incremental) { + mFrequencyScore += incremental; + } + + float getMimeFrequencyScore() { + return mMimeFrequencyScore; + } + + void setMimeFrequencyScore(float mimeFrequencyScore) { + mMimeFrequencyScore = mimeFrequencyScore; + } + + void incrementMimeFrequencyScore(float incremental) { + mMimeFrequencyScore += incremental; + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/people/data/UsageStatsQueryHelperTest.java b/services/tests/servicestests/src/com/android/server/people/data/UsageStatsQueryHelperTest.java index 7934d33f907d..03d9ad51e6c5 100644 --- a/services/tests/servicestests/src/com/android/server/people/data/UsageStatsQueryHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/people/data/UsageStatsQueryHelperTest.java @@ -21,15 +21,16 @@ import static com.android.server.people.data.TestUtils.timestamp; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.usage.UsageEvents; +import android.app.usage.UsageStats; import android.app.usage.UsageStatsManagerInternal; import android.content.Context; import android.content.LocusId; @@ -50,6 +51,7 @@ import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; import java.util.function.Predicate; @@ -58,7 +60,8 @@ import java.util.function.Predicate; public final class UsageStatsQueryHelperTest { private static final int USER_ID_PRIMARY = 0; - private static final String PKG_NAME = "pkg"; + private static final String PKG_NAME_1 = "pkg_1"; + private static final String PKG_NAME_2 = "pkg_2"; private static final String ACTIVITY_NAME = "TestActivity"; private static final String SHORTCUT_ID = "abc"; private static final LocusId LOCUS_ID_1 = new LocusId("locus_1"); @@ -80,7 +83,7 @@ public final class UsageStatsQueryHelperTest { File testDir = new File(ctx.getCacheDir(), "testdir"); ScheduledExecutorService scheduledExecutorService = new MockScheduledExecutorService(); - mPackageData = new TestPackageData(PKG_NAME, USER_ID_PRIMARY, pkg -> false, pkg -> false, + mPackageData = new TestPackageData(PKG_NAME_1, USER_ID_PRIMARY, pkg -> false, pkg -> false, scheduledExecutorService, testDir); mPackageData.mConversationStore.mConversationInfo = new ConversationInfo.Builder() .setShortcutId(SHORTCUT_ID) @@ -173,10 +176,72 @@ public final class UsageStatsQueryHelperTest { assertEquals(createInAppConversationEvent(130_000L, 30), events.get(2)); } + @Test + public void testQueryAppMovingToForegroundEvents() { + addUsageEvents( + createShortcutInvocationEvent(100_000L), + createActivityResumedEvent(110_000L), + createActivityStoppedEvent(120_000L), + createActivityResumedEvent(130_000L)); + + List<UsageEvents.Event> events = mHelper.queryAppMovingToForegroundEvents(USER_ID_PRIMARY, + 90_000L, + 200_000L); + + assertEquals(2, events.size()); + assertEquals(UsageEvents.Event.ACTIVITY_RESUMED, events.get(0).getEventType()); + assertEquals(110_000L, events.get(0).getTimeStamp()); + assertEquals(UsageEvents.Event.ACTIVITY_RESUMED, events.get(1).getEventType()); + assertEquals(130_000L, events.get(1).getTimeStamp()); + } + + @Test + public void testQueryAppLaunchCount() { + + UsageStats packageStats1 = createUsageStats(PKG_NAME_1, 2); + UsageStats packageStats2 = createUsageStats(PKG_NAME_1, 3); + UsageStats packageStats3 = createUsageStats(PKG_NAME_2, 1); + when(mUsageStatsManagerInternal.queryUsageStatsForUser(anyInt(), anyInt(), anyLong(), + anyLong(), anyBoolean())).thenReturn( + List.of(packageStats1, packageStats2, packageStats3)); + + Map<String, Integer> appLaunchCounts = mHelper.queryAppLaunchCount(USER_ID_PRIMARY, 90_000L, + 200_000L, Set.of(PKG_NAME_1, PKG_NAME_2)); + + assertEquals(2, appLaunchCounts.size()); + assertEquals(5, (long) appLaunchCounts.get(PKG_NAME_1)); + assertEquals(1, (long) appLaunchCounts.get(PKG_NAME_2)); + } + + @Test + public void testQueryAppLaunchCount_packageNameFiltered() { + + UsageStats packageStats1 = createUsageStats(PKG_NAME_1, 2); + UsageStats packageStats2 = createUsageStats(PKG_NAME_1, 3); + UsageStats packageStats3 = createUsageStats(PKG_NAME_2, 1); + when(mUsageStatsManagerInternal.queryUsageStatsForUser(anyInt(), anyInt(), anyLong(), + anyLong(), anyBoolean())).thenReturn( + List.of(packageStats1, packageStats2, packageStats3)); + + Map<String, Integer> appLaunchCounts = mHelper.queryAppLaunchCount(USER_ID_PRIMARY, 90_000L, + 200_000L, + Set.of(PKG_NAME_1)); + + assertEquals(1, appLaunchCounts.size()); + assertEquals(5, (long) appLaunchCounts.get(PKG_NAME_1)); + } + private void addUsageEvents(UsageEvents.Event... events) { UsageEvents usageEvents = new UsageEvents(Arrays.asList(events), new String[]{}); when(mUsageStatsManagerInternal.queryEventsForUser(anyInt(), anyLong(), anyLong(), - eq(UsageEvents.SHOW_ALL_EVENT_DATA))).thenReturn(usageEvents); + anyInt())).thenReturn(usageEvents); + } + + private static UsageStats createUsageStats(String packageName, int launchCount) { + UsageStats packageStats = new UsageStats(); + packageStats.mPackageName = packageName; + packageStats.mAppLaunchCount = launchCount; + return packageStats; } private static <T> void addLocalServiceMock(Class<T> clazz, T mock) { @@ -203,9 +268,15 @@ public final class UsageStatsQueryHelperTest { return e; } + private static UsageEvents.Event createActivityResumedEvent(long timestamp) { + UsageEvents.Event e = createUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, timestamp); + e.mClass = ACTIVITY_NAME; + return e; + } + private static UsageEvents.Event createUsageEvent(int eventType, long timestamp) { UsageEvents.Event e = new UsageEvents.Event(eventType, timestamp); - e.mPackage = PKG_NAME; + e.mPackage = PKG_NAME_1; return e; } diff --git a/services/tests/servicestests/src/com/android/server/people/prediction/ShareTargetPredictorTest.java b/services/tests/servicestests/src/com/android/server/people/prediction/ShareTargetPredictorTest.java index c6cd34732acf..1480627b9b9f 100644 --- a/services/tests/servicestests/src/com/android/server/people/prediction/ShareTargetPredictorTest.java +++ b/services/tests/servicestests/src/com/android/server/people/prediction/ShareTargetPredictorTest.java @@ -127,6 +127,9 @@ public final class ShareTargetPredictorTest { when(mEventHistory1.getEventIndex(anySet())).thenReturn(mEventIndex1); when(mEventHistory2.getEventIndex(anySet())).thenReturn(mEventIndex2); when(mEventHistory3.getEventIndex(anySet())).thenReturn(mEventIndex3); + when(mEventHistory1.getEventIndex(anyInt())).thenReturn(mEventIndex1); + when(mEventHistory2.getEventIndex(anyInt())).thenReturn(mEventIndex2); + when(mEventHistory3.getEventIndex(anyInt())).thenReturn(mEventIndex3); when(mEventIndex1.getMostRecentActiveTimeSlot()).thenReturn(new Range<>(1L, 2L)); when(mEventIndex2.getMostRecentActiveTimeSlot()).thenReturn(new Range<>(2L, 3L)); when(mEventIndex3.getMostRecentActiveTimeSlot()).thenReturn(new Range<>(3L, 4L)); @@ -183,6 +186,12 @@ public final class ShareTargetPredictorTest { when(mEventHistory4.getEventIndex(anySet())).thenReturn(mEventIndex4); when(mEventHistory5.getEventIndex(anySet())).thenReturn(mEventIndex5); when(mEventHistory6.getEventIndex(anySet())).thenReturn(mEventIndex6); + when(mEventHistory1.getEventIndex(anyInt())).thenReturn(mEventIndex1); + when(mEventHistory2.getEventIndex(anyInt())).thenReturn(mEventIndex2); + when(mEventHistory3.getEventIndex(anyInt())).thenReturn(mEventIndex3); + when(mEventHistory4.getEventIndex(anyInt())).thenReturn(mEventIndex4); + when(mEventHistory5.getEventIndex(anyInt())).thenReturn(mEventIndex5); + when(mEventHistory6.getEventIndex(anyInt())).thenReturn(mEventIndex6); when(mEventIndex1.getMostRecentActiveTimeSlot()).thenReturn(new Range<>(1L, 2L)); when(mEventIndex2.getMostRecentActiveTimeSlot()).thenReturn(new Range<>(2L, 3L)); when(mEventIndex3.getMostRecentActiveTimeSlot()).thenReturn(new Range<>(3L, 4L)); @@ -220,19 +229,19 @@ public final class ShareTargetPredictorTest { @Test public void testSortTargets() { AppTarget appTarget1 = new AppTarget.Builder( - new AppTargetId("cls1#pkg1"), PACKAGE_1, UserHandle.of(USER_ID)) + new AppTargetId("cls1#pkg1"), PACKAGE_1, UserHandle.of(USER_ID)) .setClassName(CLASS_1) .build(); AppTarget appTarget2 = new AppTarget.Builder( - new AppTargetId("cls2#pkg1"), PACKAGE_1, UserHandle.of(USER_ID)) + new AppTargetId("cls2#pkg1"), PACKAGE_1, UserHandle.of(USER_ID)) .setClassName(CLASS_2) .build(); AppTarget appTarget3 = new AppTarget.Builder( - new AppTargetId("cls1#pkg2"), PACKAGE_2, UserHandle.of(USER_ID)) + new AppTargetId("cls1#pkg2"), PACKAGE_2, UserHandle.of(USER_ID)) .setClassName(CLASS_1) .build(); AppTarget appTarget4 = new AppTarget.Builder( - new AppTargetId("cls2#pkg2"), PACKAGE_2, UserHandle.of(USER_ID)) + new AppTargetId("cls2#pkg2"), PACKAGE_2, UserHandle.of(USER_ID)) .setClassName(CLASS_2) .build(); AppTarget appTarget5 = new AppTarget.Builder( @@ -251,6 +260,10 @@ public final class ShareTargetPredictorTest { when(mEventHistory2.getEventIndex(anySet())).thenReturn(mEventIndex2); when(mEventHistory3.getEventIndex(anySet())).thenReturn(mEventIndex3); when(mEventHistory4.getEventIndex(anySet())).thenReturn(mEventIndex4); + when(mEventHistory1.getEventIndex(anyInt())).thenReturn(mEventIndex1); + when(mEventHistory2.getEventIndex(anyInt())).thenReturn(mEventIndex2); + when(mEventHistory3.getEventIndex(anyInt())).thenReturn(mEventIndex3); + when(mEventHistory4.getEventIndex(anyInt())).thenReturn(mEventIndex4); when(mEventIndex1.getMostRecentActiveTimeSlot()).thenReturn(new Range<>(1L, 2L)); when(mEventIndex2.getMostRecentActiveTimeSlot()).thenReturn(new Range<>(2L, 3L)); when(mEventIndex3.getMostRecentActiveTimeSlot()).thenReturn(new Range<>(3L, 4L)); @@ -265,14 +278,14 @@ public final class ShareTargetPredictorTest { appTarget4, appTarget3, appTarget2, appTarget1, appTarget5); } - private ShareShortcutInfo buildShareShortcut( + private static ShareShortcutInfo buildShareShortcut( String packageName, String className, String shortcutId) { ShortcutInfo shortcutInfo = buildShortcut(packageName, shortcutId); ComponentName componentName = new ComponentName(packageName, className); return new ShareShortcutInfo(shortcutInfo, componentName); } - private ShortcutInfo buildShortcut(String packageName, String shortcutId) { + private static ShortcutInfo buildShortcut(String packageName, String shortcutId) { Context mockContext = mock(Context.class); when(mockContext.getPackageName()).thenReturn(packageName); when(mockContext.getUserId()).thenReturn(USER_ID); diff --git a/services/tests/servicestests/src/com/android/server/people/prediction/SharesheetModelScorerTest.java b/services/tests/servicestests/src/com/android/server/people/prediction/SharesheetModelScorerTest.java new file mode 100644 index 000000000000..9d96d6b7d861 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/people/prediction/SharesheetModelScorerTest.java @@ -0,0 +1,406 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.people.prediction; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.prediction.AppTarget; +import android.app.prediction.AppTargetId; +import android.app.usage.UsageEvents; +import android.os.UserHandle; +import android.util.Range; + +import com.android.server.people.data.DataManager; +import com.android.server.people.data.Event; +import com.android.server.people.data.EventHistory; +import com.android.server.people.data.EventIndex; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.time.Duration; +import java.util.List; +import java.util.Map; + +@RunWith(JUnit4.class) +public final class SharesheetModelScorerTest { + + private static final int USER_ID = 0; + private static final String PACKAGE_1 = "pkg1"; + private static final String PACKAGE_2 = "pkg2"; + private static final String PACKAGE_3 = "pkg3"; + private static final String CLASS_1 = "cls1"; + private static final String CLASS_2 = "cls2"; + private static final double DELTA = 1e-6; + private static final long NOW = System.currentTimeMillis(); + private static final Range<Long> WITHIN_ONE_DAY = new Range( + NOW - Duration.ofHours(23).toMillis(), + NOW - Duration.ofHours(22).toMillis()); + private static final Range<Long> TWO_DAYS_AGO = new Range( + NOW - Duration.ofHours(50).toMillis(), + NOW - Duration.ofHours(49).toMillis()); + private static final Range<Long> FIVE_DAYS_AGO = new Range( + NOW - Duration.ofDays(6).toMillis(), + NOW - Duration.ofDays(5).toMillis()); + private static final Range<Long> EIGHT_DAYS_AGO = new Range( + NOW - Duration.ofDays(9).toMillis(), + NOW - Duration.ofDays(8).toMillis()); + private static final Range<Long> TWELVE_DAYS_AGO = new Range( + NOW - Duration.ofDays(13).toMillis(), + NOW - Duration.ofDays(12).toMillis()); + private static final Range<Long> TWENTY_DAYS_AGO = new Range( + NOW - Duration.ofDays(21).toMillis(), + NOW - Duration.ofDays(20).toMillis()); + private static final Range<Long> FOUR_WEEKS_AGO = new Range( + NOW - Duration.ofDays(29).toMillis(), + NOW - Duration.ofDays(28).toMillis()); + + @Mock + private DataManager mDataManager; + @Mock + private EventHistory mEventHistory1; + @Mock + private EventHistory mEventHistory2; + @Mock + private EventHistory mEventHistory3; + @Mock + private EventHistory mEventHistory4; + @Mock + private EventHistory mEventHistory5; + @Mock + private EventIndex mEventIndex1; + @Mock + private EventIndex mEventIndex2; + @Mock + private EventIndex mEventIndex3; + @Mock + private EventIndex mEventIndex4; + @Mock + private EventIndex mEventIndex5; + @Mock + private EventIndex mEventIndex6; + @Mock + private EventIndex mEventIndex7; + @Mock + private EventIndex mEventIndex8; + @Mock + private EventIndex mEventIndex9; + @Mock + private EventIndex mEventIndex10; + + private ShareTargetPredictor.ShareTarget mShareTarget1; + private ShareTargetPredictor.ShareTarget mShareTarget2; + private ShareTargetPredictor.ShareTarget mShareTarget3; + private ShareTargetPredictor.ShareTarget mShareTarget4; + private ShareTargetPredictor.ShareTarget mShareTarget5; + private ShareTargetPredictor.ShareTarget mShareTarget6; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mShareTarget1 = new ShareTargetPredictor.ShareTarget( + new AppTarget.Builder( + new AppTargetId("cls1#pkg1"), PACKAGE_1, UserHandle.of(USER_ID)) + .setClassName(CLASS_1).build(), + mEventHistory1, null); + mShareTarget2 = new ShareTargetPredictor.ShareTarget( + new AppTarget.Builder(new AppTargetId("cls2#pkg1"), PACKAGE_1, + UserHandle.of(USER_ID)).setClassName(CLASS_2).build(), + mEventHistory2, null); + mShareTarget3 = new ShareTargetPredictor.ShareTarget( + new AppTarget.Builder( + new AppTargetId("cls1#pkg2"), PACKAGE_2, UserHandle.of(USER_ID)) + .setClassName(CLASS_1).build(), + mEventHistory3, null); + mShareTarget4 = new ShareTargetPredictor.ShareTarget( + new AppTarget.Builder( + new AppTargetId("cls2#pkg2"), PACKAGE_2, UserHandle.of(USER_ID)) + .setClassName(CLASS_2).build(), + mEventHistory4, null); + mShareTarget5 = new ShareTargetPredictor.ShareTarget( + new AppTarget.Builder( + new AppTargetId("cls1#pkg3"), PACKAGE_3, UserHandle.of(USER_ID)) + .setClassName(CLASS_1).build(), + mEventHistory5, null); + mShareTarget6 = new ShareTargetPredictor.ShareTarget( + new AppTarget.Builder( + new AppTargetId("cls2#pkg3"), PACKAGE_3, UserHandle.of(USER_ID)) + .setClassName(CLASS_2).build(), + null, null); + } + + @Test + public void testComputeScore() { + // Frequency and recency + when(mEventHistory1.getEventIndex(anySet())).thenReturn(mEventIndex1); + when(mEventHistory2.getEventIndex(anySet())).thenReturn(mEventIndex2); + when(mEventHistory3.getEventIndex(anySet())).thenReturn(mEventIndex3); + when(mEventHistory4.getEventIndex(anySet())).thenReturn(mEventIndex4); + when(mEventHistory5.getEventIndex(anySet())).thenReturn(mEventIndex5); + + when(mEventIndex1.getActiveTimeSlots()).thenReturn( + List.of(WITHIN_ONE_DAY, TWO_DAYS_AGO, FIVE_DAYS_AGO)); + when(mEventIndex2.getActiveTimeSlots()).thenReturn(List.of(TWO_DAYS_AGO, TWELVE_DAYS_AGO)); + when(mEventIndex3.getActiveTimeSlots()).thenReturn(List.of(FIVE_DAYS_AGO, TWENTY_DAYS_AGO)); + when(mEventIndex4.getActiveTimeSlots()).thenReturn( + List.of(EIGHT_DAYS_AGO, TWELVE_DAYS_AGO, FOUR_WEEKS_AGO)); + when(mEventIndex5.getActiveTimeSlots()).thenReturn(List.of()); + + when(mEventIndex1.getMostRecentActiveTimeSlot()).thenReturn(WITHIN_ONE_DAY); + when(mEventIndex2.getMostRecentActiveTimeSlot()).thenReturn(TWO_DAYS_AGO); + when(mEventIndex3.getMostRecentActiveTimeSlot()).thenReturn(FIVE_DAYS_AGO); + when(mEventIndex4.getMostRecentActiveTimeSlot()).thenReturn(EIGHT_DAYS_AGO); + when(mEventIndex5.getMostRecentActiveTimeSlot()).thenReturn(null); + + // Frequency of the same mime type + when(mEventHistory1.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex6); + when(mEventHistory2.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex7); + when(mEventHistory3.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex8); + when(mEventHistory4.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex9); + when(mEventHistory5.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex10); + + when(mEventIndex6.getActiveTimeSlots()).thenReturn(List.of(TWO_DAYS_AGO)); + when(mEventIndex7.getActiveTimeSlots()).thenReturn(List.of(TWO_DAYS_AGO, TWELVE_DAYS_AGO)); + when(mEventIndex8.getActiveTimeSlots()).thenReturn(List.of()); + when(mEventIndex9.getActiveTimeSlots()).thenReturn(List.of(EIGHT_DAYS_AGO)); + when(mEventIndex10.getActiveTimeSlots()).thenReturn(List.of()); + + SharesheetModelScorer.computeScore( + List.of(mShareTarget1, mShareTarget2, mShareTarget3, mShareTarget4, mShareTarget5, + mShareTarget6), + Event.TYPE_SHARE_TEXT, + NOW); + + // Verification + assertEquals(0.514f, mShareTarget1.getScore(), DELTA); + assertEquals(0.475125f, mShareTarget2.getScore(), DELTA); + assertEquals(0.33f, mShareTarget3.getScore(), DELTA); + assertEquals(0.4411f, mShareTarget4.getScore(), DELTA); + assertEquals(0f, mShareTarget5.getScore(), DELTA); + assertEquals(0f, mShareTarget6.getScore(), DELTA); + } + + @Test + public void testComputeScoreForAppShare() { + // Frequency and recency + when(mEventHistory1.getEventIndex(anySet())).thenReturn(mEventIndex1); + when(mEventHistory2.getEventIndex(anySet())).thenReturn(mEventIndex2); + when(mEventHistory3.getEventIndex(anySet())).thenReturn(mEventIndex3); + when(mEventHistory4.getEventIndex(anySet())).thenReturn(mEventIndex4); + when(mEventHistory5.getEventIndex(anySet())).thenReturn(mEventIndex5); + + when(mEventIndex1.getActiveTimeSlots()).thenReturn( + List.of(WITHIN_ONE_DAY, TWO_DAYS_AGO, FIVE_DAYS_AGO)); + when(mEventIndex2.getActiveTimeSlots()).thenReturn(List.of(TWO_DAYS_AGO, TWELVE_DAYS_AGO)); + when(mEventIndex3.getActiveTimeSlots()).thenReturn(List.of(FIVE_DAYS_AGO, TWENTY_DAYS_AGO)); + when(mEventIndex4.getActiveTimeSlots()).thenReturn( + List.of(EIGHT_DAYS_AGO, TWELVE_DAYS_AGO, FOUR_WEEKS_AGO)); + when(mEventIndex5.getActiveTimeSlots()).thenReturn(List.of()); + + when(mEventIndex1.getMostRecentActiveTimeSlot()).thenReturn(WITHIN_ONE_DAY); + when(mEventIndex2.getMostRecentActiveTimeSlot()).thenReturn(TWO_DAYS_AGO); + when(mEventIndex3.getMostRecentActiveTimeSlot()).thenReturn(FIVE_DAYS_AGO); + when(mEventIndex4.getMostRecentActiveTimeSlot()).thenReturn(EIGHT_DAYS_AGO); + when(mEventIndex5.getMostRecentActiveTimeSlot()).thenReturn(null); + + // Frequency of the same mime type + when(mEventHistory1.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex6); + when(mEventHistory2.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex7); + when(mEventHistory3.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex8); + when(mEventHistory4.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex9); + when(mEventHistory5.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex10); + + when(mEventIndex6.getActiveTimeSlots()).thenReturn(List.of(TWO_DAYS_AGO)); + when(mEventIndex7.getActiveTimeSlots()).thenReturn(List.of(TWO_DAYS_AGO, TWELVE_DAYS_AGO)); + when(mEventIndex8.getActiveTimeSlots()).thenReturn(List.of()); + when(mEventIndex9.getActiveTimeSlots()).thenReturn(List.of(EIGHT_DAYS_AGO)); + when(mEventIndex10.getActiveTimeSlots()).thenReturn(List.of()); + + SharesheetModelScorer.computeScoreForAppShare( + List.of(mShareTarget1, mShareTarget2, mShareTarget3, mShareTarget4, mShareTarget5, + mShareTarget6), + Event.TYPE_SHARE_TEXT, 20, NOW, mDataManager, USER_ID); + + // Verification + assertEquals(0.514f, mShareTarget1.getScore(), DELTA); + assertEquals(0.475125f, mShareTarget2.getScore(), DELTA); + assertEquals(0.33f, mShareTarget3.getScore(), DELTA); + assertEquals(0.4411f, mShareTarget4.getScore(), DELTA); + assertEquals(0f, mShareTarget5.getScore(), DELTA); + assertEquals(0f, mShareTarget6.getScore(), DELTA); + } + + @Test + public void testComputeScoreForAppShare_promoteFrequentlyUsedApps() { + when(mEventHistory1.getEventIndex(anySet())).thenReturn(mEventIndex1); + when(mEventHistory2.getEventIndex(anySet())).thenReturn(mEventIndex2); + when(mEventHistory3.getEventIndex(anySet())).thenReturn(mEventIndex3); + when(mEventHistory4.getEventIndex(anySet())).thenReturn(mEventIndex4); + when(mEventHistory5.getEventIndex(anySet())).thenReturn(mEventIndex5); + when(mEventHistory1.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex6); + when(mEventHistory2.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex7); + when(mEventHistory3.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex8); + when(mEventHistory4.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex9); + when(mEventHistory5.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex10); + when(mDataManager.queryAppLaunchCount(anyInt(), anyLong(), anyLong(), anySet())) + .thenReturn( + Map.of(PACKAGE_1, 1, + PACKAGE_2, 2, + PACKAGE_3, 3)); + + SharesheetModelScorer.computeScoreForAppShare( + List.of(mShareTarget1, mShareTarget2, mShareTarget3, mShareTarget4, mShareTarget5, + mShareTarget6), + Event.TYPE_SHARE_TEXT, 20, NOW, mDataManager, USER_ID); + + verify(mDataManager, times(1)).queryAppLaunchCount(anyInt(), anyLong(), anyLong(), + anySet()); + assertEquals(0.9f, mShareTarget5.getScore(), DELTA); + assertEquals(0.81f, mShareTarget3.getScore(), DELTA); + assertEquals(0.729f, mShareTarget1.getScore(), DELTA); + assertEquals(0f, mShareTarget2.getScore(), DELTA); + assertEquals(0f, mShareTarget4.getScore(), DELTA); + assertEquals(0f, mShareTarget6.getScore(), DELTA); + } + + @Test + public void testComputeScoreForAppShare_skipPromoteFrequentlyUsedAppsWhenReachesLimit() { + when(mEventHistory1.getEventIndex(anySet())).thenReturn(mEventIndex1); + when(mEventHistory2.getEventIndex(anySet())).thenReturn(mEventIndex2); + when(mEventHistory3.getEventIndex(anySet())).thenReturn(mEventIndex3); + when(mEventHistory4.getEventIndex(anySet())).thenReturn(mEventIndex4); + when(mEventHistory5.getEventIndex(anySet())).thenReturn(mEventIndex5); + when(mEventHistory1.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex6); + when(mEventHistory2.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex7); + when(mEventHistory3.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex8); + when(mEventHistory4.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex9); + when(mEventHistory5.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex10); + when(mEventIndex1.getMostRecentActiveTimeSlot()).thenReturn(WITHIN_ONE_DAY); + when(mEventIndex2.getMostRecentActiveTimeSlot()).thenReturn(TWO_DAYS_AGO); + when(mEventIndex3.getMostRecentActiveTimeSlot()).thenReturn(FIVE_DAYS_AGO); + when(mEventIndex4.getMostRecentActiveTimeSlot()).thenReturn(EIGHT_DAYS_AGO); + when(mEventIndex5.getMostRecentActiveTimeSlot()).thenReturn(null); + when(mDataManager.queryAppLaunchCount(anyInt(), anyLong(), anyLong(), anySet())) + .thenReturn( + Map.of(PACKAGE_1, 1, + PACKAGE_2, 2, + PACKAGE_3, 3)); + + SharesheetModelScorer.computeScoreForAppShare( + List.of(mShareTarget1, mShareTarget2, mShareTarget3, mShareTarget4, mShareTarget5, + mShareTarget6), + Event.TYPE_SHARE_TEXT, 4, NOW, mDataManager, USER_ID); + + verify(mDataManager, never()).queryAppLaunchCount(anyInt(), anyLong(), anyLong(), anySet()); + assertEquals(0.4f, mShareTarget1.getScore(), DELTA); + assertEquals(0.35f, mShareTarget2.getScore(), DELTA); + assertEquals(0.33f, mShareTarget3.getScore(), DELTA); + assertEquals(0.31f, mShareTarget4.getScore(), DELTA); + assertEquals(0f, mShareTarget5.getScore(), DELTA); + assertEquals(0f, mShareTarget6.getScore(), DELTA); + } + + @Test + public void testComputeScoreForAppShare_promoteForegroundApp() { + when(mEventHistory1.getEventIndex(anySet())).thenReturn(mEventIndex1); + when(mEventHistory2.getEventIndex(anySet())).thenReturn(mEventIndex2); + when(mEventHistory3.getEventIndex(anySet())).thenReturn(mEventIndex3); + when(mEventHistory4.getEventIndex(anySet())).thenReturn(mEventIndex4); + when(mEventHistory5.getEventIndex(anySet())).thenReturn(mEventIndex5); + when(mEventHistory1.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex6); + when(mEventHistory2.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex7); + when(mEventHistory3.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex8); + when(mEventHistory4.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex9); + when(mEventHistory5.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex10); + when(mDataManager.queryAppMovingToForegroundEvents(anyInt(), anyLong(), + anyLong())).thenReturn( + List.of(createUsageEvent(PACKAGE_2), + createUsageEvent(PACKAGE_3), + createUsageEvent(SharesheetModelScorer.CHOOSER_ACTIVITY), + createUsageEvent(PACKAGE_3), + createUsageEvent(PACKAGE_3)) + ); + + SharesheetModelScorer.computeScoreForAppShare( + List.of(mShareTarget1, mShareTarget2, mShareTarget3, mShareTarget4, mShareTarget5, + mShareTarget6), + Event.TYPE_SHARE_TEXT, 20, NOW, mDataManager, USER_ID); + + verify(mDataManager, times(1)).queryAppMovingToForegroundEvents(anyInt(), anyLong(), + anyLong()); + assertEquals(0f, mShareTarget1.getScore(), DELTA); + assertEquals(0f, mShareTarget2.getScore(), DELTA); + assertEquals(SharesheetModelScorer.FOREGROUND_APP_WEIGHT, mShareTarget3.getScore(), DELTA); + assertEquals(0f, mShareTarget4.getScore(), DELTA); + assertEquals(0f, mShareTarget5.getScore(), DELTA); + assertEquals(0f, mShareTarget6.getScore(), DELTA); + } + + @Test + public void testComputeScoreForAppShare_skipPromoteForegroundAppWhenNoValidForegroundApp() { + when(mEventHistory1.getEventIndex(anySet())).thenReturn(mEventIndex1); + when(mEventHistory2.getEventIndex(anySet())).thenReturn(mEventIndex2); + when(mEventHistory3.getEventIndex(anySet())).thenReturn(mEventIndex3); + when(mEventHistory4.getEventIndex(anySet())).thenReturn(mEventIndex4); + when(mEventHistory5.getEventIndex(anySet())).thenReturn(mEventIndex5); + when(mEventHistory1.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex6); + when(mEventHistory2.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex7); + when(mEventHistory3.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex8); + when(mEventHistory4.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex9); + when(mEventHistory5.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex10); + when(mDataManager.queryAppMovingToForegroundEvents(anyInt(), anyLong(), + anyLong())).thenReturn( + List.of(createUsageEvent(PACKAGE_3), + createUsageEvent(PACKAGE_3), + createUsageEvent(SharesheetModelScorer.CHOOSER_ACTIVITY), + createUsageEvent(PACKAGE_3), + createUsageEvent(PACKAGE_3)) + ); + + SharesheetModelScorer.computeScoreForAppShare( + List.of(mShareTarget1, mShareTarget2, mShareTarget3, mShareTarget4, mShareTarget5, + mShareTarget6), + Event.TYPE_SHARE_TEXT, 20, NOW, mDataManager, USER_ID); + + verify(mDataManager, times(1)).queryAppMovingToForegroundEvents(anyInt(), anyLong(), + anyLong()); + assertEquals(0f, mShareTarget1.getScore(), DELTA); + assertEquals(0f, mShareTarget2.getScore(), DELTA); + assertEquals(0f, mShareTarget3.getScore(), DELTA); + assertEquals(0f, mShareTarget4.getScore(), DELTA); + assertEquals(0f, mShareTarget5.getScore(), DELTA); + assertEquals(0f, mShareTarget6.getScore(), DELTA); + } + + private static UsageEvents.Event createUsageEvent(String packageName) { + UsageEvents.Event e = new UsageEvents.Event(); + e.mPackage = packageName; + return e; + } +} |