summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSong Hu <songhu@google.com>2020-03-20 05:40:33 -0700
committerSong Hu <songhu@google.com>2020-03-26 06:18:52 -0700
commit1d73eaf431d6cb39f998127f495b15e859169b5e (patch)
treeaddeefff022148722d9e2d9ed340b1c0ab24d577
parentedc7799657e725b73330da7b89f1539ce2ea7a7e (diff)
Factor freqeuncy and mimetype of past sharings, foreground app into
sharesheet model. Promote most frequent sharable apps to tackle cold start issue when device doesn't hold enough sharing history. Bug: 149822311 Test: manual test Sharesheet ranking on device Test: atest com.android.server.people.prediction.ShareTargetPredictorTest Test: atest com.android.server.people.prediction.SharesheetModelHelperTest Test: atest com.android.server.people.data.UsageStatsQueryHelperTest Change-Id: I6eb764448c0619cbc3b320b5f6432f559d1a15e1
-rw-r--r--services/people/java/com/android/server/people/data/DataManager.java31
-rw-r--r--services/people/java/com/android/server/people/data/UsageStatsQueryHelper.java58
-rw-r--r--services/people/java/com/android/server/people/prediction/ShareTargetPredictor.java59
-rw-r--r--services/people/java/com/android/server/people/prediction/SharesheetModelScorer.java406
-rw-r--r--services/tests/servicestests/src/com/android/server/people/data/UsageStatsQueryHelperTest.java81
-rw-r--r--services/tests/servicestests/src/com/android/server/people/prediction/ShareTargetPredictorTest.java25
-rw-r--r--services/tests/servicestests/src/com/android/server/people/prediction/SharesheetModelScorerTest.java406
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;
+ }
+}