Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 21d62493 authored by Liahav Eitan's avatar Liahav Eitan Committed by Android (Google) Code Review
Browse files

Merge "Load work apps in RecentAppStatsMixin"

parents 3c2aebb2 82e81e23
Loading
Loading
Loading
Loading
+10 −11
Original line number Diff line number Diff line
@@ -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;

@@ -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
@@ -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) {
@@ -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);
@@ -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 -> {
+91 −45
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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;
@@ -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<>();
@@ -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();
@@ -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 "
@@ -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);
        }
    }
}
+9 −4
Original line number Diff line number Diff line
@@ -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;
@@ -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()))
@@ -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);
    }
}
+104 −5
Original line number Diff line number Diff line
@@ -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;
@@ -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 {
@@ -60,6 +62,8 @@ public class RecentAppStatsMixinTest {
    @Mock
    private UsageStatsManager mUsageStatsManager;
    @Mock
    private UsageStatsManager mWorkUsageStatsManager;
    @Mock
    private UserManager mUserManager;
    @Mock
    private ApplicationsState mAppState;
@@ -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
@@ -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);
@@ -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);
@@ -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);
@@ -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);
@@ -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);
@@ -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);
    }
}