Load work apps in RecentAppStatsMixin
This change will show work apps and personal apps together in the
recent apps settings page, sorted by decreasing last usage time.
Test: see both work and personal apps in the recent apps page in the
correct order.
Fix: 146921442
Change-Id: I174a556010529bc39c085cc006722bc2947535bd
diff --git a/src/com/android/settings/applications/AppsPreferenceController.java b/src/com/android/settings/applications/AppsPreferenceController.java
index fad513e..922ba3c 100644
--- a/src/com/android/settings/applications/AppsPreferenceController.java
+++ b/src/com/android/settings/applications/AppsPreferenceController.java
@@ -20,7 +20,6 @@
import android.app.usage.UsageStats;
import android.content.Context;
import android.icu.text.RelativeDateTimeFormatter;
-import android.os.UserHandle;
import android.text.TextUtils;
import android.util.ArrayMap;
@@ -63,10 +62,9 @@
static final String KEY_SEE_ALL = "see_all_apps";
private final ApplicationsState mApplicationsState;
- private final int mUserId;
@VisibleForTesting
- List<UsageStats> mRecentApps;
+ List<RecentAppStatsMixin.UsageStatsWrapper> mRecentApps;
@VisibleForTesting
PreferenceCategory mRecentAppsCategory;
@VisibleForTesting
@@ -83,7 +81,6 @@
super(context, KEY_RECENT_APPS_CATEGORY);
mApplicationsState = ApplicationsState.getInstance(
(Application) mContext.getApplicationContext());
- mUserId = UserHandle.myUserId();
}
public void setFragment(Fragment fragment) {
@@ -156,7 +153,7 @@
}
@VisibleForTesting
- List<UsageStats> loadRecentApps() {
+ List<RecentAppStatsMixin.UsageStatsWrapper> loadRecentApps() {
final RecentAppStatsMixin recentAppStatsMixin = new RecentAppStatsMixin(mContext,
SHOW_RECENT_APP_COUNT);
recentAppStatsMixin.loadDisplayableRecentApps(SHOW_RECENT_APP_COUNT);
@@ -187,26 +184,28 @@
}
int showAppsCount = 0;
- for (UsageStats stat : mRecentApps) {
- final String pkgName = stat.getPackageName();
+ for (RecentAppStatsMixin.UsageStatsWrapper statsWrapper : mRecentApps) {
+ final UsageStats stats = statsWrapper.mUsageStats;
+ final String pkgName = statsWrapper.mUsageStats.getPackageName();
+ final String key = pkgName + statsWrapper.mUserId;
final ApplicationsState.AppEntry appEntry =
- mApplicationsState.getEntry(pkgName, mUserId);
+ mApplicationsState.getEntry(pkgName, statsWrapper.mUserId);
if (appEntry == null) {
continue;
}
boolean rebindPref = true;
- Preference pref = existedAppPreferences.remove(pkgName);
+ Preference pref = existedAppPreferences.remove(key);
if (pref == null) {
pref = new AppPreference(mContext);
rebindPref = false;
}
- pref.setKey(pkgName);
+ pref.setKey(key);
pref.setTitle(appEntry.label);
pref.setIcon(Utils.getBadgedIcon(mContext, appEntry.info));
pref.setSummary(StringUtil.formatRelativeTime(mContext,
- System.currentTimeMillis() - stat.getLastTimeUsed(), false,
+ System.currentTimeMillis() - stats.getLastTimeUsed(), false,
RelativeDateTimeFormatter.Style.SHORT));
pref.setOrder(showAppsCount++);
pref.setOnPreferenceClickListener(preference -> {
diff --git a/src/com/android/settings/applications/RecentAppStatsMixin.java b/src/com/android/settings/applications/RecentAppStatsMixin.java
index 4bf3864..03b2203 100644
--- a/src/com/android/settings/applications/RecentAppStatsMixin.java
+++ b/src/com/android/settings/applications/RecentAppStatsMixin.java
@@ -26,6 +26,7 @@
import android.content.pm.PackageManager;
import android.os.PowerManager;
import android.os.UserHandle;
+import android.os.UserManager;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
@@ -33,6 +34,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
+import com.android.settings.Utils;
import com.android.settingslib.applications.AppUtils;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
@@ -42,26 +44,31 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
-import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
-
-public class RecentAppStatsMixin implements Comparator<UsageStats>, LifecycleObserver, OnStart {
+/**
+ * A helper class that loads recent app data in the background and sends it in a callback to a
+ * listener.
+ */
+public class RecentAppStatsMixin implements LifecycleObserver, OnStart {
private static final String TAG = "RecentAppStatsMixin";
private static final Set<String> SKIP_SYSTEM_PACKAGES = new ArraySet<>();
@VisibleForTesting
- final List<UsageStats> mRecentApps;
- private final int mUserId;
+ List<UsageStatsWrapper> mRecentApps;
+
private final int mMaximumApps;
private final Context mContext;
private final PackageManager mPm;
- private final PowerManager mPowerManager;;
- private final UsageStatsManager mUsageStatsManager;
+ private final PowerManager mPowerManager;
+ private final int mWorkUserId;
+ private final UsageStatsManager mPersonalUsageStatsManager;
+ private final Optional<UsageStatsManager> mWorkUsageStatsManager;
private final ApplicationsState mApplicationsState;
private final List<RecentAppStatsListener> mAppStatsListeners;
private Calendar mCalendar;
@@ -80,10 +87,15 @@
public RecentAppStatsMixin(Context context, int maximumApps) {
mContext = context;
mMaximumApps = maximumApps;
- mUserId = UserHandle.myUserId();
mPm = mContext.getPackageManager();
mPowerManager = mContext.getSystemService(PowerManager.class);
- mUsageStatsManager = mContext.getSystemService(UsageStatsManager.class);
+ final UserManager userManager = mContext.getSystemService(UserManager.class);
+ mWorkUserId = Utils.getManagedProfileId(userManager, UserHandle.myUserId());
+ mPersonalUsageStatsManager = mContext.getSystemService(UsageStatsManager.class);
+ final UserHandle workUserHandle = Utils.getManagedProfile(userManager);
+ mWorkUsageStatsManager = Optional.ofNullable(workUserHandle).map(
+ handle -> mContext.createContextAsUser(handle, /* flags */ 0)
+ .getSystemService(UsageStatsManager.class));
mApplicationsState = ApplicationsState.getInstance(
(Application) mContext.getApplicationContext());
mRecentApps = new ArrayList<>();
@@ -100,32 +112,56 @@
});
}
- @Override
- public final int compare(UsageStats a, UsageStats b) {
- // return by descending order
- return Long.compare(b.getLastTimeUsed(), a.getLastTimeUsed());
- }
-
public void addListener(@NonNull RecentAppStatsListener listener) {
mAppStatsListeners.add(listener);
}
@VisibleForTesting
- void loadDisplayableRecentApps(int number) {
+ void loadDisplayableRecentApps(int limit) {
mRecentApps.clear();
mCalendar = Calendar.getInstance();
mCalendar.add(Calendar.DAY_OF_YEAR, -1);
- final List<UsageStats> mStats = mPowerManager.isPowerSaveMode()
+
+ final int personalUserId = UserHandle.myUserId();
+ final List<UsageStats> personalStats =
+ getRecentAppsStats(mPersonalUsageStatsManager, personalUserId);
+ final List<UsageStats> workStats = mWorkUsageStatsManager
+ .map(statsManager -> getRecentAppsStats(statsManager, mWorkUserId))
+ .orElse(new ArrayList<>());
+
+ // Both lists are already sorted, so we can create a sorted merge in linear time
+ int personal = 0;
+ int work = 0;
+ while (personal < personalStats.size() && work < workStats.size()
+ && mRecentApps.size() < limit) {
+ UsageStats currentPersonal = personalStats.get(personal);
+ UsageStats currentWork = workStats.get(work);
+ if (currentPersonal.getLastTimeUsed() > currentWork.getLastTimeUsed()) {
+ mRecentApps.add(new UsageStatsWrapper(currentPersonal, personalUserId));
+ personal++;
+ } else {
+ mRecentApps.add(new UsageStatsWrapper(currentWork, mWorkUserId));
+ work++;
+ }
+ }
+ while (personal < personalStats.size() && mRecentApps.size() < limit) {
+ mRecentApps.add(new UsageStatsWrapper(personalStats.get(personal++), personalUserId));
+ }
+ while (work < workStats.size() && mRecentApps.size() < limit) {
+ mRecentApps.add(new UsageStatsWrapper(workStats.get(work++), mWorkUserId));
+ }
+ }
+
+ private List<UsageStats> getRecentAppsStats(UsageStatsManager usageStatsManager, int userId) {
+ final List<UsageStats> recentAppStats = mPowerManager.isPowerSaveMode()
? new ArrayList<>()
- : mUsageStatsManager.queryUsageStats(
+ : usageStatsManager.queryUsageStats(
UsageStatsManager.INTERVAL_BEST, mCalendar.getTimeInMillis(),
System.currentTimeMillis());
final Map<String, UsageStats> map = new ArrayMap<>();
- final int statCount = mStats.size();
- for (int i = 0; i < statCount; i++) {
- final UsageStats pkgStats = mStats.get(i);
- if (!shouldIncludePkgInRecents(pkgStats)) {
+ for (final UsageStats pkgStats : recentAppStats) {
+ if (!shouldIncludePkgInRecents(pkgStats, userId)) {
continue;
}
final String pkgName = pkgStats.getPackageName();
@@ -136,28 +172,15 @@
existingStats.add(pkgStats);
}
}
- final List<UsageStats> packageStats = new ArrayList<>();
- packageStats.addAll(map.values());
- Collections.sort(packageStats, this /* comparator */);
- int count = 0;
- for (UsageStats stat : packageStats) {
- final ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry(
- stat.getPackageName(), mUserId);
- if (appEntry == null) {
- continue;
- }
- mRecentApps.add(stat);
- count++;
- if (count >= number) {
- break;
- }
- }
+ final List<UsageStats> packageStats = new ArrayList<>(map.values());
+ packageStats.sort(Comparator.comparingLong(UsageStats::getLastTimeUsed).reversed());
+ return packageStats;
}
/**
* Whether or not the app should be included in recent list.
*/
- private boolean shouldIncludePkgInRecents(UsageStats stat) {
+ private boolean shouldIncludePkgInRecents(UsageStats stat, int userId) {
final String pkgName = stat.getPackageName();
if (stat.getLastTimeUsed() < mCalendar.getTimeInMillis()) {
Log.d(TAG, "Invalid timestamp (usage time is more than 24 hours ago), skipping "
@@ -169,26 +192,49 @@
Log.d(TAG, "System package, skipping " + pkgName);
return false;
}
+
if (AppUtils.isHiddenSystemModule(mContext, pkgName)) {
return false;
}
+
+ final ApplicationsState.AppEntry appEntry = mApplicationsState.getEntry(pkgName, userId);
+ if (appEntry == null) {
+ return false;
+ }
+
final Intent launchIntent = new Intent().addCategory(Intent.CATEGORY_LAUNCHER)
.setPackage(pkgName);
-
- if (mPm.resolveActivity(launchIntent, 0) == null) {
+ if (mPm.resolveActivityAsUser(launchIntent, 0, userId) == null) {
// Not visible on launcher -> likely not a user visible app, skip if non-instant.
- final ApplicationsState.AppEntry appEntry =
- mApplicationsState.getEntry(pkgName, mUserId);
- if (appEntry == null || appEntry.info == null || !AppUtils.isInstant(appEntry.info)) {
+ if (appEntry.info == null || !AppUtils.isInstant(appEntry.info)) {
Log.d(TAG, "Not a user visible or instant app, skipping " + pkgName);
return false;
}
}
+
return true;
}
public interface RecentAppStatsListener {
- void onReloadDataCompleted(List<UsageStats> recentApps);
+ /** A callback after loading the recent app data. */
+ void onReloadDataCompleted(List<UsageStatsWrapper> recentApps);
+ }
+
+ static class UsageStatsWrapper {
+
+ public final UsageStats mUsageStats;
+ public final int mUserId;
+
+ UsageStatsWrapper(UsageStats usageStats, int userId) {
+ mUsageStats = usageStats;
+ mUserId = userId;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("UsageStatsWrapper(pkg:%s,uid:%s)",
+ mUsageStats.getPackageName(), mUserId);
+ }
}
}
diff --git a/tests/robotests/src/com/android/settings/applications/AppsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/applications/AppsPreferenceControllerTest.java
index 75da4d8..d0bb754 100644
--- a/tests/robotests/src/com/android/settings/applications/AppsPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/applications/AppsPreferenceControllerTest.java
@@ -70,7 +70,7 @@
private PreferenceScreen mScreen;
private AppsPreferenceController mController;
- private List<UsageStats> mUsageStats;
+ private List<RecentAppStatsMixin.UsageStatsWrapper> mUsageStats;
private PreferenceCategory mRecentAppsCategory;
private PreferenceCategory mGeneralCategory;
private Preference mSeeAllPref;
@@ -147,15 +147,15 @@
final UsageStats stat3 = new UsageStats();
stat1.mLastTimeUsed = System.currentTimeMillis();
stat1.mPackageName = "pkg.class";
- mUsageStats.add(stat1);
+ mUsageStats.add(statsWrapperOf(stat1));
stat2.mLastTimeUsed = System.currentTimeMillis();
stat2.mPackageName = "pkg.class2";
- mUsageStats.add(stat2);
+ mUsageStats.add(statsWrapperOf(stat2));
stat3.mLastTimeUsed = System.currentTimeMillis();
stat3.mPackageName = "pkg.class3";
- mUsageStats.add(stat3);
+ mUsageStats.add(statsWrapperOf(stat3));
when(mAppState.getEntry(stat1.mPackageName, UserHandle.myUserId()))
.thenReturn(mAppEntry);
when(mAppState.getEntry(stat2.mPackageName, UserHandle.myUserId()))
@@ -164,4 +164,9 @@
.thenReturn(mAppEntry);
mAppEntry.info = mApplicationInfo;
}
+
+ private static RecentAppStatsMixin.UsageStatsWrapper statsWrapperOf(
+ UsageStats stats) {
+ return new RecentAppStatsMixin.UsageStatsWrapper(stats, /* userId= */ 0);
+ }
}
diff --git a/tests/robotests/src/com/android/settings/applications/RecentAppStatsMixinTest.java b/tests/robotests/src/com/android/settings/applications/RecentAppStatsMixinTest.java
index 0fb2a7e..6b94bce 100644
--- a/tests/robotests/src/com/android/settings/applications/RecentAppStatsMixinTest.java
+++ b/tests/robotests/src/com/android/settings/applications/RecentAppStatsMixinTest.java
@@ -21,6 +21,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
@@ -53,6 +54,7 @@
import java.util.ArrayList;
import java.util.List;
+import java.util.Optional;
@RunWith(RobolectricTestRunner.class)
public class RecentAppStatsMixinTest {
@@ -60,6 +62,8 @@
@Mock
private UsageStatsManager mUsageStatsManager;
@Mock
+ private UsageStatsManager mWorkUsageStatsManager;
+ @Mock
private UserManager mUserManager;
@Mock
private ApplicationsState mAppState;
@@ -87,6 +91,8 @@
when(mUserManager.getProfileIdsWithDisabled(anyInt())).thenReturn(new int[]{});
mRecentAppStatsMixin = new RecentAppStatsMixin(context, 3 /* maximumApps */);
+ ReflectionHelpers.setField(mRecentAppStatsMixin, "mWorkUsageStatsManager",
+ Optional.of(mWorkUsageStatsManager));
}
@Test
@@ -99,7 +105,7 @@
// stat1 is valid app.
when(mAppState.getEntry(stat1.mPackageName, UserHandle.myUserId()))
.thenReturn(mAppEntry);
- when(mPackageManager.resolveActivity(any(Intent.class), anyInt()))
+ when(mPackageManager.resolveActivityAsUser(any(Intent.class), anyInt(), anyInt()))
.thenReturn(new ResolveInfo());
when(mUsageStatsManager.queryUsageStats(anyInt(), anyLong(), anyLong()))
.thenReturn(stats);
@@ -134,7 +140,7 @@
.thenReturn(mAppEntry);
when(mAppState.getEntry(stat3.mPackageName, UserHandle.myUserId()))
.thenReturn(mAppEntry);
- when(mPackageManager.resolveActivity(any(Intent.class), anyInt()))
+ when(mPackageManager.resolveActivityAsUser(any(Intent.class), anyInt(), anyInt()))
.thenReturn(new ResolveInfo());
when(mUsageStatsManager.queryUsageStats(anyInt(), anyLong(), anyLong()))
.thenReturn(stats);
@@ -170,7 +176,7 @@
.thenReturn(mAppEntry);
when(mAppState.getEntry(stat3.mPackageName, UserHandle.myUserId()))
.thenReturn(null);
- when(mPackageManager.resolveActivity(any(Intent.class), anyInt()))
+ when(mPackageManager.resolveActivityAsUser(any(Intent.class), anyInt(), anyInt()))
.thenReturn(new ResolveInfo());
when(mUsageStatsManager.queryUsageStats(anyInt(), anyLong(), anyLong()))
.thenReturn(stats);
@@ -272,7 +278,7 @@
when(mPackageManager.getInstalledModules(anyInt() /* flags */))
.thenReturn(modules);
- when(mPackageManager.resolveActivity(any(Intent.class), anyInt()))
+ when(mPackageManager.resolveActivityAsUser(any(Intent.class), anyInt(), anyInt()))
.thenReturn(new ResolveInfo());
when(mUsageStatsManager.queryUsageStats(anyInt(), anyLong(), anyLong()))
.thenReturn(stats);
@@ -296,7 +302,7 @@
// stat1, stat2 are valid apps. stat3 is invalid.
when(mAppState.getEntry(stat1.mPackageName, UserHandle.myUserId()))
.thenReturn(mAppEntry);
- when(mPackageManager.resolveActivity(any(Intent.class), anyInt()))
+ when(mPackageManager.resolveActivityAsUser(any(Intent.class), anyInt(), anyInt()))
.thenReturn(new ResolveInfo());
when(mUsageStatsManager.queryUsageStats(anyInt(), anyLong(), anyLong()))
.thenReturn(stats);
@@ -306,4 +312,97 @@
assertThat(mRecentAppStatsMixin.mRecentApps).isEmpty();
}
+
+ @Test
+ public void loadDisplayableRecentApps_usePersonalAndWorkApps_shouldBeSortedByLastTimeUse() {
+ final List<UsageStats> personalStats = new ArrayList<>();
+ final UsageStats stats1 = new UsageStats();
+ final UsageStats stats2 = new UsageStats();
+ stats1.mLastTimeUsed = System.currentTimeMillis();
+ stats1.mPackageName = "personal.pkg.class";
+ personalStats.add(stats1);
+
+ stats2.mLastTimeUsed = System.currentTimeMillis() - 5000;
+ stats2.mPackageName = "personal.pkg.class2";
+ personalStats.add(stats2);
+
+ final List<UsageStats> workStats = new ArrayList<>();
+ final UsageStats stat3 = new UsageStats();
+ stat3.mLastTimeUsed = System.currentTimeMillis() - 2000;
+ stat3.mPackageName = "work.pkg.class3";
+ workStats.add(stat3);
+
+ when(mAppState.getEntry(anyString(), anyInt()))
+ .thenReturn(mAppEntry);
+ when(mPackageManager.resolveActivityAsUser(any(Intent.class), anyInt(), anyInt()))
+ .thenReturn(new ResolveInfo());
+ // personal app stats
+ when(mUsageStatsManager.queryUsageStats(anyInt(), anyLong(), anyLong()))
+ .thenReturn(personalStats);
+ // work app stats
+ when(mWorkUsageStatsManager.queryUsageStats(anyInt(), anyLong(), anyLong()))
+ .thenReturn(workStats);
+ mAppEntry.info = mApplicationInfo;
+
+ mRecentAppStatsMixin.loadDisplayableRecentApps(3);
+
+ assertThat(mRecentAppStatsMixin.mRecentApps.size()).isEqualTo(3);
+ assertThat(mRecentAppStatsMixin.mRecentApps.get(0).mUsageStats.mPackageName).isEqualTo(
+ "personal.pkg.class");
+ assertThat(mRecentAppStatsMixin.mRecentApps.get(1).mUsageStats.mPackageName).isEqualTo(
+ "work.pkg.class3");
+ assertThat(mRecentAppStatsMixin.mRecentApps.get(2).mUsageStats.mPackageName).isEqualTo(
+ "personal.pkg.class2");
+ }
+
+ @Test
+ public void loadDisplayableRecentApps_usePersonalAndWorkApps_shouldBeUniquePerProfile() {
+ final String firstAppPackageName = "app1.pkg.class";
+ final String secondAppPackageName = "app2.pkg.class";
+ final List<UsageStats> personalStats = new ArrayList<>();
+ final UsageStats personalStatsFirstApp = new UsageStats();
+ final UsageStats personalStatsFirstAppOlderUse = new UsageStats();
+ final UsageStats personalStatsSecondApp = new UsageStats();
+ personalStatsFirstApp.mLastTimeUsed = System.currentTimeMillis();
+ personalStatsFirstApp.mPackageName = firstAppPackageName;
+ personalStats.add(personalStatsFirstApp);
+
+ personalStatsFirstAppOlderUse.mLastTimeUsed = System.currentTimeMillis() - 5000;
+ personalStatsFirstAppOlderUse.mPackageName = firstAppPackageName;
+ personalStats.add(personalStatsFirstAppOlderUse);
+
+ personalStatsSecondApp.mLastTimeUsed = System.currentTimeMillis() - 2000;
+ personalStatsSecondApp.mPackageName = secondAppPackageName;
+ personalStats.add(personalStatsSecondApp);
+
+ final List<UsageStats> workStats = new ArrayList<>();
+ final UsageStats workStatsSecondApp = new UsageStats();
+ workStatsSecondApp.mLastTimeUsed = System.currentTimeMillis() - 1000;
+ workStatsSecondApp.mPackageName = secondAppPackageName;
+ workStats.add(workStatsSecondApp);
+
+ when(mAppState.getEntry(anyString(), anyInt()))
+ .thenReturn(mAppEntry);
+ when(mPackageManager.resolveActivityAsUser(any(Intent.class), anyInt(), anyInt()))
+ .thenReturn(new ResolveInfo());
+ // personal app stats
+ when(mUsageStatsManager.queryUsageStats(anyInt(), anyLong(), anyLong()))
+ .thenReturn(personalStats);
+ // work app stats
+ when(mWorkUsageStatsManager.queryUsageStats(anyInt(), anyLong(), anyLong()))
+ .thenReturn(workStats);
+ mAppEntry.info = mApplicationInfo;
+
+ mRecentAppStatsMixin.loadDisplayableRecentApps(3);
+
+ // The output should have the first app once since the duplicate use in the personal profile
+ // is filtered out, and the second app twice - once for each profile.
+ assertThat(mRecentAppStatsMixin.mRecentApps.size()).isEqualTo(3);
+ assertThat(mRecentAppStatsMixin.mRecentApps.get(0).mUsageStats.mPackageName).isEqualTo(
+ firstAppPackageName);
+ assertThat(mRecentAppStatsMixin.mRecentApps.get(1).mUsageStats.mPackageName).isEqualTo(
+ secondAppPackageName);
+ assertThat(mRecentAppStatsMixin.mRecentApps.get(2).mUsageStats.mPackageName).isEqualTo(
+ secondAppPackageName);
+ }
}