diff --git a/src/com/android/settings/applications/AppDashboardFragment.java b/src/com/android/settings/applications/AppDashboardFragment.java index 65f2b61cdd0..ff1259571f6 100644 --- a/src/com/android/settings/applications/AppDashboardFragment.java +++ b/src/com/android/settings/applications/AppDashboardFragment.java @@ -69,6 +69,10 @@ public class AppDashboardFragment extends DashboardFragment { use(SpecialAppAccessPreferenceController.class).setSession(getSettingsLifecycle()); mAppsPreferenceController = use(AppsPreferenceController.class); mAppsPreferenceController.setFragment(this /* fragment */); + + final HibernatedAppsPreferenceController hibernatedAppsPreferenceController = + use(HibernatedAppsPreferenceController.class); + getSettingsLifecycle().addObserver(hibernatedAppsPreferenceController); } @Override diff --git a/src/com/android/settings/applications/HibernatedAppsPreferenceController.java b/src/com/android/settings/applications/HibernatedAppsPreferenceController.java index 8d128112ad7..bf12b86c004 100644 --- a/src/com/android/settings/applications/HibernatedAppsPreferenceController.java +++ b/src/com/android/settings/applications/HibernatedAppsPreferenceController.java @@ -30,40 +30,111 @@ import android.content.pm.PackageManager; import android.provider.DeviceConfig; import android.util.ArrayMap; +import androidx.annotation.NonNull; +import androidx.annotation.WorkerThread; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + import com.android.settings.R; import com.android.settings.core.BasePreferenceController; +import com.google.common.annotations.VisibleForTesting; + import java.util.List; import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; /** * A preference controller handling the logic for updating summary of hibernated apps. */ -public final class HibernatedAppsPreferenceController extends BasePreferenceController { +public final class HibernatedAppsPreferenceController extends BasePreferenceController + implements LifecycleObserver { private static final String TAG = "HibernatedAppsPrefController"; 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 PreferenceScreen mScreen; + private int mUnusedCount = 0; + private boolean mLoadingUnusedApps; + private final Executor mBackgroundExecutor; + private final Executor mMainExecutor; public HibernatedAppsPreferenceController(Context context, String preferenceKey) { + this(context, preferenceKey, Executors.newSingleThreadExecutor(), + context.getMainExecutor()); + } + + @VisibleForTesting + HibernatedAppsPreferenceController(Context context, String preferenceKey, + Executor bgExecutor, Executor mainExecutor) { super(context, preferenceKey); + mBackgroundExecutor = bgExecutor; + mMainExecutor = mainExecutor; } @Override public int getAvailabilityStatus() { - return isHibernationEnabled() && getNumHibernated() > 0 + return isHibernationEnabled() && mUnusedCount > 0 ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; } @Override public CharSequence getSummary() { - final int numHibernated = getNumHibernated(); return mContext.getResources().getQuantityString( - R.plurals.unused_apps_summary, numHibernated, numHibernated); + R.plurals.unused_apps_summary, mUnusedCount, mUnusedCount); } - private int getNumHibernated() { + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mScreen = screen; + } + + /** + * On lifecycle resume event. + */ + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + public void onResume() { + updatePreference(); + } + + private void updatePreference() { + if (mScreen == null) { + return; + } + if (!mLoadingUnusedApps) { + loadUnusedCount(unusedCount -> { + mUnusedCount = unusedCount; + mLoadingUnusedApps = false; + mMainExecutor.execute(() -> { + super.displayPreference(mScreen); + Preference pref = mScreen.findPreference(mPreferenceKey); + refreshSummary(pref); + }); + }); + mLoadingUnusedApps = true; + } + } + + /** + * Asynchronously load the count of unused apps. + * + * @param callback callback to call when the number of unused apps is calculated + */ + private void loadUnusedCount(@NonNull UnusedCountLoadedCallback callback) { + mBackgroundExecutor.execute(() -> { + final int unusedCount = getUnusedCount(); + callback.onUnusedCountLoaded(unusedCount); + }); + } + + @WorkerThread + private int getUnusedCount() { // TODO(b/187465752): Find a way to export this logic from PermissionController module final PackageManager pm = mContext.getPackageManager(); final AppHibernationManager ahm = mContext.getSystemService(AppHibernationManager.class); @@ -71,6 +142,7 @@ public final class HibernatedAppsPreferenceController extends BasePreferenceCont int numHibernated = hibernatedPackages.size(); // Also need to count packages that are auto revoked but not hibernated. + int numAutoRevoked = 0; final UsageStatsManager usm = mContext.getSystemService(UsageStatsManager.class); final long now = System.currentTimeMillis(); final long unusedThreshold = DeviceConfig.getLong(DeviceConfig.NAMESPACE_PERMISSIONS, @@ -97,17 +169,24 @@ public final class HibernatedAppsPreferenceController extends BasePreferenceCont for (String perm : pi.requestedPermissions) { if ((pm.getPermissionFlags(perm, packageName, mContext.getUser()) & PackageManager.FLAG_PERMISSION_AUTO_REVOKED) != 0) { - numHibernated++; + numAutoRevoked++; break; } } } } - return numHibernated; + return numHibernated + numAutoRevoked; } private static boolean isHibernationEnabled() { return DeviceConfig.getBoolean( NAMESPACE_APP_HIBERNATION, PROPERTY_APP_HIBERNATION_ENABLED, false); } + + /** + * Callback for when we've determined the number of unused apps. + */ + private interface UnusedCountLoadedCallback { + void onUnusedCountLoaded(int unusedCount); + } } diff --git a/tests/unit/src/com/android/settings/applications/HibernatedAppsPreferenceControllerTest.java b/tests/unit/src/com/android/settings/applications/HibernatedAppsPreferenceControllerTest.java index 39c966d00f1..06829837a5f 100644 --- a/tests/unit/src/com/android/settings/applications/HibernatedAppsPreferenceControllerTest.java +++ b/tests/unit/src/com/android/settings/applications/HibernatedAppsPreferenceControllerTest.java @@ -41,9 +41,13 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.res.Resources; +import android.os.Looper; import android.os.RemoteException; import android.provider.DeviceConfig; +import androidx.preference.Preference; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -67,12 +71,16 @@ public class HibernatedAppsPreferenceControllerTest { AppHibernationManager mAppHibernationManager; @Mock IUsageStatsManager mIUsageStatsManager; + PreferenceScreen mPreferenceScreen; private static final String KEY = "key"; private Context mContext; private HibernatedAppsPreferenceController mController; @Before public void setUp() { + if (Looper.myLooper() == null) { + Looper.prepare(); + } MockitoAnnotations.initMocks(this); DeviceConfig.setProperty(NAMESPACE_APP_HIBERNATION, PROPERTY_APP_HIBERNATION_ENABLED, "true", false); @@ -82,7 +90,15 @@ public class HibernatedAppsPreferenceControllerTest { .thenReturn(mAppHibernationManager); when(mContext.getSystemService(UsageStatsManager.class)).thenReturn( new UsageStatsManager(mContext, mIUsageStatsManager)); - mController = new HibernatedAppsPreferenceController(mContext, KEY); + + PreferenceManager manager = new PreferenceManager(mContext); + mPreferenceScreen = manager.createPreferenceScreen(mContext); + Preference preference = mock(Preference.class); + when(preference.getKey()).thenReturn(KEY); + mPreferenceScreen.addPreference(preference); + + mController = new HibernatedAppsPreferenceController(mContext, KEY, + command -> command.run(), command -> command.run()); } @Test @@ -100,7 +116,9 @@ public class HibernatedAppsPreferenceControllerTest { Arrays.asList(hibernatedPkg, new PackageInfo())); when(mContext.getResources()).thenReturn(mock(Resources.class)); - mController.getSummary(); + mController.displayPreference(mPreferenceScreen); + mController.onResume(); + verify(mContext.getResources()).getQuantityString(anyInt(), eq(1), eq(1)); } @@ -111,7 +129,9 @@ public class HibernatedAppsPreferenceControllerTest { Arrays.asList(autoRevokedPkg, new PackageInfo())); when(mContext.getResources()).thenReturn(mock(Resources.class)); - mController.getSummary(); + mController.displayPreference(mPreferenceScreen); + mController.onResume(); + verify(mContext.getResources()).getQuantityString(anyInt(), eq(1), eq(1)); } @@ -123,7 +143,9 @@ public class HibernatedAppsPreferenceControllerTest { Arrays.asList(usedAutoRevokedPkg, new PackageInfo())); when(mContext.getResources()).thenReturn(mock(Resources.class)); - mController.getSummary(); + mController.displayPreference(mPreferenceScreen); + mController.onResume(); + verify(mContext.getResources()).getQuantityString(anyInt(), eq(0), eq(0)); }