/* * Copyright (C) 2016 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.server.om; import static android.app.AppGlobals.getPackageManager; import static android.content.Intent.ACTION_PACKAGE_ADDED; import static android.content.Intent.ACTION_PACKAGE_CHANGED; import static android.content.Intent.ACTION_PACKAGE_REMOVED; import static android.content.Intent.ACTION_USER_ADDED; import static android.content.Intent.ACTION_USER_REMOVED; import static android.content.pm.PackageManager.SIGNATURE_MATCH; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.IActivityManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.om.IOverlayManager; import android.content.om.OverlayInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageInfo; import android.content.pm.PackageManagerInternal; import android.content.pm.UserInfo; import android.net.Uri; import android.os.Binder; import android.os.Environment; import android.os.IBinder; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ShellCallback; import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.AtomicFile; import android.util.Slog; import android.util.SparseArray; import com.android.internal.util.ConcurrentUtils; import com.android.server.FgThread; import com.android.server.IoThread; import com.android.server.LocalServices; import com.android.server.SystemServerInitThreadPool; import com.android.server.SystemService; import com.android.server.pm.Installer; import com.android.server.pm.UserManagerService; import org.xmlpull.v1.XmlPullParserException; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; /** * Service to manage asset overlays. * *

Asset overlays are additional resources that come from apks loaded * alongside the system and app apks. This service, the OverlayManagerService * (OMS), tracks which installed overlays to use and provides methods to change * this. Changes propagate to running applications as part of the Activity * lifecycle. This allows Activities to reread their resources at a well * defined point.

* *

By itself, the OMS will not change what overlays should be active. * Instead, it is only responsible for making sure that overlays *can* be used * from a technical and security point of view and to activate overlays in * response to external requests. The responsibility to toggle overlays on and * off lies within components that implement different use-cases such as themes * or dynamic customization.

* *

The OMS receives input from three sources:

* * * *

The AIDL interface works with String package names, int user IDs, and * {@link OverlayInfo} objects. OverlayInfo instances are used to track a * specific pair of target and overlay packages and include information such as * the current state of the overlay. OverlayInfo objects are immutable.

* *

Internally, OverlayInfo objects are maintained by the * OverlayManagerSettings class. The OMS and its helper classes are notified of * changes to the settings by the OverlayManagerSettings.ChangeListener * callback interface. The file /data/system/overlays.xml is used to persist * the settings.

* *

Creation and deletion of idmap files are handled by the IdmapManager * class.

* *

The following is an overview of OMS and its related classes. Note how box * (2) does the heavy lifting, box (1) interacts with the Android framework, * and box (3) replaces box (1) during unit testing.

* *
 *         Android framework
 *            |         ^
 *      . . . | . . . . | . . . .
 *     .      |         |       .
 *     .    AIDL,   broadcasts  .
 *     .   intents      |       .
 *     .      |         |       . . . . . . . . . . . .
 *     .      v         |       .                     .
 *     .  OverlayManagerService . OverlayManagerTests .
 *     .                  \     .     /               .
 *     . (1)               \    .    /            (3) .
 *      . . . . . . . . . . \ . . . / . . . . . . . . .
 *     .                     \     /              .
 *     . (2)                  \   /               .
 *     .           OverlayManagerServiceImpl      .
 *     .                  |            |          .
 *     .                  |            |          .
 *     . OverlayManagerSettings     IdmapManager  .
 *     .                                          .
 *     . . . .  . . . . . . . . . . . . . . . . . .
 * 
* *

Finally, here is a list of keywords used in the OMS context.

* * */ public final class OverlayManagerService extends SystemService { static final String TAG = "OverlayManager"; static final boolean DEBUG = false; /** * The system property that specifies the default overlays to apply. * This is a semicolon separated list of package names. * * Ex: com.android.vendor.overlay_one;com.android.vendor.overlay_two */ private static final String DEFAULT_OVERLAYS_PROP = "ro.boot.vendor.overlay.theme"; private final Object mLock = new Object(); private final AtomicFile mSettingsFile; private final PackageManagerHelper mPackageManager; private final UserManagerService mUserManager; private final OverlayManagerSettings mSettings; private final OverlayManagerServiceImpl mImpl; private final AtomicBoolean mPersistSettingsScheduled = new AtomicBoolean(false); private Future mInitCompleteSignal; public OverlayManagerService(@NonNull final Context context, @NonNull final Installer installer) { super(context); mSettingsFile = new AtomicFile(new File(Environment.getDataSystemDirectory(), "overlays.xml")); mPackageManager = new PackageManagerHelper(); mUserManager = UserManagerService.getInstance(); IdmapManager im = new IdmapManager(installer); mSettings = new OverlayManagerSettings(); mImpl = new OverlayManagerServiceImpl(mPackageManager, im, mSettings, getDefaultOverlayPackages(), new OverlayChangeListener()); mInitCompleteSignal = SystemServerInitThreadPool.get().submit(() -> { final IntentFilter packageFilter = new IntentFilter(); packageFilter.addAction(ACTION_PACKAGE_ADDED); packageFilter.addAction(ACTION_PACKAGE_CHANGED); packageFilter.addAction(ACTION_PACKAGE_REMOVED); packageFilter.addDataScheme("package"); getContext().registerReceiverAsUser(new PackageReceiver(), UserHandle.ALL, packageFilter, null, null); final IntentFilter userFilter = new IntentFilter(); userFilter.addAction(ACTION_USER_ADDED); userFilter.addAction(ACTION_USER_REMOVED); getContext().registerReceiverAsUser(new UserReceiver(), UserHandle.ALL, userFilter, null, null); restoreSettings(); initIfNeeded(); onSwitchUser(UserHandle.USER_SYSTEM); publishBinderService(Context.OVERLAY_SERVICE, mService); publishLocalService(OverlayManagerService.class, this); }, "Init OverlayManagerService"); } @Override public void onStart() { // Intentionally left empty. } @Override public void onBootPhase(int phase) { if (phase == PHASE_SYSTEM_SERVICES_READY) { ConcurrentUtils.waitForFutureNoInterrupt(mInitCompleteSignal, "Wait for OverlayManagerService init"); mInitCompleteSignal = null; } } private void initIfNeeded() { final UserManager um = getContext().getSystemService(UserManager.class); final List users = um.getUsers(true /*excludeDying*/); synchronized (mLock) { final int userCount = users.size(); for (int i = 0; i < userCount; i++) { final UserInfo userInfo = users.get(i); if (!userInfo.supportsSwitchTo() && userInfo.id != UserHandle.USER_SYSTEM) { // Initialize any users that can't be switched to, as there state would // never be setup in onSwitchUser(). We will switch to the system user right // after this, and its state will be setup there. final List targets = mImpl.updateOverlaysForUser(users.get(i).id); updateOverlayPaths(users.get(i).id, targets); } } } } @Override public void onSwitchUser(final int newUserId) { // ensure overlays in the settings are up-to-date, and propagate // any asset changes to the rest of the system synchronized (mLock) { final List targets = mImpl.updateOverlaysForUser(newUserId); updateAssets(newUserId, targets); } schedulePersistSettings(); } private static Set getDefaultOverlayPackages() { final String str = SystemProperties.get(DEFAULT_OVERLAYS_PROP); if (TextUtils.isEmpty(str)) { return Collections.emptySet(); } final ArraySet defaultPackages = new ArraySet<>(); for (String packageName : str.split(";")) { if (!TextUtils.isEmpty(packageName)) { defaultPackages.add(packageName); } } return defaultPackages; } private final class PackageReceiver extends BroadcastReceiver { @Override public void onReceive(@NonNull final Context context, @NonNull final Intent intent) { final Uri data = intent.getData(); if (data == null) { Slog.e(TAG, "Cannot handle package broadcast with null data"); return; } final String packageName = data.getSchemeSpecificPart(); final boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false); final int[] userIds; final int extraUid = intent.getIntExtra(Intent.EXTRA_UID, UserHandle.USER_NULL); if (extraUid == UserHandle.USER_NULL) { userIds = mUserManager.getUserIds(); } else { userIds = new int[] { UserHandle.getUserId(extraUid) }; } switch (intent.getAction()) { case ACTION_PACKAGE_ADDED: if (replacing) { onPackageUpgraded(packageName, userIds); } else { onPackageAdded(packageName, userIds); } break; case ACTION_PACKAGE_CHANGED: onPackageChanged(packageName, userIds); break; case ACTION_PACKAGE_REMOVED: if (replacing) { onPackageUpgrading(packageName, userIds); } else { onPackageRemoved(packageName, userIds); } break; default: // do nothing break; } } private void onPackageAdded(@NonNull final String packageName, @NonNull final int[] userIds) { for (final int userId : userIds) { synchronized (mLock) { final PackageInfo pi = mPackageManager.getPackageInfo(packageName, userId, false); if (pi != null) { mPackageManager.cachePackageInfo(packageName, userId, pi); if (!isOverlayPackage(pi)) { mImpl.onTargetPackageAdded(packageName, userId); } else { mImpl.onOverlayPackageAdded(packageName, userId); } } } } } private void onPackageChanged(@NonNull final String packageName, @NonNull final int[] userIds) { for (int userId : userIds) { synchronized (mLock) { final PackageInfo pi = mPackageManager.getPackageInfo(packageName, userId, false); if (pi != null) { mPackageManager.cachePackageInfo(packageName, userId, pi); if (!isOverlayPackage(pi)) { mImpl.onTargetPackageChanged(packageName, userId); } else { mImpl.onOverlayPackageChanged(packageName, userId); } } } } } private void onPackageUpgrading(@NonNull final String packageName, @NonNull final int[] userIds) { for (int userId : userIds) { synchronized (mLock) { mPackageManager.forgetPackageInfo(packageName, userId); final OverlayInfo oi = mImpl.getOverlayInfo(packageName, userId); if (oi == null) { mImpl.onTargetPackageUpgrading(packageName, userId); } else { mImpl.onOverlayPackageUpgrading(packageName, userId); } } } } private void onPackageUpgraded(@NonNull final String packageName, @NonNull final int[] userIds) { for (int userId : userIds) { synchronized (mLock) { final PackageInfo pi = mPackageManager.getPackageInfo(packageName, userId, false); if (pi != null) { mPackageManager.cachePackageInfo(packageName, userId, pi); if (!isOverlayPackage(pi)) { mImpl.onTargetPackageUpgraded(packageName, userId); } else { mImpl.onOverlayPackageUpgraded(packageName, userId); } } } } } private void onPackageRemoved(@NonNull final String packageName, @NonNull final int[] userIds) { for (int userId : userIds) { synchronized (mLock) { mPackageManager.forgetPackageInfo(packageName, userId); final OverlayInfo oi = mImpl.getOverlayInfo(packageName, userId); if (oi == null) { mImpl.onTargetPackageRemoved(packageName, userId); } else { mImpl.onOverlayPackageRemoved(packageName, userId); } } } } } private final class UserReceiver extends BroadcastReceiver { @Override public void onReceive(@NonNull final Context context, @NonNull final Intent intent) { final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL); switch (intent.getAction()) { case ACTION_USER_ADDED: if (userId != UserHandle.USER_NULL) { final ArrayList targets; synchronized (mLock) { targets = mImpl.updateOverlaysForUser(userId); } updateOverlayPaths(userId, targets); } break; case ACTION_USER_REMOVED: if (userId != UserHandle.USER_NULL) { synchronized (mLock) { mImpl.onUserRemoved(userId); mPackageManager.forgetAllPackageInfos(userId); } } break; default: // do nothing break; } } } private final IBinder mService = new IOverlayManager.Stub() { @Override public Map> getAllOverlays(int userId) throws RemoteException { userId = handleIncomingUser(userId, "getAllOverlays"); synchronized (mLock) { return mImpl.getOverlaysForUser(userId); } } @Override public List getOverlayInfosForTarget(@Nullable final String targetPackageName, int userId) throws RemoteException { userId = handleIncomingUser(userId, "getOverlayInfosForTarget"); if (targetPackageName == null) { return Collections.emptyList(); } synchronized (mLock) { return mImpl.getOverlayInfosForTarget(targetPackageName, userId); } } @Override public OverlayInfo getOverlayInfo(@Nullable final String packageName, int userId) throws RemoteException { userId = handleIncomingUser(userId, "getOverlayInfo"); if (packageName == null) { return null; } synchronized (mLock) { return mImpl.getOverlayInfo(packageName, userId); } } @Override public boolean setEnabled(@Nullable final String packageName, final boolean enable, int userId) throws RemoteException { enforceChangeOverlayPackagesPermission("setEnabled"); userId = handleIncomingUser(userId, "setEnabled"); if (packageName == null) { return false; } final long ident = Binder.clearCallingIdentity(); try { synchronized (mLock) { return mImpl.setEnabled(packageName, enable, userId); } } finally { Binder.restoreCallingIdentity(ident); } } @Override public boolean setEnabledExclusive(@Nullable final String packageName, final boolean enable, int userId) throws RemoteException { enforceChangeOverlayPackagesPermission("setEnabled"); userId = handleIncomingUser(userId, "setEnabled"); if (packageName == null || !enable) { return false; } final long ident = Binder.clearCallingIdentity(); try { synchronized (mLock) { return mImpl.setEnabledExclusive(packageName, userId); } } finally { Binder.restoreCallingIdentity(ident); } } @Override public boolean setPriority(@Nullable final String packageName, @Nullable final String parentPackageName, int userId) throws RemoteException { enforceChangeOverlayPackagesPermission("setPriority"); userId = handleIncomingUser(userId, "setPriority"); if (packageName == null || parentPackageName == null) { return false; } final long ident = Binder.clearCallingIdentity(); try { synchronized (mLock) { return mImpl.setPriority(packageName, parentPackageName, userId); } } finally { Binder.restoreCallingIdentity(ident); } } @Override public boolean setHighestPriority(@Nullable final String packageName, int userId) throws RemoteException { enforceChangeOverlayPackagesPermission("setHighestPriority"); userId = handleIncomingUser(userId, "setHighestPriority"); if (packageName == null) { return false; } final long ident = Binder.clearCallingIdentity(); try { synchronized (mLock) { return mImpl.setHighestPriority(packageName, userId); } } finally { Binder.restoreCallingIdentity(ident); } } @Override public boolean setLowestPriority(@Nullable final String packageName, int userId) throws RemoteException { enforceChangeOverlayPackagesPermission("setLowestPriority"); userId = handleIncomingUser(userId, "setLowestPriority"); if (packageName == null) { return false; } final long ident = Binder.clearCallingIdentity(); try { synchronized (mLock) { return mImpl.setLowestPriority(packageName, userId); } } finally { Binder.restoreCallingIdentity(ident); } } @Override public void onShellCommand(@NonNull final FileDescriptor in, @NonNull final FileDescriptor out, @NonNull final FileDescriptor err, @NonNull final String[] args, @NonNull final ShellCallback callback, @NonNull final ResultReceiver resultReceiver) { (new OverlayManagerShellCommand(this)).exec( this, in, out, err, args, callback, resultReceiver); } @Override protected void dump(@NonNull final FileDescriptor fd, @NonNull final PrintWriter pw, @NonNull final String[] argv) { enforceDumpPermission("dump"); final boolean verbose = argv.length > 0 && "--verbose".equals(argv[0]); synchronized (mLock) { mImpl.onDump(pw); mPackageManager.dump(pw, verbose); } } /** * Ensure that the caller has permission to interact with the given userId. * If the calling user is not the same as the provided user, the caller needs * to hold the INTERACT_ACROSS_USERS_FULL permission (or be system uid or * root). * * @param userId the user to interact with * @param message message for any SecurityException */ private int handleIncomingUser(final int userId, @NonNull final String message) { return ActivityManager.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(), userId, false, true, message, null); } /** * Enforce that the caller holds the CHANGE_OVERLAY_PACKAGES permission (or is * system or root). * * @param message used as message if SecurityException is thrown * @throws SecurityException if the permission check fails */ private void enforceChangeOverlayPackagesPermission(@NonNull final String message) { getContext().enforceCallingOrSelfPermission( android.Manifest.permission.CHANGE_OVERLAY_PACKAGES, message); } /** * Enforce that the caller holds the DUMP permission (or is system or root). * * @param message used as message if SecurityException is thrown * @throws SecurityException if the permission check fails */ private void enforceDumpPermission(@NonNull final String message) { getContext().enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, message); } }; private boolean isOverlayPackage(@NonNull final PackageInfo pi) { return pi != null && pi.overlayTarget != null; } private final class OverlayChangeListener implements OverlayManagerServiceImpl.OverlayChangeListener { @Override public void onOverlaysChanged(@NonNull final String targetPackageName, final int userId) { schedulePersistSettings(); FgThread.getHandler().post(() -> { updateAssets(userId, targetPackageName); final Intent intent = new Intent(Intent.ACTION_OVERLAY_CHANGED, Uri.fromParts("package", targetPackageName, null)); intent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); if (DEBUG) { Slog.d(TAG, "send broadcast " + intent); } try { ActivityManager.getService().broadcastIntent(null, intent, null, null, 0, null, null, null, android.app.AppOpsManager.OP_NONE, null, false, false, userId); } catch (RemoteException e) { // Intentionally left empty. } }); } } /** * Updates the target packages' set of enabled overlays in PackageManager. */ private void updateOverlayPaths(int userId, List targetPackageNames) { if (DEBUG) { Slog.d(TAG, "Updating overlay assets"); } final PackageManagerInternal pm = LocalServices.getService(PackageManagerInternal.class); final boolean updateFrameworkRes = targetPackageNames.contains("android"); if (updateFrameworkRes) { targetPackageNames = pm.getTargetPackageNames(userId); } final Map> pendingChanges = new ArrayMap<>(targetPackageNames.size()); synchronized (mLock) { final List frameworkOverlays = mImpl.getEnabledOverlayPackageNames("android", userId); final int N = targetPackageNames.size(); for (int i = 0; i < N; i++) { final String targetPackageName = targetPackageNames.get(i); List list = new ArrayList<>(); if (!"android".equals(targetPackageName)) { list.addAll(frameworkOverlays); } list.addAll(mImpl.getEnabledOverlayPackageNames(targetPackageName, userId)); pendingChanges.put(targetPackageName, list); } } final int N = targetPackageNames.size(); for (int i = 0; i < N; i++) { final String targetPackageName = targetPackageNames.get(i); if (DEBUG) { Slog.d(TAG, "-> Updating overlay: target=" + targetPackageName + " overlays=[" + TextUtils.join(",", pendingChanges.get(targetPackageName)) + "] userId=" + userId); } if (!pm.setEnabledOverlayPackages( userId, targetPackageName, pendingChanges.get(targetPackageName))) { Slog.e(TAG, String.format("Failed to change enabled overlays for %s user %d", targetPackageName, userId)); } } } private void updateAssets(final int userId, final String targetPackageName) { updateAssets(userId, Collections.singletonList(targetPackageName)); } private void updateAssets(final int userId, List targetPackageNames) { updateOverlayPaths(userId, targetPackageNames); final IActivityManager am = ActivityManager.getService(); try { am.scheduleApplicationInfoChanged(targetPackageNames, userId); } catch (RemoteException e) { // Intentionally left empty. } } private void schedulePersistSettings() { if (mPersistSettingsScheduled.getAndSet(true)) { return; } IoThread.getHandler().post(() -> { mPersistSettingsScheduled.set(false); if (DEBUG) { Slog.d(TAG, "Writing overlay settings"); } synchronized (mLock) { FileOutputStream stream = null; try { stream = mSettingsFile.startWrite(); mSettings.persist(stream); mSettingsFile.finishWrite(stream); } catch (IOException | XmlPullParserException e) { mSettingsFile.failWrite(stream); Slog.e(TAG, "failed to persist overlay state", e); } } }); } private void restoreSettings() { synchronized (mLock) { if (!mSettingsFile.getBaseFile().exists()) { return; } try (final FileInputStream stream = mSettingsFile.openRead()) { mSettings.restore(stream); // We might have data for dying users if the device was // restarted before we received USER_REMOVED. Remove data for // users that will not exist after the system is ready. final List liveUsers = mUserManager.getUsers(true /*excludeDying*/); final int[] liveUserIds = new int[liveUsers.size()]; for (int i = 0; i < liveUsers.size(); i++) { liveUserIds[i] = liveUsers.get(i).getUserHandle().getIdentifier(); } Arrays.sort(liveUserIds); for (int userId : mSettings.getUsers()) { if (Arrays.binarySearch(liveUserIds, userId) < 0) { mSettings.removeUser(userId); } } } catch (IOException | XmlPullParserException e) { Slog.e(TAG, "failed to restore overlay state", e); } } } private static final class PackageManagerHelper implements OverlayManagerServiceImpl.PackageManagerHelper { private final IPackageManager mPackageManager; private final PackageManagerInternal mPackageManagerInternal; // Use a cache for performance and for consistency within OMS: because // additional PACKAGE_* intents may be delivered while we process an // intent, querying the PackageManagerService for the actual current // state may lead to contradictions within OMS. Better then to lag // behind until all pending intents have been processed. private final SparseArray> mCache = new SparseArray<>(); PackageManagerHelper() { mPackageManager = getPackageManager(); mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); } public PackageInfo getPackageInfo(@NonNull final String packageName, final int userId, final boolean useCache) { if (useCache) { final PackageInfo cachedPi = getCachedPackageInfo(packageName, userId); if (cachedPi != null) { return cachedPi; } } try { final PackageInfo pi = mPackageManager.getPackageInfo(packageName, 0, userId); if (useCache && pi != null) { cachePackageInfo(packageName, userId, pi); } return pi; } catch (RemoteException e) { // Intentionally left empty. } return null; } @Override public PackageInfo getPackageInfo(@NonNull final String packageName, final int userId) { return getPackageInfo(packageName, userId, true); } @Override public boolean signaturesMatching(@NonNull final String packageName1, @NonNull final String packageName2, final int userId) { // The package manager does not support different versions of packages // to be installed for different users: ignore userId for now. try { return mPackageManager.checkSignatures( packageName1, packageName2) == SIGNATURE_MATCH; } catch (RemoteException e) { // Intentionally left blank } return false; } @Override public List getOverlayPackages(final int userId) { return mPackageManagerInternal.getOverlayPackages(userId); } public PackageInfo getCachedPackageInfo(@NonNull final String packageName, final int userId) { final HashMap map = mCache.get(userId); return map == null ? null : map.get(packageName); } public void cachePackageInfo(@NonNull final String packageName, final int userId, @NonNull final PackageInfo pi) { HashMap map = mCache.get(userId); if (map == null) { map = new HashMap<>(); mCache.put(userId, map); } map.put(packageName, pi); } public void forgetPackageInfo(@NonNull final String packageName, final int userId) { final HashMap map = mCache.get(userId); if (map == null) { return; } map.remove(packageName); if (map.isEmpty()) { mCache.delete(userId); } } public void forgetAllPackageInfos(final int userId) { mCache.delete(userId); } private static final String TAB1 = " "; private static final String TAB2 = TAB1 + TAB1; public void dump(@NonNull final PrintWriter pw, final boolean verbose) { pw.println("PackageInfo cache"); if (!verbose) { int count = 0; final int N = mCache.size(); for (int i = 0; i < N; i++) { final int userId = mCache.keyAt(i); count += mCache.get(userId).size(); } pw.println(TAB1 + count + " package(s)"); return; } if (mCache.size() == 0) { pw.println(TAB1 + ""); return; } final int N = mCache.size(); for (int i = 0; i < N; i++) { final int userId = mCache.keyAt(i); pw.println(TAB1 + "User " + userId); final HashMap map = mCache.get(userId); for (Map.Entry entry : map.entrySet()) { pw.println(TAB2 + entry.getKey() + ": " + entry.getValue()); } } } } }