blob: cc709ce6f176d15372d996dec6a5d39368fa9dfa [file] [log] [blame]
/*
* 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_REMOVED;
import static android.content.pm.PackageManager.SIGNATURE_MATCH;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityManagerNative;
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.Process;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.AtomicFile;
import android.util.Slog;
import android.util.SparseArray;
import com.android.server.FgThread;
import com.android.server.IoThread;
import com.android.server.LocalServices;
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.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Service to manage asset overlays.
*
* <p>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.</p>
*
* <p>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.</p>
*
* <p>The OMS receives input from three sources:</p>
*
* <ul>
* <li>Callbacks from the SystemService class, specifically when the
* Android framework is booting and when the end user switches Android
* users.</li>
*
* <li>Intents from the PackageManagerService (PMS). Overlays are regular
* apks, and whenever a package is installed (or removed, or has a
* component enabled or disabled), the PMS broadcasts this as an intent.
* When the OMS receives one of these intents, it updates its internal
* representation of the available overlays and, if there was a visible
* change, triggers an asset refresh in the affected apps.</li>
*
* <li>External requests via the {@link IOverlayManager AIDL interface}.
* The interface allows clients to read information about the currently
* available overlays, change whether an overlay should be used or not, and
* change the relative order in which overlay packages are loaded.
* Read-access is granted if the request targets the same Android user as
* the caller runs as, or if the caller holds the
* INTERACT_ACROSS_USERS_FULL permission. Write-access is granted if the
* caller is granted read-access and additionaly holds the
* CHANGE_OVERLAY_PACKAGES permission.</li>
* </ul>
*
* <p>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.</p>
*
* <p>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.</p>
*
* <p>Creation and deletion of idmap files are handled by the IdmapManager
* class.</p>
*
* <p>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.</p>
*
* <pre>
* Android framework
* | ^
* . . . | . . . . | . . . .
* . | | .
* . AIDL, broadcasts .
* . intents | .
* . | | . . . . . . . . . . . .
* . v | . .
* . OverlayManagerService . OverlayManagerTests .
* . \ . / .
* . (1) \ . / (3) .
* . . . . . . . . . . \ . . . / . . . . . . . . .
* . \ / .
* . (2) \ / .
* . OverlayManagerServiceImpl .
* . | | .
* . | | .
* . OverlayManagerSettings IdmapManager .
* . .
* . . . . . . . . . . . . . . . . . . . . . .
* </pre>
*
* <p>Finally, here is a list of keywords used in the OMS context.</p>
*
* <ul>
* <li><b>target [package]</b> -- A regular apk that may have its resource
* pool extended by zero or more overlay packages.</li>
*
* <li><b>overlay [package]</b> -- An apk that provides additional
* resources to another apk.</li>
*
* <li><b>OMS</b> -- The OverlayManagerService, i.e. this class.</li>
*
* <li><b>approved</b> -- An overlay is approved if the OMS has verified
* that it can be used technically speaking (its target package is
* installed, at least one resource name in both packages match, the
* idmap was created, etc) and that it is secure to do so. External
* clients can not change this state.</li>
*
* <li><b>not approved</b> -- The opposite of approved.</li>
*
* <li><b>enabled</b> -- An overlay currently in active use and thus part
* of resource lookups. This requires the overlay to be approved. Only
* external clients can change this state.</li>
*
* <li><b>disabled</b> -- The opposite of enabled.</li>
*
* <li><b>idmap</b> -- A mapping of resource IDs between target and overlay
* used during resource lookup. Also the name of the binary that creates
* the mapping.</li>
* </ul>
*/
public final class OverlayManagerService extends SystemService {
static final String TAG = "OverlayManager";
static final boolean DEBUG = false;
static final String PERMISSION_DENIED = "Operation not permitted for user shell";
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);
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);
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_REMOVED);
getContext().registerReceiverAsUser(new UserReceiver(), UserHandle.ALL,
userFilter, null, null);
restoreSettings();
onSwitchUser(UserHandle.USER_SYSTEM);
schedulePersistSettings();
mSettings.addChangeListener(new OverlayChangeListener());
publishBinderService(Context.OVERLAY_SERVICE, mService);
publishLocalService(OverlayManagerService.class, this);
}
@Override
public void onStart() {
// Intentionally left empty.
}
@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
final List<String> targets;
synchronized (mLock) {
targets = mImpl.onSwitchUser(newUserId);
}
updateAssets(newUserId, targets);
}
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) {
switch (intent.getAction()) {
case ACTION_USER_REMOVED:
final int userId =
intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
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<String, List<OverlayInfo>> getAllOverlays(int userId)
throws RemoteException {
userId = handleIncomingUser(userId, "getAllOverlays");
synchronized (mLock) {
return mImpl.getOverlaysForUser(userId);
}
}
@Override
public List<OverlayInfo> 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 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 OverlayManagerSettings.ChangeListener {
@Override
public void onSettingsChanged() {
schedulePersistSettings();
}
@Override
public void onOverlayAdded(@NonNull final OverlayInfo oi) {
scheduleBroadcast(Intent.ACTION_OVERLAY_ADDED, oi, oi.isEnabled());
}
@Override
public void onOverlayRemoved(@NonNull final OverlayInfo oi) {
scheduleBroadcast(Intent.ACTION_OVERLAY_REMOVED, oi, oi.isEnabled());
}
@Override
public void onOverlayChanged(@NonNull final OverlayInfo oi,
@NonNull final OverlayInfo oldOi) {
scheduleBroadcast(Intent.ACTION_OVERLAY_CHANGED, oi, oi.isEnabled() != oldOi.isEnabled());
}
@Override
public void onOverlayPriorityChanged(@NonNull final OverlayInfo oi) {
scheduleBroadcast(Intent.ACTION_OVERLAY_PRIORITY_CHANGED, oi, oi.isEnabled());
}
private void scheduleBroadcast(@NonNull final String action, @NonNull final OverlayInfo oi,
final boolean doUpdate) {
FgThread.getHandler().post(new BroadcastRunnable(action, oi, doUpdate));
}
private final class BroadcastRunnable implements Runnable {
private final String mAction;
private final OverlayInfo mOverlayInfo;
private final boolean mDoUpdate;
BroadcastRunnable(@NonNull final String action, @NonNull final OverlayInfo oi,
final boolean doUpdate) {
mAction = action;
mOverlayInfo = oi;
mDoUpdate = doUpdate;
}
@Override
public void run() {
if (mDoUpdate) {
updateAssets(mOverlayInfo.userId, mOverlayInfo.targetPackageName);
}
sendBroadcast(mAction, mOverlayInfo.targetPackageName, mOverlayInfo.packageName,
mOverlayInfo.userId);
}
private void sendBroadcast(@NonNull final String action,
@NonNull final String targetPackageName, @NonNull final String packageName,
final int userId) {
final Intent intent = new Intent(action, Uri.fromParts("package",
String.format("%s/%s", targetPackageName, packageName), null));
intent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
if (DEBUG) {
Slog.d(TAG, String.format("send broadcast %s", intent));
}
try {
ActivityManagerNative.getDefault().broadcastIntent(null, intent, null, null, 0,
null, null, null, android.app.AppOpsManager.OP_NONE, null, false, false,
userId);
} catch (RemoteException e) {
// Intentionally left empty.
}
}
}
}
private void updateAssets(final int userId, final String targetPackageName) {
final List<String> list = new ArrayList<>();
list.add(targetPackageName);
updateAssets(userId, list);
}
private void updateAssets(final int userId, List<String> targetPackageNames) {
// TODO: implement when we integrate OMS properly
}
private void schedulePersistSettings() {
if (mPersistSettingsScheduled.getAndSet(true)) {
return;
}
IoThread.getHandler().post(new Runnable() {
@Override
public void run() {
mPersistSettingsScheduled.set(false);
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<UserInfo> deadUsers = getDeadUsers();
final int N = deadUsers.size();
for (int i = 0; i < N; i++) {
final UserInfo deadUser = deadUsers.get(i);
final int userId = deadUser.getUserHandle().getIdentifier();
mSettings.removeUser(userId);
}
} catch (IOException | XmlPullParserException e) {
Slog.e(TAG, "failed to restore overlay state", e);
}
}
}
private List<UserInfo> getDeadUsers() {
final List<UserInfo> users = mUserManager.getUsers(false);
final List<UserInfo> onlyLiveUsers = mUserManager.getUsers(true);
users.removeAll(onlyLiveUsers);
return users;
}
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<HashMap<String, PackageInfo>> 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<PackageInfo> getOverlayPackages(final int userId) {
return mPackageManagerInternal.getOverlayPackages(userId);
}
public PackageInfo getCachedPackageInfo(@NonNull final String packageName,
final int userId) {
final HashMap<String, PackageInfo> 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<String, PackageInfo> 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<String, PackageInfo> 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 + "<empty>");
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<String, PackageInfo> map = mCache.get(userId);
for (Map.Entry<String, PackageInfo> entry : map.entrySet()) {
pw.println(TAB2 + entry.getKey() + ": " + entry.getValue());
}
}
}
}
}