diff options
2 files changed, 95 insertions, 1 deletions
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/AppIconCacheManager.java b/packages/SettingsLib/src/com/android/settingslib/applications/AppIconCacheManager.java index 9dfc8eaac024..c0117b952beb 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/AppIconCacheManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/AppIconCacheManager.java @@ -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; @@ -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); + } + } + } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/AppIconCacheManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/AppIconCacheManagerTest.java index 64f8bef1ecf3..1b0e1f1236c8 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/AppIconCacheManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/AppIconCacheManagerTest.java @@ -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 @@ -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 @@ -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(); + } } |