/* * 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.NonNull; import android.annotation.Nullable; import android.content.pm.DataLoaderParams; import android.content.pm.IDataLoaderStatusListener; import android.os.PersistableBundle; import android.os.RemoteException; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Objects; import java.util.UUID; /** * Provides operations on an Incremental File System directory, using IncrementalServiceNative. * Example usage: * *
* * @hide */ public final class IncrementalStorage { private static final String TAG = "IncrementalStorage"; private final int mId; private final IIncrementalService mService; public IncrementalStorage(@NonNull IIncrementalService is, int id) { mService = is; mId = id; } public int getId() { return mId; } /** * Temporarily bind-mounts the current storage directory to a target directory. The bind-mount * will NOT be preserved between device reboots. * * @param targetPath Absolute path to the target directory. */ public void bind(@NonNull String targetPath) throws IOException { bind("", targetPath); } /** * Temporarily bind-mounts a subdir under the current storage directory to a target directory. * The bind-mount will NOT be preserved between device reboots. * * @param sourcePath Source path as a relative path under current storage * directory. * @param targetPath Absolute path to the target directory. */ public void bind(@NonNull String sourcePath, @NonNull String targetPath) throws IOException { try { int res = mService.makeBindMount(mId, sourcePath, targetPath, IIncrementalService.BIND_TEMPORARY); if (res < 0) { throw new IOException("bind() failed with errno " + -res); } } catch (RemoteException e) { e.rethrowFromSystemServer(); } } /** * Permanently bind-mounts the current storage directory to a target directory. The bind-mount * WILL be preserved between device reboots. * * @param targetPath Absolute path to the target directory. */ public void bindPermanent(@NonNull String targetPath) throws IOException { bindPermanent("", targetPath); } /** * Permanently bind-mounts a subdir under the current storage directory to a target directory. * The bind-mount WILL be preserved between device reboots. * * @param sourcePath Relative path under the current storage directory. * @param targetPath Absolute path to the target directory. */ public void bindPermanent(@NonNull String sourcePath, @NonNull String targetPath) throws IOException { try { int res = mService.makeBindMount(mId, sourcePath, targetPath, IIncrementalService.BIND_PERMANENT); if (res < 0) { throw new IOException("bind() permanent failed with errno " + -res); } } catch (RemoteException e) { e.rethrowFromSystemServer(); } } /** * Unbinds a bind mount. * * @param targetPath Absolute path to the target directory. */ public void unBind(@NonNull String targetPath) throws IOException { try { int res = mService.deleteBindMount(mId, targetPath); if (res < 0) { throw new IOException("unbind() failed with errno " + -res); } } catch (RemoteException e) { e.rethrowFromSystemServer(); } } /** * Creates a sub-directory under the current storage directory. * * @param path Relative path of the sub-directory, e.g., "subdir" */ public void makeDirectory(@NonNull String path) throws IOException { try { int res = mService.makeDirectory(mId, path); if (res < 0) { throw new IOException("makeDirectory() failed with errno " + -res); } } catch (RemoteException e) { e.rethrowFromSystemServer(); } } /** * Creates a sub-directory under the current storage directory. If its parent dirs do not exist, * create the parent dirs as well. * * @param path Full path. */ public void makeDirectories(@NonNull String path) throws IOException { try { int res = mService.makeDirectories(mId, path); if (res < 0) { throw new IOException("makeDirectory() failed with errno " + -res); } } catch (RemoteException e) { e.rethrowFromSystemServer(); } } /** * Creates a file under the current storage directory. * * @param path Relative path of the new file. * @param size Size of the new file in bytes. * @param mode File access permission mode. * @param metadata Metadata bytes. * @param v4signatureBytes Serialized V4SignatureProto. * @param content Optionally set file content. */ public void makeFile(@NonNull String path, long size, int mode, @Nullable UUID id, @Nullable byte[] metadata, @Nullable byte[] v4signatureBytes, @Nullable byte[] content) throws IOException { try { if (id == null && metadata == null) { throw new IOException("File ID and metadata cannot both be null"); } validateV4Signature(v4signatureBytes); final IncrementalNewFileParams params = new IncrementalNewFileParams(); params.size = size; params.metadata = (metadata == null ? new byte[0] : metadata); params.fileId = idToBytes(id); params.signature = v4signatureBytes; int res = mService.makeFile(mId, path, mode, params, content); if (res != 0) { throw new IOException("makeFile() failed with errno " + -res); } } catch (RemoteException e) { e.rethrowFromSystemServer(); } } /** * Creates a file in Incremental storage. The content of the file is mapped from a range inside * a source file in the same storage. * * @param destPath Target full path. * @param sourcePath Source full path. * @param rangeStart Starting offset (in bytes) in the source file. * @param rangeEnd Ending offset (in bytes) in the source file. */ public void makeFileFromRange(@NonNull String destPath, @NonNull String sourcePath, long rangeStart, long rangeEnd) throws IOException { try { int res = mService.makeFileFromRange(mId, destPath, sourcePath, rangeStart, rangeEnd); if (res < 0) { throw new IOException("makeFileFromRange() failed, errno " + -res); } } catch (RemoteException e) { e.rethrowFromSystemServer(); } } /** * Creates a hard-link between two paths, which can be under different storages but in the same * Incremental File System. * * @param sourcePath The absolute path of the source. * @param destStorage The target storage of the link target. * @param destPath The absolute path of the target. */ public void makeLink(@NonNull String sourcePath, IncrementalStorage destStorage, @NonNull String destPath) throws IOException { try { int res = mService.makeLink(mId, sourcePath, destStorage.getId(), destPath); if (res < 0) { throw new IOException("makeLink() failed with errno " + -res); } } catch (RemoteException e) { e.rethrowFromSystemServer(); } } /** * Deletes a hard-link under the current storage directory. * * @param path The absolute path of the target. */ public void unlink(@NonNull String path) throws IOException { try { int res = mService.unlink(mId, path); if (res < 0) { throw new IOException("unlink() failed with errno " + -res); } } catch (RemoteException e) { e.rethrowFromSystemServer(); } } /** * Rename an old file name to a new file name under the current storage directory. * * @param sourcepath Old file path as a full path to the storage directory. * @param destpath New file path as a full path to the storage directory. */ public void moveFile(@NonNull String sourcepath, @NonNull String destpath) throws IOException { //TODO(zyy): implement using rename(2) when confirmed that IncFS supports it. try { int res = mService.makeLink(mId, sourcepath, mId, destpath); if (res < 0) { throw new IOException("moveFile() failed at makeLink(), errno " + -res); } } catch (RemoteException e) { e.rethrowFromSystemServer(); } try { mService.unlink(mId, sourcepath); } catch (RemoteException ignored) { } } /** * Move a directory, which is bind-mounted to a given storage, to a new location. The bind mount * will be persistent between reboots. * * @param sourcePath The old path of the directory as an absolute path. * @param destPath The new path of the directory as an absolute path, expected to already * exist. */ public void moveDir(@NonNull String sourcePath, @NonNull String destPath) throws IOException { if (!new File(destPath).exists()) { throw new IOException("moveDir() requires that destination dir already exists."); } try { int res = mService.makeBindMount(mId, sourcePath, destPath, IIncrementalService.BIND_PERMANENT); if (res < 0) { throw new IOException("moveDir() failed at making bind mount, errno " + -res); } } catch (RemoteException e) { e.rethrowFromSystemServer(); } try { mService.deleteBindMount(mId, sourcePath); } catch (RemoteException ignored) { } } /** * Checks whether a file under the current storage directory is fully loaded. * * @param path The relative path of the file. * @return True if the file is fully loaded. */ public boolean isFileFullyLoaded(@NonNull String path) throws IOException { try { int res = mService.isFileFullyLoaded(mId, path); if (res < 0) { throw new IOException("isFileFullyLoaded() failed, errno " + -res); } return res == 0; } catch (RemoteException e) { e.rethrowFromSystemServer(); return false; } } /** * Checks if all files in the storage are fully loaded. */ public boolean isFullyLoaded() throws IOException { try { final int res = mService.isFullyLoaded(mId); if (res < 0) { throw new IOException( "isFullyLoaded() failed at querying loading progress, errno " + -res); } return res == 0; } catch (RemoteException e) { e.rethrowFromSystemServer(); return false; } } /** * Returns the loading progress of a storage * * @return progress value between [0, 1]. */ public float getLoadingProgress() throws IOException { try { final float res = mService.getLoadingProgress(mId); if (res < 0) { throw new IOException( "getLoadingProgress() failed at querying loading progress, errno " + -res); } return res; } catch (RemoteException e) { e.rethrowFromSystemServer(); return 0; } } /** * Returns the metadata object of an IncFs File. * * @param path The relative path of the file. * @return Byte array that contains metadata bytes. */ @Nullable public byte[] getFileMetadata(@NonNull String path) { try { return mService.getMetadataByPath(mId, path); } catch (RemoteException e) { e.rethrowFromSystemServer(); return null; } } /** * Returns the metadata object of an IncFs File. * * @param id The file id. * @return Byte array that contains metadata bytes. */ @Nullable public byte[] getFileMetadata(@NonNull UUID id) { try { final byte[] rawId = idToBytes(id); return mService.getMetadataById(mId, rawId); } catch (RemoteException e) { e.rethrowFromSystemServer(); return null; } } /** * Initializes and starts the DataLoader. * This makes sure all install-time parameters are applied. * Does not affect persistent DataLoader params. * @return True if start request was successfully queued. */ public boolean startLoading( @NonNull DataLoaderParams dataLoaderParams, @Nullable IDataLoaderStatusListener statusListener, @Nullable StorageHealthCheckParams healthCheckParams, @Nullable IStorageHealthListener healthListener, @NonNull PerUidReadTimeouts[] perUidReadTimeouts) { Objects.requireNonNull(perUidReadTimeouts); try { return mService.startLoading(mId, dataLoaderParams.getData(), statusListener, healthCheckParams, healthListener, perUidReadTimeouts); } catch (RemoteException e) { e.rethrowFromSystemServer(); return false; } } /** * Marks the completion of installation. */ public void onInstallationComplete() { try { mService.onInstallationComplete(mId); } catch (RemoteException e) { e.rethrowFromSystemServer(); } } private static final int UUID_BYTE_SIZE = 16; /** * Converts UUID to a byte array usable for Incremental API calls * * @param id The id to convert * @return Byte array that contains the same ID. */ @NonNull public static byte[] idToBytes(@Nullable UUID id) { if (id == null) { return new byte[0]; } final ByteBuffer buf = ByteBuffer.wrap(new byte[UUID_BYTE_SIZE]); buf.putLong(id.getMostSignificantBits()); buf.putLong(id.getLeastSignificantBits()); return buf.array(); } /** * Converts UUID from a byte array usable for Incremental API calls * * @param bytes The id in byte array format, 16 bytes long * @return UUID constructed from the byte array. */ @NonNull public static UUID bytesToId(byte[] bytes) throws IllegalArgumentException { if (bytes.length != UUID_BYTE_SIZE) { throw new IllegalArgumentException("Expected array of size " + UUID_BYTE_SIZE + ", got " + bytes.length); } final ByteBuffer buf = ByteBuffer.wrap(bytes); long msb = buf.getLong(); long lsb = buf.getLong(); return new UUID(msb, lsb); } private static final int INCFS_MAX_HASH_SIZE = 32; // SHA256 private static final int INCFS_MAX_ADD_DATA_SIZE = 128; /** * Permanently disable readlogs collection. */ public void disallowReadLogs() { try { mService.disallowReadLogs(mId); } catch (RemoteException e) { e.rethrowFromSystemServer(); } } /** * Deserialize and validate v4 signature bytes. */ private static void validateV4Signature(@Nullable byte[] v4signatureBytes) throws IOException { if (v4signatureBytes == null || v4signatureBytes.length == 0) { return; } final V4Signature signature; try { signature = V4Signature.readFrom(v4signatureBytes); } catch (IOException e) { throw new IOException("Failed to read v4 signature:", e); } if (!signature.isVersionSupported()) { throw new IOException("v4 signature version " + signature.version + " is not supported"); } final V4Signature.HashingInfo hashingInfo = V4Signature.HashingInfo.fromByteArray( signature.hashingInfo); final V4Signature.SigningInfos signingInfos = V4Signature.SigningInfos.fromByteArray( signature.signingInfos); if (hashingInfo.hashAlgorithm != V4Signature.HASHING_ALGORITHM_SHA256) { throw new IOException("Unsupported hashAlgorithm: " + hashingInfo.hashAlgorithm); } if (hashingInfo.log2BlockSize != V4Signature.LOG2_BLOCK_SIZE_4096_BYTES) { throw new IOException("Unsupported log2BlockSize: " + hashingInfo.log2BlockSize); } if (hashingInfo.salt != null && hashingInfo.salt.length > 0) { throw new IOException("Unsupported salt: " + Arrays.toString(hashingInfo.salt)); } if (hashingInfo.rawRootHash.length != INCFS_MAX_HASH_SIZE) { throw new IOException("rawRootHash has to be " + INCFS_MAX_HASH_SIZE + " bytes"); } if (signingInfos.signingInfo.additionalData.length > INCFS_MAX_ADD_DATA_SIZE) { throw new IOException( "additionalData has to be at most " + INCFS_MAX_ADD_DATA_SIZE + " bytes"); } } /** * Configure all the lib files inside Incremental Service, e.g., create lib dirs, create new lib * files, extract original lib file data from zip and then write data to the lib files on the * Incremental File System. * * @param apkFullPath Source APK to extract native libs from. * @param libDirRelativePath Target dir to put lib files, e.g., "lib" or "lib/arm". * @param abi Target ABI of the native lib files. Only extract native libs of this ABI. * @param extractNativeLibs If true, extract native libraries; otherwise just setup directories * without extracting. * @return Success of not. */ public boolean configureNativeBinaries(String apkFullPath, String libDirRelativePath, String abi, boolean extractNativeLibs) { try { return mService.configureNativeBinaries(mId, apkFullPath, libDirRelativePath, abi, extractNativeLibs); } catch (RemoteException e) { e.rethrowFromSystemServer(); return false; } } /** * Waits for all native binary extraction operations to complete on the storage. * * @return Success of not. */ public boolean waitForNativeBinariesExtraction() { try { return mService.waitForNativeBinariesExtraction(mId); } catch (RemoteException e) { e.rethrowFromSystemServer(); return false; } } /** * Register to listen to loading progress of all the files on this storage. * @param listener To report progress from Incremental Service to the caller. */ public boolean registerLoadingProgressListener(IStorageLoadingProgressListener listener) { try { return mService.registerLoadingProgressListener(mId, listener); } catch (RemoteException e) { e.rethrowFromSystemServer(); return false; } } /** * Unregister to stop listening to storage loading progress. */ public boolean unregisterLoadingProgressListener() { try { return mService.unregisterLoadingProgressListener(mId); } catch (RemoteException e) { e.rethrowFromSystemServer(); return false; } } /** * Returns the metrics of the current storage. * {@see IIncrementalService} for metrics keys. */ public PersistableBundle getMetrics() { try { return mService.getMetrics(mId); } catch (RemoteException e) { e.rethrowFromSystemServer(); return null; } } }* IncrementalManager manager = (IncrementalManager) getSystemService(Context.INCREMENTAL_SERVICE); * IncrementalStorage storage = manager.openStorage("/path/to/incremental/dir"); * storage.makeDirectory("subdir"); *