Promote frequently sharing apps as per monthly stats fetched from UsageStatsManager, in case PeopleService does not store enough sharing events when users just swicth to Sharesheet ranking in PeopleService.

Bug: 156320324
Test: atest com.android.server.people.prediction.ShareTargetPredictorTest
Test: atest com.android.server.people.prediction.SharesheetModelScorerTest
Test: atest com.android.server.people.data.UsageStatsQueryHelperTest

Change-Id: Iae7a3a8195ae4901cd1d9d673fe0cf8a27845488
This commit is contained in:
Song Hu
2020-05-14 10:12:53 -07:00
parent 1a66adcc56
commit bf9cb7e6e3
6 changed files with 224 additions and 81 deletions

View File

@@ -0,0 +1,52 @@
/*
* 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.data;
import com.android.internal.annotations.VisibleForTesting;
/** The data containing package usage info. */
public class AppUsageStatsData {
private int mLaunchCount;
private int mChosenCount;
@VisibleForTesting
public AppUsageStatsData(int chosenCount, int launchCount) {
this.mChosenCount = chosenCount;
this.mLaunchCount = launchCount;
}
public AppUsageStatsData() {
}
public int getLaunchCount() {
return mLaunchCount;
}
void incrementLaunchCountBy(int launchCount) {
this.mLaunchCount += launchCount;
}
public int getChosenCount() {
return mChosenCount;
}
void incrementChosenCountBy(int chosenCount) {
this.mChosenCount += chosenCount;
}
}

View File

@@ -257,13 +257,16 @@ public class DataManager {
}
/**
* Queries launch counts of apps within {@code packageNameFilter} between {@code startTime}
* and {@code endTime}.
* Queries usage stats of apps within {@code packageNameFilter} between {@code startTime} and
* {@code endTime}.
*
* @return a map which keys are package names and values are {@link AppUsageStatsData}.
*/
@NonNull
public Map<String, Integer> queryAppLaunchCount(@UserIdInt int callingUserId, long startTime,
public Map<String, AppUsageStatsData> queryAppUsageStats(
@UserIdInt int callingUserId, long startTime,
long endTime, Set<String> packageNameFilter) {
return UsageStatsQueryHelper.queryAppLaunchCount(callingUserId, startTime, endTime,
return UsageStatsQueryHelper.queryAppUsageStats(callingUserId, startTime, endTime,
packageNameFilter);
}

View File

@@ -137,27 +137,48 @@ class UsageStatsQueryHelper {
}
/**
* Queries {@link UsageStatsManagerInternal} for launch count of apps within {@code
* packageNameFilter} between {@code startTime} and {@code endTime}.obfuscateInstantApps
* Queries {@link UsageStatsManagerInternal} for usage stats of apps within {@code
* packageNameFilter} between {@code startTime} and {@code endTime}.
*
* @return a map which keys are package names and values are app launch counts.
* @return a map which keys are package names and values are {@link AppUsageStatsData}.
*/
static Map<String, Integer> queryAppLaunchCount(@UserIdInt int userId, long startTime,
static Map<String, AppUsageStatsData> queryAppUsageStats(@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<>();
Map<String, AppUsageStatsData> aggregatedStats = new ArrayMap<>();
for (UsageStats stat : stats) {
String packageName = stat.getPackageName();
if (packageNameFilter.contains(packageName)) {
aggregatedStats.put(packageName,
aggregatedStats.getOrDefault(packageName, 0) + stat.getAppLaunchCount());
AppUsageStatsData packageStats = aggregatedStats.computeIfAbsent(packageName,
(key) -> new AppUsageStatsData());
packageStats.incrementChosenCountBy(sumChooserCounts(stat.mChooserCounts));
packageStats.incrementLaunchCountBy(stat.getAppLaunchCount());
}
}
return aggregatedStats;
}
private static int sumChooserCounts(ArrayMap<String, ArrayMap<String, Integer>> chooserCounts) {
int sum = 0;
if (chooserCounts == null) {
return sum;
}
int chooserCountsSize = chooserCounts.size();
for (int i = 0; i < chooserCountsSize; i++) {
ArrayMap<String, Integer> counts = chooserCounts.valueAt(i);
if (counts == null) {
continue;
}
final int annotationSize = counts.size();
for (int j = 0; j < annotationSize; j++) {
sum += counts.valueAt(j);
}
}
return sum;
}
private void onInAppConversationEnded(@NonNull PackageData packageData,
@NonNull UsageEvents.Event endEvent) {
ComponentName activityName =

View File

@@ -27,17 +27,18 @@ import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.ChooserActivity;
import com.android.server.people.data.AppUsageStatsData;
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;
import java.util.function.Function;
/** Ranking scorer for Sharesheet targets. */
class SharesheetModelScorer {
@@ -50,8 +51,8 @@ class SharesheetModelScorer {
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 USAGE_STATS_CHOOSER_SCORE_INITIAL_DECAY = 0.9F;
private static final float FREQUENTLY_USED_APP_SCORE_INITIAL_DECAY = 0.3F;
private static final float FREQUENTLY_USED_APP_SCORE_DECAY = 0.9F;
@VisibleForTesting
static final float FOREGROUND_APP_WEIGHT = 0F;
@VisibleForTesting
@@ -192,14 +193,16 @@ class SharesheetModelScorer {
targetsList.add(index, shareTarget);
}
promoteForegroundApp(shareTargetMap, dataManager, callingUserId);
promoteFrequentlyUsedApps(shareTargetMap, targetsLimit, dataManager, callingUserId);
promoteMostChosenAndFrequentlyUsedApps(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)
* Promotes frequently chosen sharing apps and frequently used sharing apps as per
* UsageStatsManager, 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(
private static void promoteMostChosenAndFrequentlyUsedApps(
Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap, int targetsLimit,
@NonNull DataManager dataManager, @UserIdInt int callingUserId) {
int validPredictionNum = 0;
@@ -217,39 +220,50 @@ class SharesheetModelScorer {
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<>();
minValidScore *= FREQUENTLY_USED_APP_SCORE_INITIAL_DECAY;
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;
}
target.setScore(minValidScore);
minValidScore *= FREQUENTLY_USED_APP_SCORE_DECAY;
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;
Map<String, AppUsageStatsData> appStatsMap =
dataManager.queryAppUsageStats(
callingUserId, now - ONE_MONTH_WINDOW, now, shareTargetMap.keySet());
// Promotes frequently chosen sharing apps as per UsageStatsManager.
minValidScore = promoteApp(shareTargetMap, appStatsMap, AppUsageStatsData::getChosenCount,
USAGE_STATS_CHOOSER_SCORE_INITIAL_DECAY * minValidScore, minValidScore);
// Promotes frequently used sharing apps as per UsageStatsManager.
promoteApp(shareTargetMap, appStatsMap, AppUsageStatsData::getLaunchCount,
FREQUENTLY_USED_APP_SCORE_INITIAL_DECAY * minValidScore, minValidScore);
}
private static float promoteApp(
Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap,
Map<String, AppUsageStatsData> appStatsMap,
Function<AppUsageStatsData, Integer> countFunc, float baseScore, float minValidScore) {
int maxCount = 0;
for (AppUsageStatsData data : appStatsMap.values()) {
maxCount = Math.max(maxCount, countFunc.apply(data));
}
if (maxCount > 0) {
for (Map.Entry<String, AppUsageStatsData> entry : appStatsMap.entrySet()) {
if (!shareTargetMap.containsKey(entry.getKey())) {
continue;
}
ShareTargetPredictor.ShareTarget target = shareTargetMap.get(entry.getKey()).get(0);
if (target.getScore() > 0f) {
continue;
}
float curScore = baseScore * countFunc.apply(entry.getValue()) / maxCount;
target.setScore(curScore);
if (curScore > 0) {
minValidScore = Math.min(minValidScore, curScore);
}
if (DEBUG) {
Slog.d(TAG, String.format(
"SharesheetModel: promote as per AppUsageStats packageName: %s, "
+ "className: %s, total:%.2f",
target.getAppTarget().getPackageName(),
target.getAppTarget().getClassName(),
target.getScore()));
}
}
}
return minValidScore;
}
/**

View File

@@ -34,6 +34,7 @@ import android.app.usage.UsageStats;
import android.app.usage.UsageStatsManagerInternal;
import android.content.Context;
import android.content.LocusId;
import android.util.ArrayMap;
import androidx.test.InstrumentationRegistry;
@@ -196,39 +197,42 @@ public final class UsageStatsQueryHelperTest {
}
@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);
public void testQueryAppUsageStats() {
UsageStats packageStats1 = createUsageStats(PKG_NAME_1, 2, createDummyChooserCounts());
UsageStats packageStats2 = createUsageStats(PKG_NAME_1, 3, null);
UsageStats packageStats3 = createUsageStats(PKG_NAME_2, 1, createDummyChooserCounts());
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));
Map<String, AppUsageStatsData> appLaunchChooserCountCounts =
mHelper.queryAppUsageStats(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));
assertEquals(2, appLaunchChooserCountCounts.size());
assertEquals(4, (long) appLaunchChooserCountCounts.get(PKG_NAME_1).getChosenCount());
assertEquals(5, (long) appLaunchChooserCountCounts.get(PKG_NAME_1).getLaunchCount());
assertEquals(4, (long) appLaunchChooserCountCounts.get(PKG_NAME_2).getChosenCount());
assertEquals(1, (long) appLaunchChooserCountCounts.get(PKG_NAME_2).getLaunchCount());
}
@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);
public void testQueryAppUsageStats_packageNameFiltered() {
UsageStats packageStats1 = createUsageStats(PKG_NAME_1, 2, createDummyChooserCounts());
UsageStats packageStats2 = createUsageStats(PKG_NAME_1, 3, createDummyChooserCounts());
UsageStats packageStats3 = createUsageStats(PKG_NAME_2, 1, null);
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));
Map<String, AppUsageStatsData> appLaunchChooserCountCounts =
mHelper.queryAppUsageStats(USER_ID_PRIMARY, 90_000L,
200_000L,
Set.of(PKG_NAME_1));
assertEquals(1, appLaunchCounts.size());
assertEquals(5, (long) appLaunchCounts.get(PKG_NAME_1));
assertEquals(1, appLaunchChooserCountCounts.size());
assertEquals(8, (long) appLaunchChooserCountCounts.get(PKG_NAME_1).getChosenCount());
assertEquals(5, (long) appLaunchChooserCountCounts.get(PKG_NAME_1).getLaunchCount());
}
private void addUsageEvents(UsageEvents.Event... events) {
@@ -237,13 +241,27 @@ public final class UsageStatsQueryHelperTest {
anyInt())).thenReturn(usageEvents);
}
private static UsageStats createUsageStats(String packageName, int launchCount) {
private static UsageStats createUsageStats(String packageName, int launchCount,
ArrayMap<String, ArrayMap<String, Integer>> chooserCounts) {
UsageStats packageStats = new UsageStats();
packageStats.mPackageName = packageName;
packageStats.mAppLaunchCount = launchCount;
packageStats.mChooserCounts = chooserCounts;
return packageStats;
}
private static ArrayMap<String, ArrayMap<String, Integer>> createDummyChooserCounts() {
ArrayMap<String, ArrayMap<String, Integer>> chooserCounts = new ArrayMap<>();
ArrayMap<String, Integer> counts1 = new ArrayMap<>();
counts1.put("text", 2);
counts1.put("image", 1);
chooserCounts.put("intent1", counts1);
ArrayMap<String, Integer> counts2 = new ArrayMap<>();
counts2.put("video", 1);
chooserCounts.put("intent2", counts2);
return chooserCounts;
}
private static <T> void addLocalServiceMock(Class<T> clazz, T mock) {
LocalServices.removeServiceForTest(clazz);
LocalServices.addService(clazz, mock);

View File

@@ -31,6 +31,7 @@ import android.app.usage.UsageEvents;
import android.os.UserHandle;
import android.util.Range;
import com.android.server.people.data.AppUsageStatsData;
import com.android.server.people.data.DataManager;
import com.android.server.people.data.Event;
import com.android.server.people.data.EventHistory;
@@ -256,6 +257,39 @@ public final class SharesheetModelScorerTest {
assertEquals(0f, mShareTarget6.getScore(), DELTA);
}
@Test
public void testComputeScoreForAppShare_promoteFrequentlyChosenApps() {
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.queryAppUsageStats(anyInt(), anyLong(), anyLong(), anySet()))
.thenReturn(
Map.of(PACKAGE_1, new AppUsageStatsData(1, 0),
PACKAGE_2, new AppUsageStatsData(2, 0),
PACKAGE_3, new AppUsageStatsData(3, 0)));
SharesheetModelScorer.computeScoreForAppShare(
List.of(mShareTarget1, mShareTarget2, mShareTarget3, mShareTarget4, mShareTarget5,
mShareTarget6),
Event.TYPE_SHARE_TEXT, 20, NOW, mDataManager, USER_ID);
verify(mDataManager, times(1)).queryAppUsageStats(anyInt(), anyLong(), anyLong(),
anySet());
assertEquals(0.9f, mShareTarget5.getScore(), DELTA);
assertEquals(0.6f, mShareTarget3.getScore(), DELTA);
assertEquals(0.3f, mShareTarget1.getScore(), DELTA);
assertEquals(0f, mShareTarget2.getScore(), DELTA);
assertEquals(0f, mShareTarget4.getScore(), DELTA);
assertEquals(0f, mShareTarget6.getScore(), DELTA);
}
@Test
public void testComputeScoreForAppShare_promoteFrequentlyUsedApps() {
when(mEventHistory1.getEventIndex(anySet())).thenReturn(mEventIndex1);
@@ -268,22 +302,22 @@ public final class SharesheetModelScorerTest {
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()))
when(mDataManager.queryAppUsageStats(anyInt(), anyLong(), anyLong(), anySet()))
.thenReturn(
Map.of(PACKAGE_1, 1,
PACKAGE_2, 2,
PACKAGE_3, 3));
Map.of(PACKAGE_1, new AppUsageStatsData(0, 1),
PACKAGE_2, new AppUsageStatsData(0, 2),
PACKAGE_3, new AppUsageStatsData(1, 0)));
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(),
verify(mDataManager, times(1)).queryAppUsageStats(anyInt(), anyLong(), anyLong(),
anySet());
assertEquals(0.3f, mShareTarget5.getScore(), DELTA);
assertEquals(0.9f, mShareTarget5.getScore(), DELTA);
assertEquals(0.27f, mShareTarget3.getScore(), DELTA);
assertEquals(0.243f, mShareTarget1.getScore(), DELTA);
assertEquals(0.135f, mShareTarget1.getScore(), DELTA);
assertEquals(0f, mShareTarget2.getScore(), DELTA);
assertEquals(0f, mShareTarget4.getScore(), DELTA);
assertEquals(0f, mShareTarget6.getScore(), DELTA);
@@ -306,18 +340,19 @@ public final class SharesheetModelScorerTest {
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()))
when(mDataManager.queryAppUsageStats(anyInt(), anyLong(), anyLong(), anySet()))
.thenReturn(
Map.of(PACKAGE_1, 1,
PACKAGE_2, 2,
PACKAGE_3, 3));
Map.of(PACKAGE_1, new AppUsageStatsData(0, 1),
PACKAGE_2, new AppUsageStatsData(0, 2),
PACKAGE_3, new AppUsageStatsData(1, 0)));
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());
verify(mDataManager, never()).queryAppUsageStats(anyInt(), anyLong(), anyLong(),
anySet());
assertEquals(0.4f, mShareTarget1.getScore(), DELTA);
assertEquals(0.35f, mShareTarget2.getScore(), DELTA);
assertEquals(0.33f, mShareTarget3.getScore(), DELTA);