You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1163 lines
48 KiB
1163 lines
48 KiB
/*
|
|
* 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.server.art;
|
|
|
|
import android.annotation.NonNull;
|
|
import android.annotation.Nullable;
|
|
import android.annotation.SystemApi;
|
|
import android.content.BroadcastReceiver;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.IntentFilter;
|
|
import android.os.Binder;
|
|
import android.os.Build;
|
|
import android.os.Environment;
|
|
import android.os.Process;
|
|
import android.os.RemoteException;
|
|
import android.os.ServiceSpecificException;
|
|
import android.os.UserHandle;
|
|
import android.util.Log;
|
|
|
|
import androidx.annotation.RequiresApi;
|
|
|
|
import com.android.internal.annotations.GuardedBy;
|
|
import com.android.internal.annotations.Immutable;
|
|
import com.android.internal.annotations.VisibleForTesting;
|
|
import com.android.server.LocalManagerRegistry;
|
|
import com.android.server.art.model.DetailedDexInfo;
|
|
import com.android.server.art.model.DexContainerFileUseInfo;
|
|
import com.android.server.art.proto.DexUseProto;
|
|
import com.android.server.art.proto.Int32Value;
|
|
import com.android.server.art.proto.PackageDexUseProto;
|
|
import com.android.server.art.proto.PrimaryDexUseProto;
|
|
import com.android.server.art.proto.PrimaryDexUseRecordProto;
|
|
import com.android.server.art.proto.SecondaryDexUseProto;
|
|
import com.android.server.art.proto.SecondaryDexUseRecordProto;
|
|
import com.android.server.pm.PackageManagerLocal;
|
|
import com.android.server.pm.pkg.AndroidPackage;
|
|
import com.android.server.pm.pkg.AndroidPackageSplit;
|
|
import com.android.server.pm.pkg.PackageState;
|
|
|
|
import com.google.auto.value.AutoValue;
|
|
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.OutputStream;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.nio.file.Paths;
|
|
import java.nio.file.StandardCopyOption;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.Comparator;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
import java.util.Optional;
|
|
import java.util.Set;
|
|
import java.util.UUID;
|
|
import java.util.concurrent.Executors;
|
|
import java.util.concurrent.ScheduledExecutorService;
|
|
import java.util.function.Function;
|
|
import java.util.stream.Collectors;
|
|
|
|
/**
|
|
* A singleton class that maintains the information about dex uses. This class is thread-safe.
|
|
*
|
|
* This class collects data sent directly by apps, and hence the data should be trusted as little as
|
|
* possible.
|
|
*
|
|
* To avoid overwriting data, {@link #load()} must be called exactly once, during initialization.
|
|
*
|
|
* @hide
|
|
*/
|
|
@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
|
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
public class DexUseManagerLocal {
|
|
private static final String TAG = ArtManagerLocal.TAG;
|
|
private static final String FILENAME = "/data/system/package-dex-usage.pb";
|
|
|
|
/**
|
|
* The minimum interval between disk writes.
|
|
*
|
|
* In practice, the interval will be much longer because we use a debouncer to postpone the disk
|
|
* write to the end of a series of changes. Note that in theory we could postpone the disk write
|
|
* indefinitely, and therefore we could lose data if the device isn't shut down in the normal
|
|
* way, but that's fine because the data isn't crucial and is recoverable.
|
|
*
|
|
* @hide
|
|
*/
|
|
@VisibleForTesting public static final long INTERVAL_MS = 15_000;
|
|
|
|
private static final Object sLock = new Object();
|
|
@GuardedBy("sLock") @Nullable private static DexUseManagerLocal sInstance = null;
|
|
|
|
@NonNull private final Injector mInjector;
|
|
@NonNull private final Debouncer mDebouncer;
|
|
|
|
private final Object mLock = new Object();
|
|
@GuardedBy("mLock") @NonNull private DexUse mDexUse; // Initialized by `load`.
|
|
@GuardedBy("mLock") private int mRevision = 0;
|
|
@GuardedBy("mLock") private int mLastCommittedRevision = 0;
|
|
@GuardedBy("mLock")
|
|
@NonNull
|
|
private SecondaryDexLocationManager mSecondaryDexLocationManager =
|
|
new SecondaryDexLocationManager();
|
|
|
|
/**
|
|
* Creates the singleton instance.
|
|
*
|
|
* Only {@code SystemServer} should create the instance and register it in {@link
|
|
* LocalManagerRegistry}. Other API users should obtain the instance from {@link
|
|
* LocalManagerRegistry}.
|
|
*
|
|
* In practice, it must be created and registered in {@link LocalManagerRegistry} before {@code
|
|
* PackageManagerService} starts because {@code PackageManagerService} needs it as soon as it
|
|
* starts. It's safe to create an instance early because it doesn't depend on anything else.
|
|
*
|
|
* @param context the system server context
|
|
* @throws IllegalStateException if the instance is already created
|
|
* @throws NullPointerException if required dependencies are missing
|
|
*/
|
|
@NonNull
|
|
public static DexUseManagerLocal createInstance(@NonNull Context context) {
|
|
synchronized (sLock) {
|
|
if (sInstance != null) {
|
|
throw new IllegalStateException("DexUseManagerLocal is already created");
|
|
}
|
|
sInstance = new DexUseManagerLocal(context);
|
|
return sInstance;
|
|
}
|
|
}
|
|
|
|
private DexUseManagerLocal(@NonNull Context context) {
|
|
this(new Injector(context));
|
|
}
|
|
|
|
/** @hide */
|
|
@VisibleForTesting
|
|
public DexUseManagerLocal(@NonNull Injector injector) {
|
|
mInjector = injector;
|
|
mDebouncer = new Debouncer(INTERVAL_MS, mInjector::createScheduledExecutor);
|
|
load();
|
|
}
|
|
|
|
/** Notifies dex use manager that {@link Context#registerReceiver} is ready for use. */
|
|
public void systemReady() {
|
|
// Save the data when the device is being shut down. The receiver is blocking, with a
|
|
// 10s timeout.
|
|
mInjector.getContext().registerReceiver(new BroadcastReceiver() {
|
|
@Override
|
|
public void onReceive(Context context, Intent intent) {
|
|
context.unregisterReceiver(this);
|
|
save();
|
|
}
|
|
}, new IntentFilter(Intent.ACTION_SHUTDOWN));
|
|
}
|
|
|
|
/**
|
|
* Returns the information about the use of all secondary dex files owned by the given package,
|
|
* or an empty list if the package does not own any secondary dex file or it does not exist.
|
|
*/
|
|
@NonNull
|
|
public List<DexContainerFileUseInfo> getSecondaryDexContainerFileUseInfo(
|
|
@NonNull String packageName) {
|
|
return getSecondaryDexInfo(packageName)
|
|
.stream()
|
|
.map(info
|
|
-> DexContainerFileUseInfo.create(info.dexPath(), info.userHandle(),
|
|
info.loaders()
|
|
.stream()
|
|
.map(loader -> loader.loadingPackageName())
|
|
.collect(Collectors.toSet())))
|
|
.collect(Collectors.toList());
|
|
}
|
|
|
|
/**
|
|
* Returns all entities that load the given primary dex file owned by the given package.
|
|
*
|
|
* @hide
|
|
*/
|
|
@NonNull
|
|
public Set<DexLoader> getPrimaryDexLoaders(
|
|
@NonNull String packageName, @NonNull String dexPath) {
|
|
synchronized (mLock) {
|
|
PackageDexUse packageDexUse =
|
|
mDexUse.mPackageDexUseByOwningPackageName.get(packageName);
|
|
if (packageDexUse == null) {
|
|
return Set.of();
|
|
}
|
|
PrimaryDexUse primaryDexUse = packageDexUse.mPrimaryDexUseByDexFile.get(dexPath);
|
|
if (primaryDexUse == null) {
|
|
return Set.of();
|
|
}
|
|
return Set.copyOf(primaryDexUse.mRecordByLoader.keySet());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns whether a primary dex file owned by the given package is used by other apps.
|
|
*
|
|
* @hide
|
|
*/
|
|
public boolean isPrimaryDexUsedByOtherApps(
|
|
@NonNull String packageName, @NonNull String dexPath) {
|
|
return isUsedByOtherApps(getPrimaryDexLoaders(packageName, dexPath), packageName);
|
|
}
|
|
|
|
/**
|
|
* Returns the basic information about all secondary dex files owned by the given package. This
|
|
* method doesn't take dex file visibility into account, so it can only be used for debugging
|
|
* purpose, such as dumpsys.
|
|
*
|
|
* @see #getFilteredDetailedSecondaryDexInfo(String)
|
|
* @hide
|
|
*/
|
|
public @NonNull List<? extends SecondaryDexInfo> getSecondaryDexInfo(
|
|
@NonNull String packageName) {
|
|
return getSecondaryDexInfoImpl(packageName, false /* checkDexFile */);
|
|
}
|
|
|
|
/**
|
|
* Same as above, but requires disk IO, and returns the detailed information, including dex file
|
|
* visibility, filtered by dex file existence and visibility.
|
|
*
|
|
* @hide
|
|
*/
|
|
public @NonNull List<DetailedSecondaryDexInfo> getFilteredDetailedSecondaryDexInfo(
|
|
@NonNull String packageName) {
|
|
return getSecondaryDexInfoImpl(packageName, true /* checkDexFile */);
|
|
}
|
|
|
|
/**
|
|
* Returns the last time the package was used, or 0 if the package has never been used.
|
|
*
|
|
* @hide
|
|
*/
|
|
public long getPackageLastUsedAtMs(@NonNull String packageName) {
|
|
synchronized (mLock) {
|
|
PackageDexUse packageDexUse =
|
|
mDexUse.mPackageDexUseByOwningPackageName.get(packageName);
|
|
if (packageDexUse == null) {
|
|
return 0;
|
|
}
|
|
long primaryLastUsedAtMs =
|
|
packageDexUse.mPrimaryDexUseByDexFile.values()
|
|
.stream()
|
|
.flatMap(primaryDexUse
|
|
-> primaryDexUse.mRecordByLoader.values().stream())
|
|
.map(record -> record.mLastUsedAtMs)
|
|
.max(Long::compare)
|
|
.orElse(0l);
|
|
long secondaryLastUsedAtMs =
|
|
packageDexUse.mSecondaryDexUseByDexFile.values()
|
|
.stream()
|
|
.flatMap(secondaryDexUse
|
|
-> secondaryDexUse.mRecordByLoader.values().stream())
|
|
.map(record -> record.mLastUsedAtMs)
|
|
.max(Long::compare)
|
|
.orElse(0l);
|
|
return Math.max(primaryLastUsedAtMs, secondaryLastUsedAtMs);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param checkDexFile if true, check the existence and visibility of the dex files, and filter
|
|
* the results accordingly. Note that the value of the {@link
|
|
* DetailedSecondaryDexInfo#isDexFilePublic()} field is undefined if this argument is
|
|
* false.
|
|
*/
|
|
private @NonNull List<DetailedSecondaryDexInfo> getSecondaryDexInfoImpl(
|
|
@NonNull String packageName, boolean checkDexFile) {
|
|
synchronized (mLock) {
|
|
PackageDexUse packageDexUse =
|
|
mDexUse.mPackageDexUseByOwningPackageName.get(packageName);
|
|
if (packageDexUse == null) {
|
|
return List.of();
|
|
}
|
|
var results = new ArrayList<DetailedSecondaryDexInfo>();
|
|
for (var entry : packageDexUse.mSecondaryDexUseByDexFile.entrySet()) {
|
|
String dexPath = entry.getKey();
|
|
SecondaryDexUse secondaryDexUse = entry.getValue();
|
|
|
|
@FileVisibility
|
|
int visibility = checkDexFile ? getDexFileVisibility(dexPath)
|
|
: FileVisibility.OTHER_READABLE;
|
|
if (visibility == FileVisibility.NOT_FOUND) {
|
|
continue;
|
|
}
|
|
|
|
Map<DexLoader, SecondaryDexUseRecord> filteredRecordByLoader;
|
|
if (visibility == FileVisibility.OTHER_READABLE) {
|
|
filteredRecordByLoader = secondaryDexUse.mRecordByLoader;
|
|
} else {
|
|
// Only keep the entry that belongs to the same app.
|
|
DexLoader sameApp = DexLoader.create(packageName, false /* isolatedProcess */);
|
|
SecondaryDexUseRecord record = secondaryDexUse.mRecordByLoader.get(sameApp);
|
|
filteredRecordByLoader = record != null ? Map.of(sameApp, record) : Map.of();
|
|
}
|
|
if (filteredRecordByLoader.isEmpty()) {
|
|
continue;
|
|
}
|
|
List<String> distinctClcList =
|
|
filteredRecordByLoader.values()
|
|
.stream()
|
|
.map(record -> Utils.assertNonEmpty(record.mClassLoaderContext))
|
|
.filter(clc
|
|
-> !clc.equals(
|
|
SecondaryDexInfo.UNSUPPORTED_CLASS_LOADER_CONTEXT))
|
|
.distinct()
|
|
.collect(Collectors.toList());
|
|
String clc;
|
|
if (distinctClcList.size() == 0) {
|
|
clc = SecondaryDexInfo.UNSUPPORTED_CLASS_LOADER_CONTEXT;
|
|
} else if (distinctClcList.size() == 1) {
|
|
clc = distinctClcList.get(0);
|
|
} else {
|
|
// If there are more than one class loader contexts, we can't dexopt the dex
|
|
// file.
|
|
clc = SecondaryDexInfo.VARYING_CLASS_LOADER_CONTEXTS;
|
|
}
|
|
// Although we filter out unsupported CLCs above, `distinctAbiNames` and `loaders`
|
|
// still need to take apps with unsupported CLCs into account because the vdex file
|
|
// is still usable to them.
|
|
Set<String> distinctAbiNames =
|
|
filteredRecordByLoader.values()
|
|
.stream()
|
|
.map(record -> Utils.assertNonEmpty(record.mAbiName))
|
|
.collect(Collectors.toSet());
|
|
Set<DexLoader> loaders = Set.copyOf(filteredRecordByLoader.keySet());
|
|
results.add(DetailedSecondaryDexInfo.create(dexPath,
|
|
Objects.requireNonNull(secondaryDexUse.mUserHandle), clc, distinctAbiNames,
|
|
loaders, isUsedByOtherApps(loaders, packageName),
|
|
visibility == FileVisibility.OTHER_READABLE));
|
|
}
|
|
return Collections.unmodifiableList(results);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Notifies ART Service that a list of dex container files have been loaded.
|
|
*
|
|
* ART Service uses this information to:
|
|
* <ul>
|
|
* <li>Determine whether an app is used by another app
|
|
* <li>Record which secondary dex container files to dexopt and how to dexopt them
|
|
* </ul>
|
|
*
|
|
* @param loadingPackageName the name of the package who performs the load. ART Service assumes
|
|
* that this argument has been validated that it exists in the snapshot and matches the
|
|
* calling UID
|
|
* @param classLoaderContextByDexContainerFile a map from dex container files' absolute paths to
|
|
* the string representations of the class loader contexts used to load them
|
|
* @throws IllegalArgumentException if {@code classLoaderContextByDexContainerFile} contains
|
|
* invalid entries
|
|
*/
|
|
public void notifyDexContainersLoaded(@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
|
|
@NonNull String loadingPackageName,
|
|
@NonNull Map<String, String> classLoaderContextByDexContainerFile) {
|
|
// "android" comes from `SystemServerDexLoadReporter`. ART Services doesn't need to handle
|
|
// this case because it doesn't compile system server and system server isn't allowed to
|
|
// load artifacts produced by ART Services.
|
|
if (loadingPackageName.equals(Utils.PLATFORM_PACKAGE_NAME)) {
|
|
return;
|
|
}
|
|
|
|
validateInputs(snapshot, loadingPackageName, classLoaderContextByDexContainerFile);
|
|
|
|
// TODO(jiakaiz): Investigate whether it should also be considered as isolated process if
|
|
// `Process.isSdkSandboxUid` returns true.
|
|
boolean isolatedProcess = Process.isIsolatedUid(Binder.getCallingUid());
|
|
long lastUsedAtMs = mInjector.getCurrentTimeMillis();
|
|
|
|
for (var entry : classLoaderContextByDexContainerFile.entrySet()) {
|
|
String dexPath = Utils.assertNonEmpty(entry.getKey());
|
|
String classLoaderContext = Utils.assertNonEmpty(entry.getValue());
|
|
String owningPackageName = findOwningPackage(snapshot, loadingPackageName,
|
|
(pkgState) -> isOwningPackageForPrimaryDex(pkgState, dexPath));
|
|
if (owningPackageName != null) {
|
|
addPrimaryDexUse(owningPackageName, dexPath, loadingPackageName, isolatedProcess,
|
|
lastUsedAtMs);
|
|
continue;
|
|
}
|
|
Path path = Paths.get(dexPath);
|
|
synchronized (mLock) {
|
|
owningPackageName = findOwningPackage(snapshot, loadingPackageName,
|
|
(pkgState) -> isOwningPackageForSecondaryDexLocked(pkgState, path));
|
|
}
|
|
if (owningPackageName != null) {
|
|
PackageState loadingPkgState =
|
|
Utils.getPackageStateOrThrow(snapshot, loadingPackageName);
|
|
// An app is always launched with its primary ABI.
|
|
Utils.Abi abi = Utils.getPrimaryAbi(loadingPkgState);
|
|
addSecondaryDexUse(owningPackageName, dexPath, loadingPackageName, isolatedProcess,
|
|
classLoaderContext, abi.name(), lastUsedAtMs);
|
|
continue;
|
|
}
|
|
// It is expected that a dex file isn't owned by any package. For example, the dex
|
|
// file could be a shared library jar.
|
|
}
|
|
}
|
|
|
|
@Nullable
|
|
private static String findOwningPackage(@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
|
|
@NonNull String loadingPackageName,
|
|
@NonNull Function<PackageState, Boolean> predicate) {
|
|
// Most likely, the package is loading its own dex file, so we check this first as an
|
|
// optimization.
|
|
PackageState loadingPkgState = Utils.getPackageStateOrThrow(snapshot, loadingPackageName);
|
|
if (predicate.apply(loadingPkgState)) {
|
|
return loadingPkgState.getPackageName();
|
|
}
|
|
|
|
for (PackageState pkgState : snapshot.getPackageStates().values()) {
|
|
if (predicate.apply(pkgState)) {
|
|
return pkgState.getPackageName();
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static boolean isOwningPackageForPrimaryDex(
|
|
@NonNull PackageState pkgState, @NonNull String dexPath) {
|
|
AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
|
|
List<AndroidPackageSplit> splits = pkg.getSplits();
|
|
for (int i = 0; i < splits.size(); i++) {
|
|
if (splits.get(i).getPath().equals(dexPath)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@GuardedBy("mLock")
|
|
private boolean isOwningPackageForSecondaryDexLocked(
|
|
@NonNull PackageState pkgState, @NonNull Path dexPath) {
|
|
UserHandle userHandle = Binder.getCallingUserHandle();
|
|
List<Path> locations = mSecondaryDexLocationManager.getLocations(pkgState, userHandle);
|
|
for (int i = 0; i < locations.size(); i++) {
|
|
if (dexPath.startsWith(locations.get(i))) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void addPrimaryDexUse(@NonNull String owningPackageName, @NonNull String dexPath,
|
|
@NonNull String loadingPackageName, boolean isolatedProcess, long lastUsedAtMs) {
|
|
synchronized (mLock) {
|
|
PrimaryDexUseRecord record =
|
|
mDexUse.mPackageDexUseByOwningPackageName
|
|
.computeIfAbsent(owningPackageName, k -> new PackageDexUse())
|
|
.mPrimaryDexUseByDexFile
|
|
.computeIfAbsent(dexPath, k -> new PrimaryDexUse())
|
|
.mRecordByLoader.computeIfAbsent(
|
|
DexLoader.create(loadingPackageName, isolatedProcess),
|
|
k -> new PrimaryDexUseRecord());
|
|
record.mLastUsedAtMs = lastUsedAtMs;
|
|
mRevision++;
|
|
}
|
|
maybeSaveAsync();
|
|
}
|
|
|
|
private void addSecondaryDexUse(@NonNull String owningPackageName, @NonNull String dexPath,
|
|
@NonNull String loadingPackageName, boolean isolatedProcess,
|
|
@NonNull String classLoaderContext, @NonNull String abiName, long lastUsedAtMs) {
|
|
synchronized (mLock) {
|
|
SecondaryDexUse secondaryDexUse =
|
|
mDexUse.mPackageDexUseByOwningPackageName
|
|
.computeIfAbsent(owningPackageName, k -> new PackageDexUse())
|
|
.mSecondaryDexUseByDexFile.computeIfAbsent(
|
|
dexPath, k -> new SecondaryDexUse());
|
|
secondaryDexUse.mUserHandle = Binder.getCallingUserHandle();
|
|
SecondaryDexUseRecord record = secondaryDexUse.mRecordByLoader.computeIfAbsent(
|
|
DexLoader.create(loadingPackageName, isolatedProcess),
|
|
k -> new SecondaryDexUseRecord());
|
|
record.mClassLoaderContext = classLoaderContext;
|
|
record.mAbiName = abiName;
|
|
record.mLastUsedAtMs = lastUsedAtMs;
|
|
mRevision++;
|
|
}
|
|
maybeSaveAsync();
|
|
}
|
|
|
|
/** @hide */
|
|
public @NonNull String dump() {
|
|
var builder = DexUseProto.newBuilder();
|
|
synchronized (mLock) {
|
|
mDexUse.toProto(builder);
|
|
}
|
|
return builder.build().toString();
|
|
}
|
|
|
|
private void save() {
|
|
var builder = DexUseProto.newBuilder();
|
|
int thisRevision;
|
|
synchronized (mLock) {
|
|
if (mRevision <= mLastCommittedRevision) {
|
|
return;
|
|
}
|
|
mDexUse.toProto(builder);
|
|
thisRevision = mRevision;
|
|
}
|
|
var file = new File(mInjector.getFilename());
|
|
File tempFile = null;
|
|
try {
|
|
tempFile = File.createTempFile(file.getName(), null /* suffix */, file.getParentFile());
|
|
try (OutputStream out = new FileOutputStream(tempFile.getPath())) {
|
|
builder.build().writeTo(out);
|
|
}
|
|
synchronized (mLock) {
|
|
// Check revision again in case `mLastCommittedRevision` has changed since the check
|
|
// above, to avoid ABA race.
|
|
if (thisRevision > mLastCommittedRevision) {
|
|
Files.move(tempFile.toPath(), file.toPath(),
|
|
StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
|
mLastCommittedRevision = thisRevision;
|
|
}
|
|
}
|
|
} catch (IOException e) {
|
|
Log.e(TAG, "Failed to save dex use data", e);
|
|
} finally {
|
|
Utils.deleteIfExistsSafe(tempFile);
|
|
}
|
|
}
|
|
|
|
private void maybeSaveAsync() {
|
|
mDebouncer.maybeRunAsync(this::save);
|
|
}
|
|
|
|
/** This should only be called during initialization. */
|
|
private void load() {
|
|
DexUseProto proto = null;
|
|
try (InputStream in = new FileInputStream(mInjector.getFilename())) {
|
|
proto = DexUseProto.parseFrom(in);
|
|
} catch (IOException e) {
|
|
// Nothing else we can do but to start from scratch.
|
|
Log.e(TAG, "Failed to load dex use data", e);
|
|
}
|
|
synchronized (mLock) {
|
|
if (mDexUse != null) {
|
|
throw new IllegalStateException("Load has already been attempted");
|
|
}
|
|
mDexUse = new DexUse();
|
|
if (proto != null) {
|
|
mDexUse.fromProto(proto);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static boolean isUsedByOtherApps(
|
|
@NonNull Set<DexLoader> loaders, @NonNull String owningPackageName) {
|
|
return loaders.stream().anyMatch(loader -> isLoaderOtherApp(loader, owningPackageName));
|
|
}
|
|
|
|
/**
|
|
* Returns true if {@code loader} is considered as "other app" (i.e., its process UID is
|
|
* different from the UID of the package represented by {@code owningPackageName}).
|
|
*
|
|
* @hide
|
|
*/
|
|
public static boolean isLoaderOtherApp(
|
|
@NonNull DexLoader loader, @NonNull String owningPackageName) {
|
|
// If the dex file is loaded by an isolated process of the same app, it can also be
|
|
// considered as "used by other apps" because isolated processes are sandboxed and can only
|
|
// read world readable files, so they need the dexopt artifacts to be world readable. An
|
|
// example of such a package is webview.
|
|
return !loader.loadingPackageName().equals(owningPackageName) || loader.isolatedProcess();
|
|
}
|
|
|
|
private static void validateInputs(@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
|
|
@NonNull String loadingPackageName,
|
|
@NonNull Map<String, String> classLoaderContextByDexContainerFile) {
|
|
if (classLoaderContextByDexContainerFile.isEmpty()) {
|
|
throw new IllegalArgumentException("Nothing to record");
|
|
}
|
|
|
|
for (var entry : classLoaderContextByDexContainerFile.entrySet()) {
|
|
Utils.assertNonEmpty(entry.getKey());
|
|
if (!Paths.get(entry.getKey()).isAbsolute()) {
|
|
throw new IllegalArgumentException(String.format(
|
|
"Dex container file path must be absolute, got '%s'", entry.getKey()));
|
|
}
|
|
Utils.assertNonEmpty(entry.getValue());
|
|
}
|
|
|
|
// TODO(b/253570365): Make the validation more strict.
|
|
}
|
|
|
|
private @FileVisibility int getDexFileVisibility(@NonNull String dexPath) {
|
|
try {
|
|
return mInjector.getArtd().getDexFileVisibility(dexPath);
|
|
} catch (ServiceSpecificException | RemoteException e) {
|
|
Log.e(TAG, "Failed to get visibility of " + dexPath, e);
|
|
return FileVisibility.NOT_FOUND;
|
|
}
|
|
}
|
|
|
|
/** @hide */
|
|
@Nullable
|
|
public String getSecondaryClassLoaderContext(
|
|
@NonNull String owningPackageName, @NonNull String dexFile, @NonNull DexLoader loader) {
|
|
synchronized (mLock) {
|
|
return Optional
|
|
.ofNullable(mDexUse.mPackageDexUseByOwningPackageName.get(owningPackageName))
|
|
.map(packageDexUse -> packageDexUse.mSecondaryDexUseByDexFile.get(dexFile))
|
|
.map(secondaryDexUse -> secondaryDexUse.mRecordByLoader.get(loader))
|
|
.map(record -> record.mClassLoaderContext)
|
|
.orElse(null);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cleans up obsolete information about dex files and packages that no longer exist.
|
|
*
|
|
* @hide
|
|
*/
|
|
public void cleanup() {
|
|
Set<String> packageNames = mInjector.getAllPackageNames();
|
|
Map<String, Integer> dexFileVisibilityByName = new HashMap<>();
|
|
|
|
// Scan the data in two passes to avoid holding the lock during I/O.
|
|
synchronized (mLock) {
|
|
for (PackageDexUse packageDexUse : mDexUse.mPackageDexUseByOwningPackageName.values()) {
|
|
for (String dexFile : packageDexUse.mPrimaryDexUseByDexFile.keySet()) {
|
|
dexFileVisibilityByName.put(dexFile, FileVisibility.NOT_FOUND);
|
|
}
|
|
for (String dexFile : packageDexUse.mSecondaryDexUseByDexFile.keySet()) {
|
|
dexFileVisibilityByName.put(dexFile, FileVisibility.NOT_FOUND);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (var entry : dexFileVisibilityByName.entrySet()) {
|
|
entry.setValue(getDexFileVisibility(entry.getKey()));
|
|
}
|
|
|
|
synchronized (mLock) {
|
|
for (var it = mDexUse.mPackageDexUseByOwningPackageName.entrySet().iterator();
|
|
it.hasNext();) {
|
|
Map.Entry<String, PackageDexUse> entry = it.next();
|
|
String owningPackageName = entry.getKey();
|
|
PackageDexUse packageDexUse = entry.getValue();
|
|
|
|
if (!packageNames.contains(owningPackageName)) {
|
|
// Remove information about the non-existing owning package.
|
|
it.remove();
|
|
mRevision++;
|
|
continue;
|
|
}
|
|
|
|
cleanupPrimaryDexUsesLocked(packageDexUse.mPrimaryDexUseByDexFile, packageNames,
|
|
dexFileVisibilityByName, owningPackageName);
|
|
|
|
cleanupSecondaryDexUsesLocked(packageDexUse.mSecondaryDexUseByDexFile, packageNames,
|
|
dexFileVisibilityByName, owningPackageName);
|
|
|
|
if (packageDexUse.mPrimaryDexUseByDexFile.isEmpty()
|
|
&& packageDexUse.mSecondaryDexUseByDexFile.isEmpty()) {
|
|
it.remove();
|
|
mRevision++;
|
|
}
|
|
}
|
|
}
|
|
|
|
maybeSaveAsync();
|
|
}
|
|
|
|
@GuardedBy("mLock")
|
|
private void cleanupPrimaryDexUsesLocked(@NonNull Map<String, PrimaryDexUse> primaryDexUses,
|
|
@NonNull Set<String> packageNames,
|
|
@NonNull Map<String, Integer> dexFileVisibilityByName,
|
|
@NonNull String owningPackageName) {
|
|
for (var it = primaryDexUses.entrySet().iterator(); it.hasNext();) {
|
|
Map.Entry<String, PrimaryDexUse> entry = it.next();
|
|
String dexFile = entry.getKey();
|
|
PrimaryDexUse primaryDexUse = entry.getValue();
|
|
|
|
if (!dexFileVisibilityByName.containsKey(dexFile)) {
|
|
// This can only happen when the file is added after the first pass. We can just
|
|
// keep it as-is and check it in the next `cleanup` run.
|
|
continue;
|
|
}
|
|
|
|
@FileVisibility int visibility = dexFileVisibilityByName.get(dexFile);
|
|
|
|
if (visibility == FileVisibility.NOT_FOUND) {
|
|
// Remove information about the non-existing dex files.
|
|
it.remove();
|
|
mRevision++;
|
|
continue;
|
|
}
|
|
|
|
cleanupRecordsLocked(
|
|
primaryDexUse.mRecordByLoader, packageNames, visibility, owningPackageName);
|
|
|
|
if (primaryDexUse.mRecordByLoader.isEmpty()) {
|
|
it.remove();
|
|
mRevision++;
|
|
}
|
|
}
|
|
}
|
|
|
|
@GuardedBy("mLock")
|
|
private void cleanupSecondaryDexUsesLocked(
|
|
@NonNull Map<String, SecondaryDexUse> secondaryDexUses,
|
|
@NonNull Set<String> packageNames,
|
|
@NonNull Map<String, Integer> dexFileVisibilityByName,
|
|
@NonNull String owningPackageName) {
|
|
for (var it = secondaryDexUses.entrySet().iterator(); it.hasNext();) {
|
|
Map.Entry<String, SecondaryDexUse> entry = it.next();
|
|
String dexFile = entry.getKey();
|
|
SecondaryDexUse secondaryDexUse = entry.getValue();
|
|
|
|
if (!dexFileVisibilityByName.containsKey(dexFile)) {
|
|
// This can only happen when the file is added after the first pass. We can just
|
|
// keep it as-is and check it in the next `cleanup` run.
|
|
continue;
|
|
}
|
|
|
|
@FileVisibility int visibility = dexFileVisibilityByName.get(dexFile);
|
|
|
|
// Remove information about non-existing dex files.
|
|
if (visibility == FileVisibility.NOT_FOUND) {
|
|
it.remove();
|
|
mRevision++;
|
|
continue;
|
|
}
|
|
|
|
cleanupRecordsLocked(
|
|
secondaryDexUse.mRecordByLoader, packageNames, visibility, owningPackageName);
|
|
|
|
if (secondaryDexUse.mRecordByLoader.isEmpty()) {
|
|
it.remove();
|
|
mRevision++;
|
|
}
|
|
}
|
|
}
|
|
|
|
@GuardedBy("mLock")
|
|
private void cleanupRecordsLocked(@NonNull Map<DexLoader, ?> records,
|
|
@NonNull Set<String> packageNames, @FileVisibility int visibility,
|
|
@NonNull String owningPackageName) {
|
|
for (var it = records.entrySet().iterator(); it.hasNext();) {
|
|
Map.Entry<DexLoader, ?> entry = it.next();
|
|
DexLoader loader = entry.getKey();
|
|
|
|
if (!packageNames.contains(loader.loadingPackageName())) {
|
|
// Remove information about the non-existing loading package.
|
|
it.remove();
|
|
mRevision++;
|
|
continue;
|
|
}
|
|
|
|
if (visibility == FileVisibility.NOT_OTHER_READABLE
|
|
&& isLoaderOtherApp(loader, owningPackageName)) {
|
|
// The visibility must have changed since the last load. The loader cannot load this
|
|
// dex file anymore.
|
|
it.remove();
|
|
mRevision++;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Basic information about a secondary dex file (an APK or JAR file that an app adds to its
|
|
* own data directory and loads dynamically).
|
|
*
|
|
* @hide
|
|
*/
|
|
@Immutable
|
|
public abstract static class SecondaryDexInfo implements DetailedDexInfo {
|
|
// Special encoding used to denote a foreign ClassLoader was found when trying to encode
|
|
// class loader contexts for each classpath element in a ClassLoader.
|
|
// Must be in sync with `kUnsupportedClassLoaderContextEncoding` in
|
|
// `art/runtime/class_loader_context.h`.
|
|
public static final String UNSUPPORTED_CLASS_LOADER_CONTEXT =
|
|
"=UnsupportedClassLoaderContext=";
|
|
|
|
// Special encoding used to denote that a dex file is loaded by different packages with
|
|
// different ClassLoader's. Only for display purpose (e.g., in dumpsys). This value is not
|
|
// written to the file, and so far only used here.
|
|
@VisibleForTesting
|
|
public static final String VARYING_CLASS_LOADER_CONTEXTS = "=VaryingClassLoaderContexts=";
|
|
|
|
/** The absolute path to the dex file within the user's app data directory. */
|
|
public abstract @NonNull String dexPath();
|
|
|
|
/**
|
|
* The {@link UserHandle} that represents the human user who owns and loads the dex file. A
|
|
* secondary dex file belongs to a specific human user, and only that user can load it.
|
|
*/
|
|
public abstract @NonNull UserHandle userHandle();
|
|
|
|
/**
|
|
* A string describing the structure of the class loader that the dex file is loaded with,
|
|
* or {@link #UNSUPPORTED_CLASS_LOADER_CONTEXT} or {@link #VARYING_CLASS_LOADER_CONTEXTS}.
|
|
*/
|
|
public abstract @NonNull String displayClassLoaderContext();
|
|
|
|
/**
|
|
* A string describing the structure of the class loader that the dex file is loaded with,
|
|
* or null if the class loader context is invalid.
|
|
*/
|
|
public @Nullable String classLoaderContext() {
|
|
return !displayClassLoaderContext().equals(UNSUPPORTED_CLASS_LOADER_CONTEXT)
|
|
&& !displayClassLoaderContext().equals(VARYING_CLASS_LOADER_CONTEXTS)
|
|
? displayClassLoaderContext()
|
|
: null;
|
|
}
|
|
|
|
/** The set of ABIs of the dex file is loaded with. Guaranteed to be non-empty. */
|
|
public abstract @NonNull Set<String> abiNames();
|
|
|
|
/** The set of entities that load the dex file. Guaranteed to be non-empty. */
|
|
public abstract @NonNull Set<DexLoader> loaders();
|
|
|
|
/** Returns whether the dex file is used by apps other than the app that owns it. */
|
|
public abstract boolean isUsedByOtherApps();
|
|
}
|
|
|
|
/**
|
|
* Detailed information about a secondary dex file (an APK or JAR file that an app adds to its
|
|
* own data directory and loads dynamically). It contains the visibility of the dex file in
|
|
* addition to what is in {@link SecondaryDexInfo}, but producing it requires disk IO.
|
|
*
|
|
* @hide
|
|
*/
|
|
@Immutable
|
|
@AutoValue
|
|
public abstract static class DetailedSecondaryDexInfo
|
|
extends SecondaryDexInfo implements DetailedDexInfo {
|
|
static DetailedSecondaryDexInfo create(@NonNull String dexPath,
|
|
@NonNull UserHandle userHandle, @NonNull String displayClassLoaderContext,
|
|
@NonNull Set<String> abiNames, @NonNull Set<DexLoader> loaders,
|
|
boolean isUsedByOtherApps, boolean isDexFilePublic) {
|
|
return new AutoValue_DexUseManagerLocal_DetailedSecondaryDexInfo(dexPath, userHandle,
|
|
displayClassLoaderContext, Collections.unmodifiableSet(abiNames),
|
|
Collections.unmodifiableSet(loaders), isUsedByOtherApps, isDexFilePublic);
|
|
}
|
|
|
|
/**
|
|
* Returns true if the filesystem permission of the dex file has the "read" bit for "others"
|
|
* (S_IROTH).
|
|
*/
|
|
public abstract boolean isDexFilePublic();
|
|
}
|
|
|
|
private static class DexUse {
|
|
@NonNull Map<String, PackageDexUse> mPackageDexUseByOwningPackageName = new HashMap<>();
|
|
|
|
void toProto(@NonNull DexUseProto.Builder builder) {
|
|
for (var entry : mPackageDexUseByOwningPackageName.entrySet()) {
|
|
var packageBuilder =
|
|
PackageDexUseProto.newBuilder().setOwningPackageName(entry.getKey());
|
|
entry.getValue().toProto(packageBuilder);
|
|
builder.addPackageDexUse(packageBuilder);
|
|
}
|
|
}
|
|
|
|
void fromProto(@NonNull DexUseProto proto) {
|
|
for (PackageDexUseProto packageProto : proto.getPackageDexUseList()) {
|
|
var packageDexUse = new PackageDexUse();
|
|
packageDexUse.fromProto(packageProto);
|
|
mPackageDexUseByOwningPackageName.put(
|
|
Utils.assertNonEmpty(packageProto.getOwningPackageName()), packageDexUse);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static class PackageDexUse {
|
|
/**
|
|
* The keys are absolute paths to primary dex files of the owning package (the base APK and
|
|
* split APKs).
|
|
*/
|
|
@NonNull Map<String, PrimaryDexUse> mPrimaryDexUseByDexFile = new HashMap<>();
|
|
|
|
/**
|
|
* The keys are absolute paths to secondary dex files of the owning package (the APKs and
|
|
* JARs in CE and DE directories).
|
|
*/
|
|
@NonNull Map<String, SecondaryDexUse> mSecondaryDexUseByDexFile = new HashMap<>();
|
|
|
|
void toProto(@NonNull PackageDexUseProto.Builder builder) {
|
|
for (var entry : mPrimaryDexUseByDexFile.entrySet()) {
|
|
var primaryBuilder = PrimaryDexUseProto.newBuilder().setDexFile(entry.getKey());
|
|
entry.getValue().toProto(primaryBuilder);
|
|
builder.addPrimaryDexUse(primaryBuilder);
|
|
}
|
|
for (var entry : mSecondaryDexUseByDexFile.entrySet()) {
|
|
var secondaryBuilder = SecondaryDexUseProto.newBuilder().setDexFile(entry.getKey());
|
|
entry.getValue().toProto(secondaryBuilder);
|
|
builder.addSecondaryDexUse(secondaryBuilder);
|
|
}
|
|
}
|
|
|
|
void fromProto(@NonNull PackageDexUseProto proto) {
|
|
for (PrimaryDexUseProto primaryProto : proto.getPrimaryDexUseList()) {
|
|
var primaryDexUse = new PrimaryDexUse();
|
|
primaryDexUse.fromProto(primaryProto);
|
|
mPrimaryDexUseByDexFile.put(
|
|
Utils.assertNonEmpty(primaryProto.getDexFile()), primaryDexUse);
|
|
}
|
|
for (SecondaryDexUseProto secondaryProto : proto.getSecondaryDexUseList()) {
|
|
var secondaryDexUse = new SecondaryDexUse();
|
|
secondaryDexUse.fromProto(secondaryProto);
|
|
mSecondaryDexUseByDexFile.put(
|
|
Utils.assertNonEmpty(secondaryProto.getDexFile()), secondaryDexUse);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static class PrimaryDexUse {
|
|
@NonNull Map<DexLoader, PrimaryDexUseRecord> mRecordByLoader = new HashMap<>();
|
|
|
|
void toProto(@NonNull PrimaryDexUseProto.Builder builder) {
|
|
for (var entry : mRecordByLoader.entrySet()) {
|
|
var recordBuilder =
|
|
PrimaryDexUseRecordProto.newBuilder()
|
|
.setLoadingPackageName(entry.getKey().loadingPackageName())
|
|
.setIsolatedProcess(entry.getKey().isolatedProcess());
|
|
entry.getValue().toProto(recordBuilder);
|
|
builder.addRecord(recordBuilder);
|
|
}
|
|
}
|
|
|
|
void fromProto(@NonNull PrimaryDexUseProto proto) {
|
|
for (PrimaryDexUseRecordProto recordProto : proto.getRecordList()) {
|
|
var record = new PrimaryDexUseRecord();
|
|
record.fromProto(recordProto);
|
|
mRecordByLoader.put(
|
|
DexLoader.create(Utils.assertNonEmpty(recordProto.getLoadingPackageName()),
|
|
recordProto.getIsolatedProcess()),
|
|
record);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static class SecondaryDexUse {
|
|
@Nullable UserHandle mUserHandle = null;
|
|
@NonNull Map<DexLoader, SecondaryDexUseRecord> mRecordByLoader = new HashMap<>();
|
|
|
|
void toProto(@NonNull SecondaryDexUseProto.Builder builder) {
|
|
builder.setUserId(Int32Value.newBuilder().setValue(mUserHandle.getIdentifier()));
|
|
for (var entry : mRecordByLoader.entrySet()) {
|
|
var recordBuilder =
|
|
SecondaryDexUseRecordProto.newBuilder()
|
|
.setLoadingPackageName(entry.getKey().loadingPackageName())
|
|
.setIsolatedProcess(entry.getKey().isolatedProcess());
|
|
entry.getValue().toProto(recordBuilder);
|
|
builder.addRecord(recordBuilder);
|
|
}
|
|
}
|
|
|
|
void fromProto(@NonNull SecondaryDexUseProto proto) {
|
|
Utils.check(proto.hasUserId());
|
|
mUserHandle = UserHandle.of(proto.getUserId().getValue());
|
|
for (SecondaryDexUseRecordProto recordProto : proto.getRecordList()) {
|
|
var record = new SecondaryDexUseRecord();
|
|
record.fromProto(recordProto);
|
|
mRecordByLoader.put(
|
|
DexLoader.create(Utils.assertNonEmpty(recordProto.getLoadingPackageName()),
|
|
recordProto.getIsolatedProcess()),
|
|
record);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Represents an entity that loads a dex file.
|
|
*
|
|
* @hide
|
|
*/
|
|
@Immutable
|
|
@AutoValue
|
|
public abstract static class DexLoader implements Comparable<DexLoader> {
|
|
static DexLoader create(@NonNull String loadingPackageName, boolean isolatedProcess) {
|
|
return new AutoValue_DexUseManagerLocal_DexLoader(loadingPackageName, isolatedProcess);
|
|
}
|
|
|
|
abstract @NonNull String loadingPackageName();
|
|
|
|
/** @see Process#isIsolatedUid(int) */
|
|
abstract boolean isolatedProcess();
|
|
|
|
@Override
|
|
@NonNull
|
|
public String toString() {
|
|
return loadingPackageName() + (isolatedProcess() ? " (isolated)" : "");
|
|
}
|
|
|
|
@Override
|
|
public int compareTo(DexLoader o) {
|
|
return Comparator.comparing(DexLoader::loadingPackageName)
|
|
.thenComparing(DexLoader::isolatedProcess)
|
|
.compare(this, o);
|
|
}
|
|
}
|
|
|
|
private static class PrimaryDexUseRecord {
|
|
@Nullable long mLastUsedAtMs = 0;
|
|
|
|
void toProto(@NonNull PrimaryDexUseRecordProto.Builder builder) {
|
|
builder.setLastUsedAtMs(mLastUsedAtMs);
|
|
}
|
|
|
|
void fromProto(@NonNull PrimaryDexUseRecordProto proto) {
|
|
mLastUsedAtMs = proto.getLastUsedAtMs();
|
|
Utils.check(mLastUsedAtMs > 0);
|
|
}
|
|
}
|
|
|
|
private static class SecondaryDexUseRecord {
|
|
// An app constructs their own class loader to load a secondary dex file, so only itself
|
|
// knows the class loader context. Therefore, we need to record the class loader context
|
|
// reported by the app.
|
|
@Nullable String mClassLoaderContext = null;
|
|
@Nullable String mAbiName = null;
|
|
@Nullable long mLastUsedAtMs = 0;
|
|
|
|
void toProto(@NonNull SecondaryDexUseRecordProto.Builder builder) {
|
|
builder.setClassLoaderContext(mClassLoaderContext)
|
|
.setAbiName(mAbiName)
|
|
.setLastUsedAtMs(mLastUsedAtMs);
|
|
}
|
|
|
|
void fromProto(@NonNull SecondaryDexUseRecordProto proto) {
|
|
mClassLoaderContext = Utils.assertNonEmpty(proto.getClassLoaderContext());
|
|
mAbiName = Utils.assertNonEmpty(proto.getAbiName());
|
|
mLastUsedAtMs = proto.getLastUsedAtMs();
|
|
Utils.check(mLastUsedAtMs > 0);
|
|
}
|
|
}
|
|
|
|
// TODO(b/278697552): Consider removing the cache or moving it to `Environment`.
|
|
static class SecondaryDexLocationManager {
|
|
private @NonNull Map<CacheKey, CacheValue> mCache = new HashMap<>();
|
|
|
|
public @NonNull List<Path> getLocations(
|
|
@NonNull PackageState pkgState, @NonNull UserHandle userHandle) {
|
|
AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
|
|
UUID storageUuid = pkg.getStorageUuid();
|
|
String packageName = pkgState.getPackageName();
|
|
|
|
CacheKey cacheKey = CacheKey.create(packageName, userHandle);
|
|
CacheValue cacheValue = mCache.get(cacheKey);
|
|
if (cacheValue != null && cacheValue.storageUuid().equals(storageUuid)) {
|
|
return cacheValue.locations();
|
|
}
|
|
|
|
File ceDir = Environment.getDataCePackageDirectoryForUser(
|
|
storageUuid, userHandle, packageName);
|
|
File deDir = Environment.getDataDePackageDirectoryForUser(
|
|
storageUuid, userHandle, packageName);
|
|
List<Path> locations = List.of(ceDir.toPath(), deDir.toPath());
|
|
mCache.put(cacheKey, CacheValue.create(locations, storageUuid));
|
|
return locations;
|
|
}
|
|
|
|
@Immutable
|
|
@AutoValue
|
|
abstract static class CacheKey {
|
|
static CacheKey create(@NonNull String packageName, @NonNull UserHandle userHandle) {
|
|
return new AutoValue_DexUseManagerLocal_SecondaryDexLocationManager_CacheKey(
|
|
packageName, userHandle);
|
|
}
|
|
|
|
abstract @NonNull String packageName();
|
|
|
|
abstract @NonNull UserHandle userHandle();
|
|
}
|
|
|
|
@Immutable
|
|
@AutoValue
|
|
abstract static class CacheValue {
|
|
static CacheValue create(@NonNull List<Path> locations, @NonNull UUID storageUuid) {
|
|
return new AutoValue_DexUseManagerLocal_SecondaryDexLocationManager_CacheValue(
|
|
locations, storageUuid);
|
|
}
|
|
|
|
abstract @NonNull List<Path> locations();
|
|
|
|
abstract @NonNull UUID storageUuid();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Injector pattern for testing purpose.
|
|
*
|
|
* @hide
|
|
*/
|
|
@VisibleForTesting
|
|
public static class Injector {
|
|
@NonNull private final Context mContext;
|
|
|
|
Injector(@NonNull Context context) {
|
|
mContext = context;
|
|
|
|
// Call the getters for various dependencies, to ensure correct initialization order.
|
|
ArtModuleServiceInitializer.getArtModuleServiceManager();
|
|
getPackageManagerLocal();
|
|
}
|
|
|
|
@NonNull
|
|
public IArtd getArtd() {
|
|
return Utils.getArtd();
|
|
}
|
|
|
|
public long getCurrentTimeMillis() {
|
|
return System.currentTimeMillis();
|
|
}
|
|
|
|
@NonNull
|
|
public String getFilename() {
|
|
return FILENAME;
|
|
}
|
|
|
|
@NonNull
|
|
public ScheduledExecutorService createScheduledExecutor() {
|
|
return Executors.newSingleThreadScheduledExecutor();
|
|
}
|
|
|
|
@NonNull
|
|
public Context getContext() {
|
|
return mContext;
|
|
}
|
|
|
|
@NonNull
|
|
public Set<String> getAllPackageNames() {
|
|
try (PackageManagerLocal.UnfilteredSnapshot snapshot =
|
|
getPackageManagerLocal().withUnfilteredSnapshot()) {
|
|
return new HashSet<>(snapshot.getPackageStates().keySet());
|
|
}
|
|
}
|
|
|
|
@NonNull
|
|
private PackageManagerLocal getPackageManagerLocal() {
|
|
return Objects.requireNonNull(
|
|
LocalManagerRegistry.getManager(PackageManagerLocal.class));
|
|
}
|
|
}
|
|
}
|