blob: 9c8ee562849cd5ceec3baa37f38ddf2190076fb7 [file] [log] [blame]
/*
* Copyright (C) 2019 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 android.os.incremental;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemService;
import android.content.Context;
import android.content.pm.DataLoaderParams;
import android.content.pm.IPackageLoadingProgressCallback;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Slog;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Objects;
/**
* Provides operations to open or create an IncrementalStorage, using IIncrementalService
* service. Example Usage:
*
* <blockquote><pre>
* IncrementalManager manager = (IncrementalManager) getSystemService(Context.INCREMENTAL_SERVICE);
* IncrementalStorage storage = manager.openStorage("/path/to/incremental/dir");
* </pre></blockquote>
*
* @hide
*/
@SystemService(Context.INCREMENTAL_SERVICE)
public final class IncrementalManager {
private static final String TAG = "IncrementalManager";
private static final String ALLOWED_PROPERTY = "incremental.allowed";
public static final int MIN_VERSION_TO_SUPPORT_FSVERITY = 2;
public static final int CREATE_MODE_TEMPORARY_BIND =
IIncrementalService.CREATE_MODE_TEMPORARY_BIND;
public static final int CREATE_MODE_PERMANENT_BIND =
IIncrementalService.CREATE_MODE_PERMANENT_BIND;
public static final int CREATE_MODE_CREATE =
IIncrementalService.CREATE_MODE_CREATE;
public static final int CREATE_MODE_OPEN_EXISTING =
IIncrementalService.CREATE_MODE_OPEN_EXISTING;
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = {"CREATE_MODE_"}, value = {
CREATE_MODE_TEMPORARY_BIND,
CREATE_MODE_PERMANENT_BIND,
CREATE_MODE_CREATE,
CREATE_MODE_OPEN_EXISTING,
})
public @interface CreateMode {
}
private final @Nullable IIncrementalService mService;
private final LoadingProgressCallbacks mLoadingProgressCallbacks =
new LoadingProgressCallbacks();
public IncrementalManager(IIncrementalService service) {
mService = service;
}
/**
* Opens or create an Incremental File System mounted directory and returns an
* IncrementalStorage object.
*
* @param path Absolute path to mount Incremental File System on.
* @param params IncrementalDataLoaderParams object to configure data loading.
* @param createMode Mode for opening an old Incremental File System mount or creating
* a new mount.
* @return IncrementalStorage object corresponding to the mounted directory.
*/
@Nullable
public IncrementalStorage createStorage(@NonNull String path,
@NonNull DataLoaderParams params,
@CreateMode int createMode) {
Objects.requireNonNull(path);
Objects.requireNonNull(params);
try {
final int id = mService.createStorage(path, params.getData(), createMode);
if (id < 0) {
return null;
}
return new IncrementalStorage(mService, id);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Opens an existing Incremental File System mounted directory and returns an IncrementalStorage
* object.
*
* @param path Absolute target path that Incremental File System has been mounted on.
* @return IncrementalStorage object corresponding to the mounted directory.
*/
@Nullable
public IncrementalStorage openStorage(@NonNull String path) {
try {
final int id = mService.openStorage(path);
if (id < 0) {
return null;
}
final IncrementalStorage storage = new IncrementalStorage(mService, id);
return storage;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Opens or creates an IncrementalStorage that is linked to another IncrementalStorage.
*
* @return IncrementalStorage object corresponding to the linked storage.
*/
@Nullable
public IncrementalStorage createStorage(@NonNull String path,
@NonNull IncrementalStorage linkedStorage, @CreateMode int createMode) {
try {
final int id = mService.createLinkedStorage(
path, linkedStorage.getId(), createMode);
if (id < 0) {
return null;
}
return new IncrementalStorage(mService, id);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Link an app's files from the stage dir to the final installation location.
* The expected outcome of this method is:
* 1) The actual apk directory under /data/incremental is bind-mounted to the parent directory
* of {@code afterCodeFile}.
* 2) All the files under {@code beforeCodeFile} will show up under {@code afterCodeFile}.
*
* @param beforeCodeFile Path that is currently bind-mounted and have APKs under it.
* Example: /data/app/vmdl*tmp
* @param afterCodeFile Path that should will have APKs after this method is called. Its parent
* directory should be bind-mounted to a directory under /data/incremental.
* Example: /data/app/~~[randomStringA]/[packageName]-[randomStringB]
* @throws IllegalArgumentException
* @throws IOException
*/
public void linkCodePath(File beforeCodeFile, File afterCodeFile)
throws IllegalArgumentException, IOException {
final File beforeCodeAbsolute = beforeCodeFile.getAbsoluteFile();
final IncrementalStorage apkStorage = openStorage(beforeCodeAbsolute.toString());
if (apkStorage == null) {
throw new IllegalArgumentException("Not an Incremental path: " + beforeCodeAbsolute);
}
final String targetStorageDir = afterCodeFile.getAbsoluteFile().getParent();
final IncrementalStorage linkedApkStorage =
createStorage(targetStorageDir, apkStorage,
IncrementalManager.CREATE_MODE_CREATE
| IncrementalManager.CREATE_MODE_PERMANENT_BIND);
if (linkedApkStorage == null) {
throw new IOException("Failed to create linked storage at dir: " + targetStorageDir);
}
try {
final String afterCodePathName = afterCodeFile.getName();
linkFiles(apkStorage, beforeCodeAbsolute, "", linkedApkStorage, afterCodePathName);
} catch (Exception e) {
linkedApkStorage.unBind(targetStorageDir);
throw e;
}
}
/**
* Recursively set up directories and link all the files from source storage to target storage.
*
* @param sourceStorage The storage that has all the files and directories underneath.
* @param sourceAbsolutePath The absolute path of the directory that holds all files and dirs.
* @param sourceRelativePath The relative path on the source directory, e.g., "" or "lib".
* @param targetStorage The target storage that will have the same files and directories.
* @param targetRelativePath The relative path to the directory on the target storage that
* should have all the files and dirs underneath,
* e.g., "packageName-random".
* @throws IOException When makeDirectory or makeLink fails on the Incremental File System.
*/
private void linkFiles(IncrementalStorage sourceStorage, File sourceAbsolutePath,
String sourceRelativePath, IncrementalStorage targetStorage,
String targetRelativePath) throws IOException {
final Path sourceBase = sourceAbsolutePath.toPath().resolve(sourceRelativePath);
final Path targetRelative = Paths.get(targetRelativePath);
Files.walkFileTree(sourceAbsolutePath.toPath(), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException {
final Path relativeDir = sourceBase.relativize(dir);
targetStorage.makeDirectory(targetRelative.resolve(relativeDir).toString());
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
final Path relativeFile = sourceBase.relativize(file);
sourceStorage.makeLink(
file.toAbsolutePath().toString(), targetStorage,
targetRelative.resolve(relativeFile).toString());
return FileVisitResult.CONTINUE;
}
});
}
/**
* Checks if Incremental feature is enabled on this device.
*/
public static boolean isFeatureEnabled() {
return nativeIsEnabled();
}
/**
* 0 - IncFs is disabled.
* 1 - IncFs v1, core features, no PerUid support. Optional in R.
* 2 - IncFs v2, PerUid support, fs-verity support. Required in S.
*/
public static int getVersion() {
return nativeIsEnabled() ? nativeIsV2Available() ? 2 : 1 : 0;
}
/**
* Checks if Incremental installations are allowed.
* A developer can disable Incremental installations by setting the property.
*/
public static boolean isAllowed() {
return isFeatureEnabled() && android.os.SystemProperties.getBoolean(ALLOWED_PROPERTY, true);
}
/**
* Checks if path is mounted on Incremental File System.
*/
public static boolean isIncrementalPath(@NonNull String path) {
return nativeIsIncrementalPath(path);
}
/**
* Checks if an fd corresponds to a file on a mounted Incremental File System.
*/
public static boolean isIncrementalFileFd(@NonNull FileDescriptor fd) {
return nativeIsIncrementalFd(fd.getInt$());
}
/**
* Returns raw signature for file if it's on Incremental File System.
* Unsafe, use only if you are sure what you are doing.
*/
public static @Nullable byte[] unsafeGetFileSignature(@NonNull String path) {
return nativeUnsafeGetFileSignature(path);
}
/**
* Closes a storage specified by the absolute path. If the path is not Incremental, do nothing.
* Unbinds the target dir and deletes the corresponding storage instance.
* Deletes the package name and associated storage id from maps.
*/
public void rmPackageDir(@NonNull File codeFile) {
try {
final String codePath = codeFile.getAbsolutePath();
final IncrementalStorage storage = openStorage(codePath);
if (storage == null) {
return;
}
mLoadingProgressCallbacks.cleanUpCallbacks(storage);
storage.unBind(codePath);
} catch (IOException e) {
Slog.w(TAG, "Failed to remove code path", e);
}
}
/**
* Called when a new callback wants to listen to the loading progress of an installed package.
* Increment the count of callbacks associated to the corresponding storage.
* Only register storage listener if there hasn't been any existing callback on the storage yet.
* @param codePath Path of the installed package. This path is on an Incremental Storage.
* @param callback To report loading progress to.
* @return True if the package name and associated storage id are valid. False otherwise.
*/
public boolean registerLoadingProgressCallback(@NonNull String codePath,
@NonNull IPackageLoadingProgressCallback callback) {
final IncrementalStorage storage = openStorage(codePath);
if (storage == null) {
// storage does not exist, package not installed
return false;
}
return mLoadingProgressCallbacks.registerCallback(storage, callback);
}
/**
* Called to stop all listeners from listening to loading progress of an installed package.
* @param codePath Path of the installed package
*/
public void unregisterLoadingProgressCallbacks(@NonNull String codePath) {
final IncrementalStorage storage = openStorage(codePath);
if (storage == null) {
// storage does not exist, package not installed
return;
}
mLoadingProgressCallbacks.cleanUpCallbacks(storage);
}
private static class LoadingProgressCallbacks extends IStorageLoadingProgressListener.Stub {
@GuardedBy("mCallbacks")
private final SparseArray<RemoteCallbackList<IPackageLoadingProgressCallback>> mCallbacks =
new SparseArray<>();
public void cleanUpCallbacks(@NonNull IncrementalStorage storage) {
final int storageId = storage.getId();
final RemoteCallbackList<IPackageLoadingProgressCallback> callbacksForStorage;
synchronized (mCallbacks) {
callbacksForStorage = mCallbacks.removeReturnOld(storageId);
}
if (callbacksForStorage == null) {
return;
}
// Unregister all existing callbacks on this storage
callbacksForStorage.kill();
storage.unregisterLoadingProgressListener();
}
public boolean registerCallback(@NonNull IncrementalStorage storage,
@NonNull IPackageLoadingProgressCallback callback) {
final int storageId = storage.getId();
synchronized (mCallbacks) {
RemoteCallbackList<IPackageLoadingProgressCallback> callbacksForStorage =
mCallbacks.get(storageId);
if (callbacksForStorage == null) {
callbacksForStorage = new RemoteCallbackList<>();
mCallbacks.put(storageId, callbacksForStorage);
}
// Registration in RemoteCallbackList needs to be done first, such that when events
// come from Incremental Service, the callback is already registered
callbacksForStorage.register(callback);
if (callbacksForStorage.getRegisteredCallbackCount() > 1) {
// already listening for progress for this storage
return true;
}
}
return storage.registerLoadingProgressListener(this);
}
@Override
public void onStorageLoadingProgressChanged(int storageId, float progress) {
final RemoteCallbackList<IPackageLoadingProgressCallback> callbacksForStorage;
synchronized (mCallbacks) {
callbacksForStorage = mCallbacks.get(storageId);
}
if (callbacksForStorage == null) {
// no callback has ever been registered on this storage
return;
}
final int n = callbacksForStorage.beginBroadcast();
// RemoteCallbackList use ArrayMap internally and it's safe to iterate this way
for (int i = 0; i < n; i++) {
final IPackageLoadingProgressCallback callback =
callbacksForStorage.getBroadcastItem(i);
try {
callback.onPackageLoadingProgressChanged(progress);
} catch (RemoteException ignored) {
}
}
callbacksForStorage.finishBroadcast();
}
}
/**
* Returns the metrics of an Incremental Storage.
*/
public IncrementalMetrics getMetrics(@NonNull String codePath) {
final IncrementalStorage storage = openStorage(codePath);
if (storage == null) {
// storage does not exist, package not installed
return null;
}
return new IncrementalMetrics(storage.getMetrics());
}
/* Native methods */
private static native boolean nativeIsEnabled();
private static native boolean nativeIsV2Available();
private static native boolean nativeIsIncrementalPath(@NonNull String path);
private static native boolean nativeIsIncrementalFd(@NonNull int fd);
private static native byte[] nativeUnsafeGetFileSignature(@NonNull String path);
}