diff options
| author | 2022-01-07 02:01:50 +0800 | |
|---|---|---|
| committer | 2022-01-27 17:23:00 +0800 | |
| commit | eefcdeefb357bd142f810237eacecb7a8548f929 (patch) | |
| tree | 1b96ba2d577addfdfbedf8c12dbe419f50d18285 | |
| parent | c5900ec5711c49f4e81e050a3913254000f34771 (diff) | |
Create a new icon cache mechanism for memory improvement
- Avoid loading all app icons at once to decrease memory usage.
- Implement AppIconCacheManager with LruCache to optimize cache usage.
- Create related APIs to access the icon cache conveniently.
- Enable the new cache mechanism for the Settings process.
Bug: 187118427
Bug: 209898662
Test: robotests & check memory trace of apps pages.
Change-Id: I3843d7ed00812b6923e3c6dc175f98d507f81b7b
6 files changed, 526 insertions, 12 deletions
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/AppIconCacheManager.java b/packages/SettingsLib/src/com/android/settingslib/applications/AppIconCacheManager.java new file mode 100644 index 000000000000..9dfc8eaac024 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/applications/AppIconCacheManager.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2022 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.settingslib.applications; + +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.UserHandle; +import android.util.Log; +import android.util.LruCache; + +/** + * 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(); + private static final String DELIMITER = ":"; + private static AppIconCacheManager sAppIconCacheManager; + private final LruCache<String, Drawable> mDrawableCache; + + private AppIconCacheManager() { + mDrawableCache = new LruCache<String, Drawable>(MAX_CACHE_SIZE_IN_KB) { + @Override + protected int sizeOf(String key, Drawable drawable) { + if (drawable instanceof BitmapDrawable) { + return ((BitmapDrawable) drawable).getBitmap().getByteCount() / 1024; + } + // Rough estimate each pixel will use 4 bytes by default. + return drawable.getIntrinsicHeight() * drawable.getIntrinsicWidth() * 4 / 1024; + } + }; + } + + /** + * Get an {@link AppIconCacheManager} instance. + */ + public static synchronized AppIconCacheManager getInstance() { + if (sAppIconCacheManager == null) { + sAppIconCacheManager = new AppIconCacheManager(); + } + return sAppIconCacheManager; + } + + /** + * Put app icon to cache + * + * @param packageName of icon + * @param uid of packageName + * @param drawable app icon + */ + public void put(String packageName, int uid, Drawable drawable) { + final String key = getKey(packageName, uid); + if (key == null || drawable == null || drawable.getIntrinsicHeight() < 0 + || drawable.getIntrinsicWidth() < 0) { + Log.w(TAG, "Invalid key or drawable."); + return; + } + mDrawableCache.put(key, drawable); + } + + /** + * Get app icon from cache. + * + * @param packageName of icon + * @param uid of packageName + * @return app icon + */ + public Drawable get(String packageName, int uid) { + final String key = getKey(packageName, uid); + if (key == null) { + Log.w(TAG, "Invalid key with package or uid."); + return null; + } + final Drawable cachedDrawable = mDrawableCache.get(key); + return cachedDrawable != null ? cachedDrawable.mutate() : null; + } + + /** + * Release cache. + */ + public static void release() { + if (sAppIconCacheManager != null) { + sAppIconCacheManager.mDrawableCache.evictAll(); + } + } + + private static String getKey(String packageName, int uid) { + if (packageName == null || uid < 0) { + return null; + } + return packageName + DELIMITER + UserHandle.getUserId(uid); + } + + private static int getMaxCacheInKb() { + return Math.round(CACHE_RATIO * Runtime.getRuntime().maxMemory() / 1024); + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java index a5da8b6bd15e..cc4fef8399c3 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java @@ -25,6 +25,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.graphics.drawable.Drawable; import android.hardware.usb.IUsbManager; import android.net.Uri; import android.os.Environment; @@ -35,7 +36,9 @@ import android.text.TextUtils; import android.util.Log; import com.android.settingslib.R; +import com.android.settingslib.Utils; import com.android.settingslib.applications.instantapps.InstantAppDataProvider; +import com.android.settingslib.utils.ThreadUtils; import java.util.ArrayList; import java.util.List; @@ -212,4 +215,82 @@ public class AppUtils { UserHandle.myUserId()); return TextUtils.equals(packageName, defaultBrowserPackage); } + + /** + * Get the app icon by app entry. + * + * @param context caller's context + * @param appEntry AppEntry of ApplicationsState + * @return app icon of the app entry + */ + public static Drawable getIcon(Context context, ApplicationsState.AppEntry appEntry) { + if (appEntry == null || appEntry.info == null) { + return null; + } + + final AppIconCacheManager appIconCacheManager = AppIconCacheManager.getInstance(); + final String packageName = appEntry.info.packageName; + final int uid = appEntry.info.uid; + + Drawable icon = appIconCacheManager.get(packageName, uid); + if (icon == null) { + if (appEntry.apkFile != null && appEntry.apkFile.exists()) { + icon = Utils.getBadgedIcon(context, appEntry.info); + appIconCacheManager.put(packageName, uid, icon); + } else { + setAppEntryMounted(appEntry, /* mounted= */ false); + icon = context.getDrawable( + com.android.internal.R.drawable.sym_app_on_sd_unavailable_icon); + } + } else if (!appEntry.mounted && appEntry.apkFile != null && appEntry.apkFile.exists()) { + // If the app wasn't mounted but is now mounted, reload its icon. + setAppEntryMounted(appEntry, /* mounted= */ true); + icon = Utils.getBadgedIcon(context, appEntry.info); + appIconCacheManager.put(packageName, uid, icon); + } + + return icon; + } + + /** + * Get the app icon from cache by app entry. + * + * @param appEntry AppEntry of ApplicationsState + * @return app icon of the app entry + */ + public static Drawable getIconFromCache(ApplicationsState.AppEntry appEntry) { + return appEntry == null || appEntry.info == null ? null + : AppIconCacheManager.getInstance().get( + appEntry.info.packageName, + appEntry.info.uid); + } + + /** + * Preload the top N icons of app entry list. + * + * @param context caller's context + * @param appEntries AppEntry list of ApplicationsState + * @param number the number of Top N icons of the appEntries + */ + public static void preloadTopIcons(Context context, + ArrayList<ApplicationsState.AppEntry> appEntries, int number) { + if (appEntries == null || appEntries.isEmpty() || number <= 0) { + return; + } + + for (int i = 0; i < Math.min(appEntries.size(), number); i++) { + final ApplicationsState.AppEntry entry = appEntries.get(i); + ThreadUtils.postOnBackgroundThread(() -> { + getIcon(context, entry); + }); + } + } + + private static void setAppEntryMounted(ApplicationsState.AppEntry appEntry, boolean mounted) { + if (appEntry.mounted != mounted) { + synchronized (appEntry) { + appEntry.mounted = mounted; + } + } + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java index f046f06cc691..fdb06072bbd1 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java @@ -95,6 +95,7 @@ public class ApplicationsState { private static final Object sLock = new Object(); private static final Pattern REMOVE_DIACRITICALS_PATTERN = Pattern.compile("\\p{InCombiningDiacriticalMarks}+"); + private static final String SETTING_PKG = "com.android.settings"; @VisibleForTesting static ApplicationsState sInstance; @@ -492,6 +493,9 @@ public class ApplicationsState { return null; } + /** + * Starting Android T, this method will not be used if {@link AppIconCacheManager} is applied. + */ public void ensureIcon(AppEntry entry) { if (entry.icon != null) { return; @@ -758,6 +762,10 @@ public class ApplicationsState { return null; } + private static boolean isAppIconCacheEnabled(Context context) { + return SETTING_PKG.equals(context.getPackageName()); + } + void rebuildActiveSessions() { synchronized (mEntriesMap) { if (!mSessionsChanged) { @@ -806,6 +814,11 @@ public class ApplicationsState { } else { mHasLifecycle = false; } + + if (isAppIconCacheEnabled(mContext)) { + // Skip the preloading all icons step to save memory usage. + mFlags = mFlags & ~FLAG_SESSION_REQUEST_ICONS; + } } @SessionFlags @@ -814,7 +827,12 @@ public class ApplicationsState { } public void setSessionFlags(@SessionFlags int flags) { - mFlags = flags; + if (isAppIconCacheEnabled(mContext)) { + // Skip the preloading all icons step to save memory usage. + mFlags = flags & ~FLAG_SESSION_REQUEST_ICONS; + } else { + mFlags = flags; + } } @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) @@ -1576,6 +1594,10 @@ public class ApplicationsState { // Need to synchronize on 'this' for the following. public ApplicationInfo info; + /** + * Starting Android T, this field will not be used if {@link AppIconCacheManager} is + * applied. + */ public Drawable icon; public String sizeStr; public String internalSizeStr; @@ -1596,15 +1618,11 @@ public class ApplicationsState { this.size = SIZE_UNKNOWN; this.sizeStale = true; ensureLabel(context); - // Speed up the cache of the icon and label description if they haven't been created. - ThreadUtils.postOnBackgroundThread(() -> { - if (this.icon == null) { - this.ensureIconLocked(context); - } - if (this.labelDescription == null) { - this.ensureLabelDescriptionLocked(context); - } - }); + // Speed up the cache of the label description if they haven't been created. + if (this.labelDescription == null) { + ThreadUtils.postOnBackgroundThread( + () -> this.ensureLabelDescriptionLocked(context)); + } } public void ensureLabel(Context context) { @@ -1620,7 +1638,15 @@ public class ApplicationsState { } } + /** + * Starting Android T, this method will not be used if {@link AppIconCacheManager} is + * applied. + */ boolean ensureIconLocked(Context context) { + if (isAppIconCacheEnabled(context)) { + return false; + } + if (this.icon == null) { if (this.apkFile.exists()) { this.icon = Utils.getBadgedIcon(context, info); 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 new file mode 100644 index 000000000000..64f8bef1ecf3 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/AppIconCacheManagerTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2022 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.settingslib.applications; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.doReturn; + +import android.graphics.drawable.Drawable; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class AppIconCacheManagerTest { + + private static final String APP_PACKAGE_NAME = "com.test.app"; + private static final int APP_UID = 9999; + + @Mock + private Drawable mIcon; + + private AppIconCacheManager mAppIconCacheManager; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mAppIconCacheManager = AppIconCacheManager.getInstance(); + doReturn(10).when(mIcon).getIntrinsicHeight(); + doReturn(10).when(mIcon).getIntrinsicWidth(); + doReturn(mIcon).when(mIcon).mutate(); + } + + @After + public void tearDown() { + AppIconCacheManager.release(); + } + + @Test + public void get_invalidPackageOrUid_shouldReturnNull() { + assertThat(mAppIconCacheManager.get(/* packageName= */ null, /* uid= */ -1)).isNull(); + } + + @Test + public void put_invalidPackageOrUid_shouldNotCrash() { + mAppIconCacheManager.put(/* packageName= */ null, /* uid= */ 0, mIcon); + // no crash + } + + @Test + public void put_invalidIcon_shouldNotCacheIcon() { + mAppIconCacheManager.put(APP_PACKAGE_NAME, APP_UID, /* drawable= */ null); + + assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME, APP_UID)).isNull(); + } + + @Test + public void put_invalidIconSize_shouldNotCacheIcon() { + doReturn(-1).when(mIcon).getIntrinsicHeight(); + doReturn(-1).when(mIcon).getIntrinsicWidth(); + + mAppIconCacheManager.put(APP_PACKAGE_NAME, APP_UID, mIcon); + + assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME, APP_UID)).isNull(); + } + + @Test + public void put_shouldCacheIcon() { + mAppIconCacheManager.put(APP_PACKAGE_NAME, APP_UID, mIcon); + + assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME, APP_UID)).isEqualTo(mIcon); + } + + @Test + public void release_noInstance_shouldNotCrash() { + mAppIconCacheManager = null; + + AppIconCacheManager.release(); + // no crash + } + + @Test + public void release_existInstance_shouldClearCache() { + mAppIconCacheManager.put(APP_PACKAGE_NAME, APP_UID, mIcon); + + AppIconCacheManager.release(); + + assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME, APP_UID)).isNull(); + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/AppUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/AppUtilsTest.java new file mode 100644 index 000000000000..8e448aa0eace --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/AppUtilsTest.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2022 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.settingslib.applications; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.fail; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.graphics.drawable.Drawable; + +import com.android.settingslib.Utils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; + +import java.io.File; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +@RunWith(RobolectricTestRunner.class) +public class AppUtilsTest { + + private static final String APP_PACKAGE_NAME = "com.test.app"; + private static final int APP_UID = 9999; + + @Mock + private Drawable mIcon; + + private Context mContext; + private AppIconCacheManager mAppIconCacheManager; + private ApplicationInfo mAppInfo; + private ApplicationsState.AppEntry mAppEntry; + private ArrayList<ApplicationsState.AppEntry> mAppEntries; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mAppIconCacheManager = AppIconCacheManager.getInstance(); + mAppInfo = createApplicationInfo(APP_PACKAGE_NAME, APP_UID); + mAppEntry = createAppEntry(mAppInfo, /* id= */ 1); + mAppEntries = new ArrayList<>(Arrays.asList(mAppEntry)); + doReturn(mIcon).when(mIcon).mutate(); + } + + @After + public void tearDown() { + AppIconCacheManager.release(); + } + + @Test + public void getIcon_nullAppEntry_shouldReturnNull() { + assertThat(AppUtils.getIcon(mContext, /* appEntry= */ null)).isNull(); + } + + @Test + @Config(shadows = ShadowUtils.class) + public void getIcon_noCachedIcon_shouldNotReturnNull() { + assertThat(AppUtils.getIcon(mContext, mAppEntry)).isNotNull(); + } + + @Test + public void getIcon_existCachedIcon_shouldReturnCachedIcon() { + mAppIconCacheManager.put(APP_PACKAGE_NAME, APP_UID, mIcon); + + assertThat(AppUtils.getIcon(mContext, mAppEntry)).isEqualTo(mIcon); + } + + @Test + public void getIconFromCache_nullAppEntry_shouldReturnNull() { + assertThat(AppUtils.getIconFromCache(/* appEntry= */ null)).isNull(); + } + + @Test + public void getIconFromCache_shouldReturnCachedIcon() { + mAppIconCacheManager.put(APP_PACKAGE_NAME, APP_UID, mIcon); + + assertThat(AppUtils.getIconFromCache(mAppEntry)).isEqualTo(mIcon); + } + + @Test + public void preloadTopIcons_nullAppEntries_shouldNotCrash() { + AppUtils.preloadTopIcons(mContext, /* appEntries= */ null, /* number= */ 1); + // no crash + } + + @Test + public void preloadTopIcons_zeroPreloadIcons_shouldNotCacheIcons() { + AppUtils.preloadTopIcons(mContext, mAppEntries, /* number= */ 0); + + assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME, APP_UID)).isNull(); + } + + @Test + @Config(shadows = ShadowUtils.class) + public void preloadTopIcons_shouldCheckIconFromCache() throws InterruptedException { + AppUtils.preloadTopIcons(mContext, mAppEntries, /* number= */ 1); + + TimeUnit.SECONDS.sleep(1); + assertThat(mAppIconCacheManager.get(APP_PACKAGE_NAME, APP_UID)).isNotNull(); + } + + private ApplicationsState.AppEntry createAppEntry(ApplicationInfo appInfo, int id) { + ApplicationsState.AppEntry appEntry = new ApplicationsState.AppEntry(mContext, appInfo, id); + appEntry.label = "label"; + appEntry.mounted = true; + final File apkFile = mock(File.class); + doReturn(true).when(apkFile).exists(); + try { + Field field = ApplicationsState.AppEntry.class.getDeclaredField("apkFile"); + field.setAccessible(true); + field.set(appEntry, apkFile); + } catch (NoSuchFieldException | IllegalAccessException e) { + fail("Not able to mock apkFile: " + e); + } + return appEntry; + } + + private ApplicationInfo createApplicationInfo(String packageName, int uid) { + ApplicationInfo appInfo = new ApplicationInfo(); + appInfo.sourceDir = "appPath"; + appInfo.packageName = packageName; + appInfo.uid = uid; + return appInfo; + } + + @Implements(Utils.class) + private static class ShadowUtils { + @Implementation + public static Drawable getBadgedIcon(Context context, ApplicationInfo appInfo) { + final Drawable icon = mock(Drawable.class); + doReturn(10).when(icon).getIntrinsicHeight(); + doReturn(10).when(icon).getIntrinsicWidth(); + doReturn(icon).when(icon).mutate(); + return icon; + } + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java index 10ccd22eca83..1f2297ba3a0c 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java @@ -33,6 +33,7 @@ import static org.mockito.Mockito.when; import static org.robolectric.shadow.api.Shadow.extract; import android.annotation.UserIdInt; +import android.app.Application; import android.app.ApplicationPackageManager; import android.app.usage.StorageStats; import android.app.usage.StorageStatsManager; @@ -110,6 +111,7 @@ public class ApplicationsStateRoboTest { private ApplicationsState mApplicationsState; private Session mSession; + private Application mApplication; @Mock private Callbacks mCallbacks; @@ -190,6 +192,7 @@ public class ApplicationsStateRoboTest { ShadowContextImpl shadowContext = Shadow.extract( RuntimeEnvironment.application.getBaseContext()); shadowContext.setSystemService(Context.STORAGE_STATS_SERVICE, mStorageStatsManager); + mApplication = spy(RuntimeEnvironment.application); StorageStats storageStats = new StorageStats(); storageStats.codeBytes = 10; storageStats.cacheBytes = 30; @@ -207,8 +210,7 @@ public class ApplicationsStateRoboTest { anyLong() /* flags */, anyInt() /* userId */)).thenReturn(new ParceledListSlice(infos)); ApplicationsState.sInstance = null; - mApplicationsState = - ApplicationsState.getInstance(RuntimeEnvironment.application, mPackageManagerService); + mApplicationsState = ApplicationsState.getInstance(mApplication, mPackageManagerService); mApplicationsState.clearEntries(); mSession = mApplicationsState.newSession(mCallbacks); @@ -703,6 +705,23 @@ public class ApplicationsStateRoboTest { verify(mApplicationsState, never()).clearEntries(); } + @Test + public void testDefaultSession_enabledAppIconCache_shouldSkipPreloadIcon() { + when(mApplication.getPackageName()).thenReturn("com.android.settings"); + mSession.onResume(); + + addApp(HOME_PACKAGE_NAME, 1); + addApp(LAUNCHABLE_PACKAGE_NAME, 2); + mSession.rebuild(ApplicationsState.FILTER_EVERYTHING, ApplicationsState.SIZE_COMPARATOR); + processAllMessages(); + verify(mCallbacks).onRebuildComplete(mAppEntriesCaptor.capture()); + + List<AppEntry> appEntries = mAppEntriesCaptor.getValue(); + for (AppEntry appEntry : appEntries) { + assertThat(appEntry.icon).isNull(); + } + } + private void setupDoResumeIfNeededLocked(ArrayList<ApplicationInfo> ownerApps, ArrayList<ApplicationInfo> profileApps) throws RemoteException { |