Load number of unused apps in the background

The phone team flagged that this operation can take hundreds of
milliseconds. This change shows the item immediatley and loads the count
for the summary in the background.

This also includes the following changes:
- Always show the "Unused apps" setting item. The phone team also decided
to always show this item, instead of only showing when the count is
greater than 0. This is because otherwise the UI might skip around with
the background loading.
- Fix a bug with how the number of hibernated apps is counted. Copied
changes from ag/14471254

Bug: 191922683
Test: atest HibernatedAppsPreferenceControllerTest
Test: atest HibernatedAppsItemManagerTest
Change-Id: Ic80750eeb0b8dee639a78f9f868e79e437807643
diff --git a/src/com/android/car/settings/applications/AppsFragment.java b/src/com/android/car/settings/applications/AppsFragment.java
index e41092e..4be421d 100644
--- a/src/com/android/car/settings/applications/AppsFragment.java
+++ b/src/com/android/car/settings/applications/AppsFragment.java
@@ -33,6 +33,7 @@
 
     private RecentAppsItemManager mRecentAppsItemManager;
     private InstalledAppCountItemManager mInstalledAppCountItemManager;
+    private HibernatedAppsItemManager mHibernatedAppsItemManager;
 
     @Override
     @XmlRes
@@ -57,6 +58,10 @@
                 R.string.pk_applications_settings_screen_entry));
         mInstalledAppCountItemManager.addListener(use(RecentAppsViewAllPreferenceController.class,
                 R.string.pk_recent_apps_view_all));
+
+        mHibernatedAppsItemManager = new HibernatedAppsItemManager(context);
+        mHibernatedAppsItemManager.setListener(use(HibernatedAppsPreferenceController.class,
+                R.string.pk_hibernated_apps));
     }
 
     @Override
@@ -64,6 +69,7 @@
         super.onCreate(savedInstanceState);
         mRecentAppsItemManager.startLoading();
         mInstalledAppCountItemManager.startLoading();
+        mHibernatedAppsItemManager.startLoading();
     }
 
     /**
diff --git a/src/com/android/car/settings/applications/HibernatedAppsItemManager.java b/src/com/android/car/settings/applications/HibernatedAppsItemManager.java
new file mode 100644
index 0000000..c4ee12d
--- /dev/null
+++ b/src/com/android/car/settings/applications/HibernatedAppsItemManager.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2021 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.car.settings.applications;
+
+import static android.app.usage.UsageStatsManager.INTERVAL_MONTHLY;
+
+import android.app.usage.UsageStats;
+import android.app.usage.UsageStatsManager;
+import android.apphibernation.AppHibernationManager;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.provider.DeviceConfig;
+import android.util.ArrayMap;
+
+import androidx.annotation.NonNull;
+
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Class for fetching and returning the number of hibernated apps. Largely derived from
+ * {@link com.android.settings.applications.HibernatedAppsPreferenceController}.
+ */
+public class HibernatedAppsItemManager {
+    private static final String PROPERTY_HIBERNATION_UNUSED_THRESHOLD_MILLIS =
+            "auto_revoke_unused_threshold_millis2";
+    private static final long DEFAULT_UNUSED_THRESHOLD_MS = TimeUnit.DAYS.toMillis(90);
+
+    private final Context mContext;
+
+    private HibernatedAppsCountListener mListener;
+
+    public HibernatedAppsItemManager(Context context) {
+        mContext = context;
+    }
+
+    /**
+     * Starts fetching recently used apps
+     */
+    public void startLoading() {
+        ThreadUtils.postOnBackgroundThread(() -> {
+            int count = getNumHibernated();
+            ThreadUtils.postOnMainThread(() -> {
+                HibernatedAppsCountListener localListener = mListener;
+                if (localListener != null) {
+                    localListener.onHibernatedAppsCountLoaded(count);
+                }
+            });
+        });
+    }
+
+    /**
+     * Registers a listener that will be notified once the data is loaded.
+     */
+    public void setListener(@NonNull HibernatedAppsCountListener listener) {
+        mListener = listener;
+    }
+
+    private int getNumHibernated() {
+        // TODO(b/187465752): Find a way to export this logic from PermissionController module
+        PackageManager pm = mContext.getPackageManager();
+        AppHibernationManager ahm =
+                mContext.getSystemService(AppHibernationManager.class);
+        List<String> hibernatedPackages = ahm.getHibernatingPackagesForUser();
+        int numHibernated = hibernatedPackages.size();
+
+        // Also need to count packages that are auto revoked but not hibernated.
+        int numAutoRevoked = 0;
+        UsageStatsManager usm = mContext.getSystemService(UsageStatsManager.class);
+        long now = System.currentTimeMillis();
+        long unusedThreshold = DeviceConfig.getLong(DeviceConfig.NAMESPACE_PERMISSIONS,
+                PROPERTY_HIBERNATION_UNUSED_THRESHOLD_MILLIS, DEFAULT_UNUSED_THRESHOLD_MS);
+        List<UsageStats> usageStatsList = usm.queryUsageStats(INTERVAL_MONTHLY,
+                now - unusedThreshold, now);
+        Map<String, UsageStats> recentlyUsedPackages = new ArrayMap<>();
+        for (UsageStats us : usageStatsList) {
+            recentlyUsedPackages.put(us.mPackageName, us);
+        }
+        List<PackageInfo> packages = pm.getInstalledPackages(
+                PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.GET_PERMISSIONS);
+        for (PackageInfo pi : packages) {
+            String packageName = pi.packageName;
+            UsageStats usageStats = recentlyUsedPackages.get(packageName);
+            // Only count packages that have not been used recently as auto-revoked permissions may
+            // stay revoked even after use if the user has not regranted them.
+            boolean usedRecently = (usageStats != null
+                    && (now - usageStats.getLastTimeAnyComponentUsed() < unusedThreshold
+                    || now - usageStats.getLastTimeVisible() < unusedThreshold));
+            if (!hibernatedPackages.contains(packageName)
+                    && pi.requestedPermissions != null
+                    && !usedRecently) {
+                for (String perm : pi.requestedPermissions) {
+                    if ((pm.getPermissionFlags(perm, packageName, mContext.getUser())
+                            & PackageManager.FLAG_PERMISSION_AUTO_REVOKED) != 0) {
+                        numAutoRevoked++;
+                        break;
+                    }
+                }
+            }
+        }
+        return numHibernated + numAutoRevoked;
+    }
+
+
+    /**
+     * Callback that is called once the count of hibernated apps has been fetched.
+     */
+    public interface HibernatedAppsCountListener {
+        /**
+         * Called when the count of hibernated apps has loaded.
+         */
+        void onHibernatedAppsCountLoaded(int hibernatedAppsCount);
+    }
+}
diff --git a/src/com/android/car/settings/applications/HibernatedAppsPreferenceController.java b/src/com/android/car/settings/applications/HibernatedAppsPreferenceController.java
index 23f3a2a..b568c0a 100644
--- a/src/com/android/car/settings/applications/HibernatedAppsPreferenceController.java
+++ b/src/com/android/car/settings/applications/HibernatedAppsPreferenceController.java
@@ -18,11 +18,8 @@
 
 import static com.android.car.settings.applications.ApplicationsUtils.isHibernationEnabled;
 
-import android.apphibernation.AppHibernationManager;
 import android.car.drivingstate.CarUxRestrictions;
 import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
 
 import androidx.preference.Preference;
 
@@ -30,12 +27,11 @@
 import com.android.car.settings.common.FragmentController;
 import com.android.car.settings.common.PreferenceController;
 
-import java.util.List;
-
 /**
  * A preference controller handling the logic for updating summary of hibernated apps.
  */
-public final class HibernatedAppsPreferenceController extends PreferenceController<Preference> {
+public final class HibernatedAppsPreferenceController extends PreferenceController<Preference>
+        implements HibernatedAppsItemManager.HibernatedAppsCountListener {
     private static final String TAG = "HibernatedAppsPrefController";
 
     public HibernatedAppsPreferenceController(Context context, String preferenceKey,
@@ -51,39 +47,13 @@
 
     @Override
     public int getAvailabilityStatus() {
-        return isHibernationEnabled() && getNumHibernated() > 0
-                ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
+        return isHibernationEnabled() ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
     }
 
     @Override
-    protected void updateState(Preference preference) {
-        int numHibernated = getNumHibernated();
-        preference.setSummary(getContext().getResources().getQuantityString(
-                R.plurals.unused_apps_summary, numHibernated, numHibernated));
-    }
-
-    private int getNumHibernated() {
-        PackageManager pm = getContext().getPackageManager();
-        AppHibernationManager ahm =
-                getContext().getSystemService(AppHibernationManager.class);
-        List<String> hibernatedPackages = ahm.getHibernatingPackagesForUser();
-        int numHibernated = hibernatedPackages.size();
-
-        // Also need to count packages that are auto revoked but not hibernated.
-        List<PackageInfo> packages = pm.getInstalledPackages(
-                PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.GET_PERMISSIONS);
-        for (PackageInfo pi : packages) {
-            String packageName = pi.packageName;
-            if (!hibernatedPackages.contains(packageName) && pi.requestedPermissions != null) {
-                for (String perm : pi.requestedPermissions) {
-                    if ((pm.getPermissionFlags(perm, packageName, getContext().getUser())
-                            & PackageManager.FLAG_PERMISSION_AUTO_REVOKED) != 0) {
-                        numHibernated++;
-                        break;
-                    }
-                }
-            }
-        }
-        return numHibernated;
+    public void onHibernatedAppsCountLoaded(int hibernatedAppsCount) {
+        getPreference().setSummary(getContext().getResources().getQuantityString(
+                R.plurals.unused_apps_summary, hibernatedAppsCount, hibernatedAppsCount));
+        refreshUi();
     }
 }
diff --git a/tests/unit/src/com/android/car/settings/applications/HibernatedAppsItemManagerTest.java b/tests/unit/src/com/android/car/settings/applications/HibernatedAppsItemManagerTest.java
new file mode 100644
index 0000000..ba5697c
--- /dev/null
+++ b/tests/unit/src/com/android/car/settings/applications/HibernatedAppsItemManagerTest.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2021 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.car.settings.applications;
+
+import static android.provider.DeviceConfig.NAMESPACE_APP_HIBERNATION;
+
+import static com.android.car.settings.applications.ApplicationsUtils.PROPERTY_APP_HIBERNATION_ENABLED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.app.usage.IUsageStatsManager;
+import android.app.usage.UsageStats;
+import android.app.usage.UsageStatsManager;
+import android.apphibernation.AppHibernationManager;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ParceledListSlice;
+import android.content.res.Resources;
+import android.os.RemoteException;
+import android.provider.DeviceConfig;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+public class HibernatedAppsItemManagerTest {
+    private static final int CALLBACK_TIMEOUT_MS = 100;
+
+    private static final String HIBERNATED_PACKAGE_NAME = "hibernated_package";
+    private static final String AUTO_REVOKED_PACKAGE_NAME = "auto_revoked_package";
+    private static final String PERMISSION = "permission";
+    private final CountDownLatch mCountDownLatch = new CountDownLatch(1);
+    private final TestListener mHibernatedAppsCountListener = new TestListener();
+    @Mock
+    private PackageManager mPackageManager;
+    @Mock
+    private AppHibernationManager mAppHibernationManager;
+    @Mock
+    private IUsageStatsManager mIUsageStatsManager;
+    private Context mContext;
+    private HibernatedAppsItemManager mManager;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        DeviceConfig.setProperty(NAMESPACE_APP_HIBERNATION, PROPERTY_APP_HIBERNATION_ENABLED,
+                "true", false);
+        mContext = spy(ApplicationProvider.getApplicationContext());
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        when(mContext.getSystemService(AppHibernationManager.class))
+                .thenReturn(mAppHibernationManager);
+        when(mContext.getSystemService(UsageStatsManager.class)).thenReturn(
+                new UsageStatsManager(mContext, mIUsageStatsManager));
+        mManager = new HibernatedAppsItemManager(mContext);
+        mManager.setListener(mHibernatedAppsCountListener);
+    }
+
+
+    @Test
+    public void getSummary_getsRightCountForHibernatedPackage() throws Exception {
+        PackageInfo hibernatedPkg = getHibernatedPackage();
+        when(mPackageManager.getInstalledPackages(anyInt())).thenReturn(
+                Arrays.asList(hibernatedPkg, new PackageInfo()));
+        when(mContext.getResources()).thenReturn(mock(Resources.class));
+
+        mManager.startLoading();
+        mCountDownLatch.await(CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+
+        assertThat(mHibernatedAppsCountListener.mResult).isEqualTo(1);
+    }
+
+    @Test
+    public void getSummary_getsRightCountForUnusedAutoRevokedPackage() throws Exception {
+        PackageInfo autoRevokedPkg = getAutoRevokedPackage();
+        when(mPackageManager.getInstalledPackages(anyInt())).thenReturn(
+                Arrays.asList(autoRevokedPkg, new PackageInfo()));
+        when(mContext.getResources()).thenReturn(mock(Resources.class));
+
+        mManager.startLoading();
+        mCountDownLatch.await(CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+
+        assertThat(mHibernatedAppsCountListener.mResult).isEqualTo(1);
+    }
+
+    @Test
+    public void getSummary_getsRightCountForUsedAutoRevokedPackage() throws Exception {
+        PackageInfo usedAutoRevokedPkg = getAutoRevokedPackage();
+        setAutoRevokedPackageUsageStats();
+        when(mPackageManager.getInstalledPackages(anyInt())).thenReturn(
+                Arrays.asList(usedAutoRevokedPkg, new PackageInfo()));
+        when(mContext.getResources()).thenReturn(mock(Resources.class));
+
+        mManager.startLoading();
+        mCountDownLatch.await(CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+
+        assertThat(mHibernatedAppsCountListener.mResult).isEqualTo(0);
+    }
+
+
+    private PackageInfo getHibernatedPackage() {
+        PackageInfo pi = new PackageInfo();
+        pi.packageName = HIBERNATED_PACKAGE_NAME;
+        pi.requestedPermissions = new String[]{PERMISSION};
+        when(mAppHibernationManager.getHibernatingPackagesForUser())
+                .thenReturn(Arrays.asList(pi.packageName));
+        when(mPackageManager.getPermissionFlags(
+                pi.requestedPermissions[0], pi.packageName, mContext.getUser()))
+                .thenReturn(PackageManager.FLAG_PERMISSION_AUTO_REVOKED);
+        return pi;
+    }
+
+    private PackageInfo getAutoRevokedPackage() {
+        PackageInfo pi = new PackageInfo();
+        pi.packageName = AUTO_REVOKED_PACKAGE_NAME;
+        pi.requestedPermissions = new String[]{PERMISSION};
+        when(mPackageManager.getPermissionFlags(
+                pi.requestedPermissions[0], pi.packageName, mContext.getUser()))
+                .thenReturn(PackageManager.FLAG_PERMISSION_AUTO_REVOKED);
+        return pi;
+    }
+
+    private void setAutoRevokedPackageUsageStats() {
+        UsageStats us = new UsageStats();
+        us.mPackageName = AUTO_REVOKED_PACKAGE_NAME;
+        us.mLastTimeVisible = System.currentTimeMillis();
+        try {
+            when(mIUsageStatsManager.queryUsageStats(
+                    anyInt(), anyLong(), anyLong(), anyString(), anyInt()))
+                    .thenReturn(new ParceledListSlice(Arrays.asList(us)));
+        } catch (RemoteException e) {
+            // no-op
+        }
+    }
+
+    private class TestListener implements HibernatedAppsItemManager.HibernatedAppsCountListener {
+        int mResult;
+
+        @Override
+        public void onHibernatedAppsCountLoaded(int hibernatedAppsCount) {
+            mResult = hibernatedAppsCount;
+            mCountDownLatch.countDown();
+        }
+    };
+}
diff --git a/tests/unit/src/com/android/car/settings/applications/HibernatedAppsPreferenceControllerTest.java b/tests/unit/src/com/android/car/settings/applications/HibernatedAppsPreferenceControllerTest.java
index 4efa406..2f9a4d4 100644
--- a/tests/unit/src/com/android/car/settings/applications/HibernatedAppsPreferenceControllerTest.java
+++ b/tests/unit/src/com/android/car/settings/applications/HibernatedAppsPreferenceControllerTest.java
@@ -33,19 +33,16 @@
 import android.apphibernation.AppHibernationManager;
 import android.car.drivingstate.CarUxRestrictions;
 import android.content.Context;
-import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.provider.DeviceConfig;
 
-import androidx.lifecycle.LifecycleOwner;
 import androidx.preference.Preference;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import com.android.car.settings.common.FragmentController;
 import com.android.car.settings.common.PreferenceControllerTestUtil;
-import com.android.car.settings.testutils.TestLifecycleOwner;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -53,14 +50,9 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.util.Arrays;
-
 @RunWith(AndroidJUnit4.class)
 public class HibernatedAppsPreferenceControllerTest {
 
-    private static final String HIBERNATED_PACKAGE_NAME = "hibernated_package";
-    private static final String AUTO_REVOKED_PACKAGE_NAME = "auto_revoked_package";
-    private static final String PERMISSION = "permission";
     @Mock
     private FragmentController mFragmentController;
     @Mock
@@ -72,15 +64,11 @@
     private static final String KEY = "key";
     private Context mContext;
     private HibernatedAppsPreferenceController mController;
-    private PackageInfo mHibernatedPackage;
-    private PackageInfo mAutoRevokedPackage;
     private CarUxRestrictions mCarUxRestrictions;
-    private LifecycleOwner mLifecycleOwner;
 
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        mLifecycleOwner = new TestLifecycleOwner();
         mCarUxRestrictions = new CarUxRestrictions.Builder(/* reqOpt= */ true,
                 CarUxRestrictions.UX_RESTRICTIONS_BASELINE, /* timestamp= */ 0).build();
         DeviceConfig.setProperty(NAMESPACE_APP_HIBERNATION, PROPERTY_APP_HIBERNATION_ENABLED,
@@ -91,9 +79,6 @@
                 .thenReturn(mAppHibernationManager);
         mController = new HibernatedAppsPreferenceController(mContext, KEY, mFragmentController,
                 mCarUxRestrictions);
-        mHibernatedPackage =
-                getHibernatedPackage(mAppHibernationManager, mPackageManager, mContext);
-        mAutoRevokedPackage = getAutoRevokedPackage(mPackageManager, mContext);
     }
 
     @Test
@@ -105,41 +90,25 @@
     }
 
     @Test
-    public void getSummary_shouldReturnCorrectly() {
+    public void getAvailabilityStatus_featureEnabled_shouldReturnAvailable() {
+        DeviceConfig.setProperty(NAMESPACE_APP_HIBERNATION, PROPERTY_APP_HIBERNATION_ENABLED,
+                /* value= */ "true", /* makeDefault= */ true);
+
+        assertThat((mController).getAvailabilityStatus()).isEqualTo(AVAILABLE);
+    }
+
+    @Test
+    public void onHibernatedAppsCountCallback_setsSummary() {
         assignPreference();
-        when(mPackageManager.getInstalledPackages(anyInt())).thenReturn(
-                Arrays.asList(mHibernatedPackage, mAutoRevokedPackage, new PackageInfo()));
         when(mContext.getResources()).thenReturn(mock(Resources.class));
         int totalHibernated = 2;
 
-        mController.onCreate(mLifecycleOwner);
+        mController.onHibernatedAppsCountLoaded(totalHibernated);
 
         verify(mContext.getResources()).getQuantityString(
                 anyInt(), eq(totalHibernated), eq(totalHibernated));
     }
 
-    private static PackageInfo getHibernatedPackage(
-            AppHibernationManager apm, PackageManager pm, Context context) {
-        PackageInfo pi = new PackageInfo();
-        pi.packageName = HIBERNATED_PACKAGE_NAME;
-        pi.requestedPermissions = new String[]{PERMISSION};
-        when(apm.getHibernatingPackagesForUser()).thenReturn(Arrays.asList(pi.packageName));
-        when(pm.getPermissionFlags(
-                pi.requestedPermissions[0], pi.packageName, context.getUser()))
-                .thenReturn(PackageManager.FLAG_PERMISSION_AUTO_REVOKED);
-        return pi;
-    }
-
-    private static PackageInfo getAutoRevokedPackage(PackageManager pm, Context context) {
-        PackageInfo pi = new PackageInfo();
-        pi.packageName = AUTO_REVOKED_PACKAGE_NAME;
-        pi.requestedPermissions = new String[]{PERMISSION};
-        when(pm.getPermissionFlags(
-                pi.requestedPermissions[0], pi.packageName, context.getUser()))
-                .thenReturn(PackageManager.FLAG_PERMISSION_AUTO_REVOKED);
-        return pi;
-    }
-
     private void assignPreference() {
         PreferenceControllerTestUtil.assignPreference(mController,
                 mPreference);