blob: 0ac5724210da8e613612da29402fbfea006bafcd [file] [log] [blame]
/*
* 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;
}
}
}