Loading packages/SettingsLib/src/com/android/settingslib/applications/AppIconCacheManager.java +25 −1 Original line number Diff line number Diff line Loading @@ -22,13 +22,16 @@ import android.os.UserHandle; import android.util.Log; import android.util.LruCache; import androidx.annotation.VisibleForTesting; /** * Cache app icon for management. */ public class AppIconCacheManager { private static final String TAG = "AppIconCacheManager"; private static final float CACHE_RATIO = 0.1f; private static final int MAX_CACHE_SIZE_IN_KB = getMaxCacheInKb(); @VisibleForTesting static final int MAX_CACHE_SIZE_IN_KB = getMaxCacheInKb(); private static final String DELIMITER = ":"; private static AppIconCacheManager sAppIconCacheManager; private final LruCache<String, Drawable> mDrawableCache; Loading Loading @@ -109,4 +112,25 @@ public class AppIconCacheManager { private static int getMaxCacheInKb() { return Math.round(CACHE_RATIO * Runtime.getRuntime().maxMemory() / 1024); } /** * Clears as much memory as possible. * * @see android.content.ComponentCallbacks2#onTrimMemory(int) */ public void trimMemory(int level) { if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { // Time to clear everything if (sAppIconCacheManager != null) { sAppIconCacheManager.mDrawableCache.trimToSize(0); } } else if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN || level == android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) { // Tough time but still affordable, clear half of the cache if (sAppIconCacheManager != null) { final int maxSize = sAppIconCacheManager.mDrawableCache.maxSize(); sAppIconCacheManager.mDrawableCache.trimToSize(maxSize / 2); } } } } packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/AppIconCacheManagerTest.java +70 −0 Original line number Diff line number Diff line Loading @@ -34,11 +34,21 @@ import org.robolectric.RobolectricTestRunner; public class AppIconCacheManagerTest { private static final String APP_PACKAGE_NAME = "com.test.app"; private static final String APP_PACKAGE_NAME1 = "com.test.app1"; private static final String APP_PACKAGE_NAME2 = "com.test.app2"; private static final String APP_PACKAGE_NAME3 = "com.test.app3"; private static final int APP_UID = 9999; @Mock private Drawable mIcon; @Mock private Drawable mIcon1; @Mock private Drawable mIcon2; @Mock private Drawable mIcon3; private AppIconCacheManager mAppIconCacheManager; @Before Loading @@ -48,6 +58,29 @@ public class AppIconCacheManagerTest { doReturn(10).when(mIcon).getIntrinsicHeight(); doReturn(10).when(mIcon).getIntrinsicWidth(); doReturn(mIcon).when(mIcon).mutate(); // Algorithm for trim memory test: // The real maxsize is defined by AppIconCacheManager.MAX_CACHE_SIZE_IN_KB, and the size // of each element is calculated as following: // n * n * 4 / 1024 // In the testcase, we want to mock the maxsize of LruCache is 3, so the formula calculating // the size of each element will be like: // n * n * 4 / 1024 = maxsize / 3 // Thus, n = square_root(maxsize / 3 * 1024 / 4), which can be used as an icon size. final int iconSize = (int) Math.sqrt(AppIconCacheManager.MAX_CACHE_SIZE_IN_KB / 3f * 1024f / 4f); doReturn(iconSize).when(mIcon1).getIntrinsicHeight(); doReturn(iconSize).when(mIcon1).getIntrinsicWidth(); doReturn(mIcon1).when(mIcon1).mutate(); doReturn(iconSize).when(mIcon2).getIntrinsicHeight(); doReturn(iconSize).when(mIcon2).getIntrinsicWidth(); doReturn(mIcon2).when(mIcon2).mutate(); doReturn(iconSize).when(mIcon3).getIntrinsicHeight(); doReturn(iconSize).when(mIcon3).getIntrinsicWidth(); doReturn(mIcon3).when(mIcon3).mutate(); } @After Loading Loading @@ -106,4 +139,41 @@ public class AppIconCacheManagerTest { assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME, APP_UID)).isNull(); } @Test public void trimMemory_levelSatisfied_shouldNotCacheIcon() { mAppIconCacheManager.put(APP_PACKAGE_NAME1, APP_UID, mIcon1); mAppIconCacheManager.put(APP_PACKAGE_NAME2, APP_UID, mIcon2); mAppIconCacheManager.put(APP_PACKAGE_NAME3, APP_UID, mIcon3); // Expected to trim size to 0 final int level = android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND; mAppIconCacheManager.trimMemory(level); // None of the elements should be cached assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME1, APP_UID)).isNull(); assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME2, APP_UID)).isNull(); assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME3, APP_UID)).isNull(); } @Test public void trimMemory_levelSatisfied_shouldCacheAtLeastHalf() { mAppIconCacheManager.put(APP_PACKAGE_NAME1, APP_UID, mIcon1); mAppIconCacheManager.put(APP_PACKAGE_NAME2, APP_UID, mIcon2); mAppIconCacheManager.put(APP_PACKAGE_NAME3, APP_UID, mIcon3); // Get the last element mAppIconCacheManager.get(APP_PACKAGE_NAME1, APP_UID); // Expected to trim size to half of it, which is int( 3 / 2 ) = 1 final int level = android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL; mAppIconCacheManager.trimMemory(level); // There should be only one cached element, which is the last recently used one assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME1, APP_UID)).isNotNull(); assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME2, APP_UID)).isNull(); assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME3, APP_UID)).isNull(); } } Loading
packages/SettingsLib/src/com/android/settingslib/applications/AppIconCacheManager.java +25 −1 Original line number Diff line number Diff line Loading @@ -22,13 +22,16 @@ import android.os.UserHandle; import android.util.Log; import android.util.LruCache; import androidx.annotation.VisibleForTesting; /** * Cache app icon for management. */ public class AppIconCacheManager { private static final String TAG = "AppIconCacheManager"; private static final float CACHE_RATIO = 0.1f; private static final int MAX_CACHE_SIZE_IN_KB = getMaxCacheInKb(); @VisibleForTesting static final int MAX_CACHE_SIZE_IN_KB = getMaxCacheInKb(); private static final String DELIMITER = ":"; private static AppIconCacheManager sAppIconCacheManager; private final LruCache<String, Drawable> mDrawableCache; Loading Loading @@ -109,4 +112,25 @@ public class AppIconCacheManager { private static int getMaxCacheInKb() { return Math.round(CACHE_RATIO * Runtime.getRuntime().maxMemory() / 1024); } /** * Clears as much memory as possible. * * @see android.content.ComponentCallbacks2#onTrimMemory(int) */ public void trimMemory(int level) { if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) { // Time to clear everything if (sAppIconCacheManager != null) { sAppIconCacheManager.mDrawableCache.trimToSize(0); } } else if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN || level == android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) { // Tough time but still affordable, clear half of the cache if (sAppIconCacheManager != null) { final int maxSize = sAppIconCacheManager.mDrawableCache.maxSize(); sAppIconCacheManager.mDrawableCache.trimToSize(maxSize / 2); } } } }
packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/AppIconCacheManagerTest.java +70 −0 Original line number Diff line number Diff line Loading @@ -34,11 +34,21 @@ import org.robolectric.RobolectricTestRunner; public class AppIconCacheManagerTest { private static final String APP_PACKAGE_NAME = "com.test.app"; private static final String APP_PACKAGE_NAME1 = "com.test.app1"; private static final String APP_PACKAGE_NAME2 = "com.test.app2"; private static final String APP_PACKAGE_NAME3 = "com.test.app3"; private static final int APP_UID = 9999; @Mock private Drawable mIcon; @Mock private Drawable mIcon1; @Mock private Drawable mIcon2; @Mock private Drawable mIcon3; private AppIconCacheManager mAppIconCacheManager; @Before Loading @@ -48,6 +58,29 @@ public class AppIconCacheManagerTest { doReturn(10).when(mIcon).getIntrinsicHeight(); doReturn(10).when(mIcon).getIntrinsicWidth(); doReturn(mIcon).when(mIcon).mutate(); // Algorithm for trim memory test: // The real maxsize is defined by AppIconCacheManager.MAX_CACHE_SIZE_IN_KB, and the size // of each element is calculated as following: // n * n * 4 / 1024 // In the testcase, we want to mock the maxsize of LruCache is 3, so the formula calculating // the size of each element will be like: // n * n * 4 / 1024 = maxsize / 3 // Thus, n = square_root(maxsize / 3 * 1024 / 4), which can be used as an icon size. final int iconSize = (int) Math.sqrt(AppIconCacheManager.MAX_CACHE_SIZE_IN_KB / 3f * 1024f / 4f); doReturn(iconSize).when(mIcon1).getIntrinsicHeight(); doReturn(iconSize).when(mIcon1).getIntrinsicWidth(); doReturn(mIcon1).when(mIcon1).mutate(); doReturn(iconSize).when(mIcon2).getIntrinsicHeight(); doReturn(iconSize).when(mIcon2).getIntrinsicWidth(); doReturn(mIcon2).when(mIcon2).mutate(); doReturn(iconSize).when(mIcon3).getIntrinsicHeight(); doReturn(iconSize).when(mIcon3).getIntrinsicWidth(); doReturn(mIcon3).when(mIcon3).mutate(); } @After Loading Loading @@ -106,4 +139,41 @@ public class AppIconCacheManagerTest { assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME, APP_UID)).isNull(); } @Test public void trimMemory_levelSatisfied_shouldNotCacheIcon() { mAppIconCacheManager.put(APP_PACKAGE_NAME1, APP_UID, mIcon1); mAppIconCacheManager.put(APP_PACKAGE_NAME2, APP_UID, mIcon2); mAppIconCacheManager.put(APP_PACKAGE_NAME3, APP_UID, mIcon3); // Expected to trim size to 0 final int level = android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND; mAppIconCacheManager.trimMemory(level); // None of the elements should be cached assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME1, APP_UID)).isNull(); assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME2, APP_UID)).isNull(); assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME3, APP_UID)).isNull(); } @Test public void trimMemory_levelSatisfied_shouldCacheAtLeastHalf() { mAppIconCacheManager.put(APP_PACKAGE_NAME1, APP_UID, mIcon1); mAppIconCacheManager.put(APP_PACKAGE_NAME2, APP_UID, mIcon2); mAppIconCacheManager.put(APP_PACKAGE_NAME3, APP_UID, mIcon3); // Get the last element mAppIconCacheManager.get(APP_PACKAGE_NAME1, APP_UID); // Expected to trim size to half of it, which is int( 3 / 2 ) = 1 final int level = android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL; mAppIconCacheManager.trimMemory(level); // There should be only one cached element, which is the last recently used one assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME1, APP_UID)).isNotNull(); assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME2, APP_UID)).isNull(); assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME3, APP_UID)).isNull(); } }