Loading src/com/android/settings/applications/AppDashboardFragment.java +4 −0 Original line number Original line Diff line number Diff line Loading @@ -69,6 +69,10 @@ public class AppDashboardFragment extends DashboardFragment { use(SpecialAppAccessPreferenceController.class).setSession(getSettingsLifecycle()); use(SpecialAppAccessPreferenceController.class).setSession(getSettingsLifecycle()); mAppsPreferenceController = use(AppsPreferenceController.class); mAppsPreferenceController = use(AppsPreferenceController.class); mAppsPreferenceController.setFragment(this /* fragment */); mAppsPreferenceController.setFragment(this /* fragment */); final HibernatedAppsPreferenceController hibernatedAppsPreferenceController = use(HibernatedAppsPreferenceController.class); getSettingsLifecycle().addObserver(hibernatedAppsPreferenceController); } } @Override @Override Loading src/com/android/settings/applications/HibernatedAppsPreferenceController.java +86 −7 Original line number Original line Diff line number Diff line Loading @@ -30,40 +30,111 @@ import android.content.pm.PackageManager; import android.provider.DeviceConfig; import android.provider.DeviceConfig; import android.util.ArrayMap; 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.R; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.BasePreferenceController; import com.google.common.annotations.VisibleForTesting; import java.util.List; import java.util.List; import java.util.Map; import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit; /** /** * A preference controller handling the logic for updating summary of hibernated apps. * 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 TAG = "HibernatedAppsPrefController"; private static final String PROPERTY_HIBERNATION_UNUSED_THRESHOLD_MILLIS = private static final String PROPERTY_HIBERNATION_UNUSED_THRESHOLD_MILLIS = "auto_revoke_unused_threshold_millis2"; "auto_revoke_unused_threshold_millis2"; private static final long DEFAULT_UNUSED_THRESHOLD_MS = TimeUnit.DAYS.toMillis(90); 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) { 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); super(context, preferenceKey); mBackgroundExecutor = bgExecutor; mMainExecutor = mainExecutor; } } @Override @Override public int getAvailabilityStatus() { public int getAvailabilityStatus() { return isHibernationEnabled() && getNumHibernated() > 0 return isHibernationEnabled() && mUnusedCount > 0 ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; } } @Override @Override public CharSequence getSummary() { public CharSequence getSummary() { final int numHibernated = getNumHibernated(); return mContext.getResources().getQuantityString( return mContext.getResources().getQuantityString( R.plurals.unused_apps_summary, numHibernated, numHibernated); R.plurals.unused_apps_summary, mUnusedCount, mUnusedCount); } @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); }); } } private int getNumHibernated() { @WorkerThread private int getUnusedCount() { // TODO(b/187465752): Find a way to export this logic from PermissionController module // TODO(b/187465752): Find a way to export this logic from PermissionController module final PackageManager pm = mContext.getPackageManager(); final PackageManager pm = mContext.getPackageManager(); final AppHibernationManager ahm = mContext.getSystemService(AppHibernationManager.class); final AppHibernationManager ahm = mContext.getSystemService(AppHibernationManager.class); Loading @@ -71,6 +142,7 @@ public final class HibernatedAppsPreferenceController extends BasePreferenceCont int numHibernated = hibernatedPackages.size(); int numHibernated = hibernatedPackages.size(); // Also need to count packages that are auto revoked but not hibernated. // Also need to count packages that are auto revoked but not hibernated. int numAutoRevoked = 0; final UsageStatsManager usm = mContext.getSystemService(UsageStatsManager.class); final UsageStatsManager usm = mContext.getSystemService(UsageStatsManager.class); final long now = System.currentTimeMillis(); final long now = System.currentTimeMillis(); final long unusedThreshold = DeviceConfig.getLong(DeviceConfig.NAMESPACE_PERMISSIONS, final long unusedThreshold = DeviceConfig.getLong(DeviceConfig.NAMESPACE_PERMISSIONS, Loading @@ -97,17 +169,24 @@ public final class HibernatedAppsPreferenceController extends BasePreferenceCont for (String perm : pi.requestedPermissions) { for (String perm : pi.requestedPermissions) { if ((pm.getPermissionFlags(perm, packageName, mContext.getUser()) if ((pm.getPermissionFlags(perm, packageName, mContext.getUser()) & PackageManager.FLAG_PERMISSION_AUTO_REVOKED) != 0) { & PackageManager.FLAG_PERMISSION_AUTO_REVOKED) != 0) { numHibernated++; numAutoRevoked++; break; break; } } } } } } } } return numHibernated; return numHibernated + numAutoRevoked; } } private static boolean isHibernationEnabled() { private static boolean isHibernationEnabled() { return DeviceConfig.getBoolean( return DeviceConfig.getBoolean( NAMESPACE_APP_HIBERNATION, PROPERTY_APP_HIBERNATION_ENABLED, false); 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); } } } tests/unit/src/com/android/settings/applications/HibernatedAppsPreferenceControllerTest.java +26 −4 Original line number Original line Diff line number Diff line Loading @@ -41,9 +41,13 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.pm.ParceledListSlice; import android.content.res.Resources; import android.content.res.Resources; import android.os.Looper; import android.os.RemoteException; import android.os.RemoteException; import android.provider.DeviceConfig; 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.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4; Loading @@ -67,12 +71,16 @@ public class HibernatedAppsPreferenceControllerTest { AppHibernationManager mAppHibernationManager; AppHibernationManager mAppHibernationManager; @Mock @Mock IUsageStatsManager mIUsageStatsManager; IUsageStatsManager mIUsageStatsManager; PreferenceScreen mPreferenceScreen; private static final String KEY = "key"; private static final String KEY = "key"; private Context mContext; private Context mContext; private HibernatedAppsPreferenceController mController; private HibernatedAppsPreferenceController mController; @Before @Before public void setUp() { public void setUp() { if (Looper.myLooper() == null) { Looper.prepare(); } MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this); DeviceConfig.setProperty(NAMESPACE_APP_HIBERNATION, PROPERTY_APP_HIBERNATION_ENABLED, DeviceConfig.setProperty(NAMESPACE_APP_HIBERNATION, PROPERTY_APP_HIBERNATION_ENABLED, "true", false); "true", false); Loading @@ -82,7 +90,15 @@ public class HibernatedAppsPreferenceControllerTest { .thenReturn(mAppHibernationManager); .thenReturn(mAppHibernationManager); when(mContext.getSystemService(UsageStatsManager.class)).thenReturn( when(mContext.getSystemService(UsageStatsManager.class)).thenReturn( new UsageStatsManager(mContext, mIUsageStatsManager)); 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 @Test Loading @@ -100,7 +116,9 @@ public class HibernatedAppsPreferenceControllerTest { Arrays.asList(hibernatedPkg, new PackageInfo())); Arrays.asList(hibernatedPkg, new PackageInfo())); when(mContext.getResources()).thenReturn(mock(Resources.class)); when(mContext.getResources()).thenReturn(mock(Resources.class)); mController.getSummary(); mController.displayPreference(mPreferenceScreen); mController.onResume(); verify(mContext.getResources()).getQuantityString(anyInt(), eq(1), eq(1)); verify(mContext.getResources()).getQuantityString(anyInt(), eq(1), eq(1)); } } Loading @@ -111,7 +129,9 @@ public class HibernatedAppsPreferenceControllerTest { Arrays.asList(autoRevokedPkg, new PackageInfo())); Arrays.asList(autoRevokedPkg, new PackageInfo())); when(mContext.getResources()).thenReturn(mock(Resources.class)); when(mContext.getResources()).thenReturn(mock(Resources.class)); mController.getSummary(); mController.displayPreference(mPreferenceScreen); mController.onResume(); verify(mContext.getResources()).getQuantityString(anyInt(), eq(1), eq(1)); verify(mContext.getResources()).getQuantityString(anyInt(), eq(1), eq(1)); } } Loading @@ -123,7 +143,9 @@ public class HibernatedAppsPreferenceControllerTest { Arrays.asList(usedAutoRevokedPkg, new PackageInfo())); Arrays.asList(usedAutoRevokedPkg, new PackageInfo())); when(mContext.getResources()).thenReturn(mock(Resources.class)); when(mContext.getResources()).thenReturn(mock(Resources.class)); mController.getSummary(); mController.displayPreference(mPreferenceScreen); mController.onResume(); verify(mContext.getResources()).getQuantityString(anyInt(), eq(0), eq(0)); verify(mContext.getResources()).getQuantityString(anyInt(), eq(0), eq(0)); } } Loading Loading
src/com/android/settings/applications/AppDashboardFragment.java +4 −0 Original line number Original line Diff line number Diff line Loading @@ -69,6 +69,10 @@ public class AppDashboardFragment extends DashboardFragment { use(SpecialAppAccessPreferenceController.class).setSession(getSettingsLifecycle()); use(SpecialAppAccessPreferenceController.class).setSession(getSettingsLifecycle()); mAppsPreferenceController = use(AppsPreferenceController.class); mAppsPreferenceController = use(AppsPreferenceController.class); mAppsPreferenceController.setFragment(this /* fragment */); mAppsPreferenceController.setFragment(this /* fragment */); final HibernatedAppsPreferenceController hibernatedAppsPreferenceController = use(HibernatedAppsPreferenceController.class); getSettingsLifecycle().addObserver(hibernatedAppsPreferenceController); } } @Override @Override Loading
src/com/android/settings/applications/HibernatedAppsPreferenceController.java +86 −7 Original line number Original line Diff line number Diff line Loading @@ -30,40 +30,111 @@ import android.content.pm.PackageManager; import android.provider.DeviceConfig; import android.provider.DeviceConfig; import android.util.ArrayMap; 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.R; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.BasePreferenceController; import com.google.common.annotations.VisibleForTesting; import java.util.List; import java.util.List; import java.util.Map; import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit; /** /** * A preference controller handling the logic for updating summary of hibernated apps. * 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 TAG = "HibernatedAppsPrefController"; private static final String PROPERTY_HIBERNATION_UNUSED_THRESHOLD_MILLIS = private static final String PROPERTY_HIBERNATION_UNUSED_THRESHOLD_MILLIS = "auto_revoke_unused_threshold_millis2"; "auto_revoke_unused_threshold_millis2"; private static final long DEFAULT_UNUSED_THRESHOLD_MS = TimeUnit.DAYS.toMillis(90); 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) { 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); super(context, preferenceKey); mBackgroundExecutor = bgExecutor; mMainExecutor = mainExecutor; } } @Override @Override public int getAvailabilityStatus() { public int getAvailabilityStatus() { return isHibernationEnabled() && getNumHibernated() > 0 return isHibernationEnabled() && mUnusedCount > 0 ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; } } @Override @Override public CharSequence getSummary() { public CharSequence getSummary() { final int numHibernated = getNumHibernated(); return mContext.getResources().getQuantityString( return mContext.getResources().getQuantityString( R.plurals.unused_apps_summary, numHibernated, numHibernated); R.plurals.unused_apps_summary, mUnusedCount, mUnusedCount); } @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); }); } } private int getNumHibernated() { @WorkerThread private int getUnusedCount() { // TODO(b/187465752): Find a way to export this logic from PermissionController module // TODO(b/187465752): Find a way to export this logic from PermissionController module final PackageManager pm = mContext.getPackageManager(); final PackageManager pm = mContext.getPackageManager(); final AppHibernationManager ahm = mContext.getSystemService(AppHibernationManager.class); final AppHibernationManager ahm = mContext.getSystemService(AppHibernationManager.class); Loading @@ -71,6 +142,7 @@ public final class HibernatedAppsPreferenceController extends BasePreferenceCont int numHibernated = hibernatedPackages.size(); int numHibernated = hibernatedPackages.size(); // Also need to count packages that are auto revoked but not hibernated. // Also need to count packages that are auto revoked but not hibernated. int numAutoRevoked = 0; final UsageStatsManager usm = mContext.getSystemService(UsageStatsManager.class); final UsageStatsManager usm = mContext.getSystemService(UsageStatsManager.class); final long now = System.currentTimeMillis(); final long now = System.currentTimeMillis(); final long unusedThreshold = DeviceConfig.getLong(DeviceConfig.NAMESPACE_PERMISSIONS, final long unusedThreshold = DeviceConfig.getLong(DeviceConfig.NAMESPACE_PERMISSIONS, Loading @@ -97,17 +169,24 @@ public final class HibernatedAppsPreferenceController extends BasePreferenceCont for (String perm : pi.requestedPermissions) { for (String perm : pi.requestedPermissions) { if ((pm.getPermissionFlags(perm, packageName, mContext.getUser()) if ((pm.getPermissionFlags(perm, packageName, mContext.getUser()) & PackageManager.FLAG_PERMISSION_AUTO_REVOKED) != 0) { & PackageManager.FLAG_PERMISSION_AUTO_REVOKED) != 0) { numHibernated++; numAutoRevoked++; break; break; } } } } } } } } return numHibernated; return numHibernated + numAutoRevoked; } } private static boolean isHibernationEnabled() { private static boolean isHibernationEnabled() { return DeviceConfig.getBoolean( return DeviceConfig.getBoolean( NAMESPACE_APP_HIBERNATION, PROPERTY_APP_HIBERNATION_ENABLED, false); 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); } } }
tests/unit/src/com/android/settings/applications/HibernatedAppsPreferenceControllerTest.java +26 −4 Original line number Original line Diff line number Diff line Loading @@ -41,9 +41,13 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.pm.ParceledListSlice; import android.content.res.Resources; import android.content.res.Resources; import android.os.Looper; import android.os.RemoteException; import android.os.RemoteException; import android.provider.DeviceConfig; 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.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4; Loading @@ -67,12 +71,16 @@ public class HibernatedAppsPreferenceControllerTest { AppHibernationManager mAppHibernationManager; AppHibernationManager mAppHibernationManager; @Mock @Mock IUsageStatsManager mIUsageStatsManager; IUsageStatsManager mIUsageStatsManager; PreferenceScreen mPreferenceScreen; private static final String KEY = "key"; private static final String KEY = "key"; private Context mContext; private Context mContext; private HibernatedAppsPreferenceController mController; private HibernatedAppsPreferenceController mController; @Before @Before public void setUp() { public void setUp() { if (Looper.myLooper() == null) { Looper.prepare(); } MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this); DeviceConfig.setProperty(NAMESPACE_APP_HIBERNATION, PROPERTY_APP_HIBERNATION_ENABLED, DeviceConfig.setProperty(NAMESPACE_APP_HIBERNATION, PROPERTY_APP_HIBERNATION_ENABLED, "true", false); "true", false); Loading @@ -82,7 +90,15 @@ public class HibernatedAppsPreferenceControllerTest { .thenReturn(mAppHibernationManager); .thenReturn(mAppHibernationManager); when(mContext.getSystemService(UsageStatsManager.class)).thenReturn( when(mContext.getSystemService(UsageStatsManager.class)).thenReturn( new UsageStatsManager(mContext, mIUsageStatsManager)); 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 @Test Loading @@ -100,7 +116,9 @@ public class HibernatedAppsPreferenceControllerTest { Arrays.asList(hibernatedPkg, new PackageInfo())); Arrays.asList(hibernatedPkg, new PackageInfo())); when(mContext.getResources()).thenReturn(mock(Resources.class)); when(mContext.getResources()).thenReturn(mock(Resources.class)); mController.getSummary(); mController.displayPreference(mPreferenceScreen); mController.onResume(); verify(mContext.getResources()).getQuantityString(anyInt(), eq(1), eq(1)); verify(mContext.getResources()).getQuantityString(anyInt(), eq(1), eq(1)); } } Loading @@ -111,7 +129,9 @@ public class HibernatedAppsPreferenceControllerTest { Arrays.asList(autoRevokedPkg, new PackageInfo())); Arrays.asList(autoRevokedPkg, new PackageInfo())); when(mContext.getResources()).thenReturn(mock(Resources.class)); when(mContext.getResources()).thenReturn(mock(Resources.class)); mController.getSummary(); mController.displayPreference(mPreferenceScreen); mController.onResume(); verify(mContext.getResources()).getQuantityString(anyInt(), eq(1), eq(1)); verify(mContext.getResources()).getQuantityString(anyInt(), eq(1), eq(1)); } } Loading @@ -123,7 +143,9 @@ public class HibernatedAppsPreferenceControllerTest { Arrays.asList(usedAutoRevokedPkg, new PackageInfo())); Arrays.asList(usedAutoRevokedPkg, new PackageInfo())); when(mContext.getResources()).thenReturn(mock(Resources.class)); when(mContext.getResources()).thenReturn(mock(Resources.class)); mController.getSummary(); mController.displayPreference(mPreferenceScreen); mController.onResume(); verify(mContext.getResources()).getQuantityString(anyInt(), eq(0), eq(0)); verify(mContext.getResources()).getQuantityString(anyInt(), eq(0), eq(0)); } } Loading