Loading src/com/android/settings/applications/AppsPreferenceController.java +10 −11 Original line number Diff line number Diff line Loading @@ -20,7 +20,6 @@ import android.app.Application; 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; Loading Loading @@ -63,10 +62,9 @@ public class AppsPreferenceController extends BasePreferenceController implement 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 Loading @@ -83,7 +81,6 @@ public class AppsPreferenceController extends BasePreferenceController implement super(context, KEY_RECENT_APPS_CATEGORY); mApplicationsState = ApplicationsState.getInstance( (Application) mContext.getApplicationContext()); mUserId = UserHandle.myUserId(); } public void setFragment(Fragment fragment) { Loading Loading @@ -156,7 +153,7 @@ public class AppsPreferenceController extends BasePreferenceController implement } @VisibleForTesting List<UsageStats> loadRecentApps() { List<RecentAppStatsMixin.UsageStatsWrapper> loadRecentApps() { final RecentAppStatsMixin recentAppStatsMixin = new RecentAppStatsMixin(mContext, SHOW_RECENT_APP_COUNT); recentAppStatsMixin.loadDisplayableRecentApps(SHOW_RECENT_APP_COUNT); Loading Loading @@ -187,26 +184,28 @@ public class AppsPreferenceController extends BasePreferenceController implement } 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 -> { Loading src/com/android/settings/applications/RecentAppStatsMixin.java +91 −45 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import android.content.Intent; 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; Loading @@ -33,6 +34,7 @@ import android.util.Log; 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; Loading @@ -42,26 +44,31 @@ import com.android.settingslib.utils.ThreadUtils; 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; Loading @@ -80,10 +87,15 @@ public class RecentAppStatsMixin implements Comparator<UsageStats>, LifecycleObs 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<>(); Loading @@ -100,32 +112,56 @@ public class RecentAppStatsMixin implements Comparator<UsageStats>, LifecycleObs }); } @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(); Loading @@ -136,28 +172,15 @@ public class RecentAppStatsMixin implements Comparator<UsageStats>, LifecycleObs 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 " Loading @@ -169,26 +192,49 @@ public class RecentAppStatsMixin implements Comparator<UsageStats>, LifecycleObs 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); } } } tests/robotests/src/com/android/settings/applications/AppsPreferenceControllerTest.java +9 −4 Original line number Diff line number Diff line Loading @@ -70,7 +70,7 @@ public class AppsPreferenceControllerTest { private PreferenceScreen mScreen; private AppsPreferenceController mController; private List<UsageStats> mUsageStats; private List<RecentAppStatsMixin.UsageStatsWrapper> mUsageStats; private PreferenceCategory mRecentAppsCategory; private PreferenceCategory mGeneralCategory; private Preference mSeeAllPref; Loading Loading @@ -147,15 +147,15 @@ public class AppsPreferenceControllerTest { 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())) Loading @@ -164,4 +164,9 @@ public class AppsPreferenceControllerTest { .thenReturn(mAppEntry); mAppEntry.info = mApplicationInfo; } private static RecentAppStatsMixin.UsageStatsWrapper statsWrapperOf( UsageStats stats) { return new RecentAppStatsMixin.UsageStatsWrapper(stats, /* userId= */ 0); } } tests/robotests/src/com/android/settings/applications/RecentAppStatsMixinTest.java +104 −5 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat; 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; Loading Loading @@ -53,6 +54,7 @@ import org.robolectric.util.ReflectionHelpers; import java.util.ArrayList; import java.util.List; import java.util.Optional; @RunWith(RobolectricTestRunner.class) public class RecentAppStatsMixinTest { Loading @@ -60,6 +62,8 @@ public class RecentAppStatsMixinTest { @Mock private UsageStatsManager mUsageStatsManager; @Mock private UsageStatsManager mWorkUsageStatsManager; @Mock private UserManager mUserManager; @Mock private ApplicationsState mAppState; Loading Loading @@ -87,6 +91,8 @@ public class RecentAppStatsMixinTest { when(mUserManager.getProfileIdsWithDisabled(anyInt())).thenReturn(new int[]{}); mRecentAppStatsMixin = new RecentAppStatsMixin(context, 3 /* maximumApps */); ReflectionHelpers.setField(mRecentAppStatsMixin, "mWorkUsageStatsManager", Optional.of(mWorkUsageStatsManager)); } @Test Loading @@ -99,7 +105,7 @@ public class RecentAppStatsMixinTest { // 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); Loading Loading @@ -134,7 +140,7 @@ public class RecentAppStatsMixinTest { .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); Loading Loading @@ -170,7 +176,7 @@ public class RecentAppStatsMixinTest { .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); Loading Loading @@ -272,7 +278,7 @@ public class RecentAppStatsMixinTest { 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); Loading @@ -296,7 +302,7 @@ public class RecentAppStatsMixinTest { // 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); Loading @@ -306,4 +312,97 @@ public class RecentAppStatsMixinTest { 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); } } Loading
src/com/android/settings/applications/AppsPreferenceController.java +10 −11 Original line number Diff line number Diff line Loading @@ -20,7 +20,6 @@ import android.app.Application; 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; Loading Loading @@ -63,10 +62,9 @@ public class AppsPreferenceController extends BasePreferenceController implement 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 Loading @@ -83,7 +81,6 @@ public class AppsPreferenceController extends BasePreferenceController implement super(context, KEY_RECENT_APPS_CATEGORY); mApplicationsState = ApplicationsState.getInstance( (Application) mContext.getApplicationContext()); mUserId = UserHandle.myUserId(); } public void setFragment(Fragment fragment) { Loading Loading @@ -156,7 +153,7 @@ public class AppsPreferenceController extends BasePreferenceController implement } @VisibleForTesting List<UsageStats> loadRecentApps() { List<RecentAppStatsMixin.UsageStatsWrapper> loadRecentApps() { final RecentAppStatsMixin recentAppStatsMixin = new RecentAppStatsMixin(mContext, SHOW_RECENT_APP_COUNT); recentAppStatsMixin.loadDisplayableRecentApps(SHOW_RECENT_APP_COUNT); Loading Loading @@ -187,26 +184,28 @@ public class AppsPreferenceController extends BasePreferenceController implement } 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 -> { Loading
src/com/android/settings/applications/RecentAppStatsMixin.java +91 −45 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import android.content.Intent; 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; Loading @@ -33,6 +34,7 @@ import android.util.Log; 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; Loading @@ -42,26 +44,31 @@ import com.android.settingslib.utils.ThreadUtils; 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; Loading @@ -80,10 +87,15 @@ public class RecentAppStatsMixin implements Comparator<UsageStats>, LifecycleObs 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<>(); Loading @@ -100,32 +112,56 @@ public class RecentAppStatsMixin implements Comparator<UsageStats>, LifecycleObs }); } @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(); Loading @@ -136,28 +172,15 @@ public class RecentAppStatsMixin implements Comparator<UsageStats>, LifecycleObs 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 " Loading @@ -169,26 +192,49 @@ public class RecentAppStatsMixin implements Comparator<UsageStats>, LifecycleObs 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); } } }
tests/robotests/src/com/android/settings/applications/AppsPreferenceControllerTest.java +9 −4 Original line number Diff line number Diff line Loading @@ -70,7 +70,7 @@ public class AppsPreferenceControllerTest { private PreferenceScreen mScreen; private AppsPreferenceController mController; private List<UsageStats> mUsageStats; private List<RecentAppStatsMixin.UsageStatsWrapper> mUsageStats; private PreferenceCategory mRecentAppsCategory; private PreferenceCategory mGeneralCategory; private Preference mSeeAllPref; Loading Loading @@ -147,15 +147,15 @@ public class AppsPreferenceControllerTest { 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())) Loading @@ -164,4 +164,9 @@ public class AppsPreferenceControllerTest { .thenReturn(mAppEntry); mAppEntry.info = mApplicationInfo; } private static RecentAppStatsMixin.UsageStatsWrapper statsWrapperOf( UsageStats stats) { return new RecentAppStatsMixin.UsageStatsWrapper(stats, /* userId= */ 0); } }
tests/robotests/src/com/android/settings/applications/RecentAppStatsMixinTest.java +104 −5 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat; 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; Loading Loading @@ -53,6 +54,7 @@ import org.robolectric.util.ReflectionHelpers; import java.util.ArrayList; import java.util.List; import java.util.Optional; @RunWith(RobolectricTestRunner.class) public class RecentAppStatsMixinTest { Loading @@ -60,6 +62,8 @@ public class RecentAppStatsMixinTest { @Mock private UsageStatsManager mUsageStatsManager; @Mock private UsageStatsManager mWorkUsageStatsManager; @Mock private UserManager mUserManager; @Mock private ApplicationsState mAppState; Loading Loading @@ -87,6 +91,8 @@ public class RecentAppStatsMixinTest { when(mUserManager.getProfileIdsWithDisabled(anyInt())).thenReturn(new int[]{}); mRecentAppStatsMixin = new RecentAppStatsMixin(context, 3 /* maximumApps */); ReflectionHelpers.setField(mRecentAppStatsMixin, "mWorkUsageStatsManager", Optional.of(mWorkUsageStatsManager)); } @Test Loading @@ -99,7 +105,7 @@ public class RecentAppStatsMixinTest { // 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); Loading Loading @@ -134,7 +140,7 @@ public class RecentAppStatsMixinTest { .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); Loading Loading @@ -170,7 +176,7 @@ public class RecentAppStatsMixinTest { .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); Loading Loading @@ -272,7 +278,7 @@ public class RecentAppStatsMixinTest { 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); Loading @@ -296,7 +302,7 @@ public class RecentAppStatsMixinTest { // 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); Loading @@ -306,4 +312,97 @@ public class RecentAppStatsMixinTest { 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); } }