blob: c4a06c8f53db7f6f010948b9c8852dd1f20226c0 [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.service.incremental;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Service;
import android.content.Intent;
import android.content.pm.IDataLoader;
import android.content.pm.IDataLoaderStatusListener;
import android.content.pm.InstallationFile;
import android.os.Bundle;
import android.os.IBinder;
import android.os.incremental.IncrementalDataLoaderParams;
import android.os.incremental.IncrementalDataLoaderParamsParcel;
import android.os.incremental.IncrementalFileSystemControlParcel;
import android.os.incremental.NamedParcelFileDescriptor;
import android.util.Slog;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Collection;
import java.util.List;
/**
* The base class for implementing data loader service to control data loaders. Expecting
* Incremental Service to bind to a children class of this.
*
* @hide
*
* Hide for now, should be @SystemApi
* TODO(b/136132412): update with latest API design
*/
public abstract class IncrementalDataLoaderService extends Service {
private static final String TAG = "IncrementalDataLoaderService";
private final DataLoaderBinderService mBinder = new DataLoaderBinderService();
public static final int DATA_LOADER_READY =
IDataLoaderStatusListener.DATA_LOADER_READY;
public static final int DATA_LOADER_NOT_READY =
IDataLoaderStatusListener.DATA_LOADER_NOT_READY;
public static final int DATA_LOADER_RUNNING =
IDataLoaderStatusListener.DATA_LOADER_RUNNING;
public static final int DATA_LOADER_STOPPED =
IDataLoaderStatusListener.DATA_LOADER_STOPPED;
public static final int DATA_LOADER_SLOW_CONNECTION =
IDataLoaderStatusListener.DATA_LOADER_SLOW_CONNECTION;
public static final int DATA_LOADER_NO_CONNECTION =
IDataLoaderStatusListener.DATA_LOADER_NO_CONNECTION;
public static final int DATA_LOADER_CONNECTION_OK =
IDataLoaderStatusListener.DATA_LOADER_CONNECTION_OK;
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = {"DATA_LOADER_"}, value = {
DATA_LOADER_READY,
DATA_LOADER_NOT_READY,
DATA_LOADER_RUNNING,
DATA_LOADER_STOPPED,
DATA_LOADER_SLOW_CONNECTION,
DATA_LOADER_NO_CONNECTION,
DATA_LOADER_CONNECTION_OK
})
public @interface DataLoaderStatus {
}
/**
* Incremental FileSystem block size.
**/
public static final int BLOCK_SIZE = 4096;
/**
* Data compression types
*/
public static final int COMPRESSION_NONE = 0;
public static final int COMPRESSION_LZ4 = 1;
/**
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({COMPRESSION_NONE, COMPRESSION_LZ4})
public @interface CompressionType {
}
/**
* Managed DataLoader interface. Each instance corresponds to a single Incremental File System
* instance.
*/
public abstract static class DataLoader {
/**
* A virtual constructor used to do simple initialization. Not ready to serve any data yet.
* All heavy-lifting has to be done in onStart.
*
* @param params Data loader configuration parameters.
* @param connector IncFS API wrapper.
* @param listener Used for reporting internal state to IncrementalService.
* @return True if initialization of a Data Loader was successful. False will be reported to
* IncrementalService and can cause an unmount of an IFS instance.
*/
public abstract boolean onCreate(@NonNull IncrementalDataLoaderParams params,
@NonNull FileSystemConnector connector,
@NonNull StatusListener listener);
/**
* Start the data loader. After this method returns data loader is considered to be ready to
* receive callbacks from IFS, supply data via connector and send status updates via
* callbacks.
*
* @return True if Data Loader was able to start. False will be reported to
* IncrementalService and can cause an unmount of an IFS instance.
*/
public abstract boolean onStart();
/**
* Stop the data loader. Use to stop any additional threads and free up resources. Data
* loader is not longer responsible for supplying data. Start/Stop pair can be called
* multiple times e.g. if IFS detects corruption and data needs to be re-loaded.
*/
public abstract void onStop();
/**
* Virtual destructor. Use to cleanup all internal state. After this method returns, the
* data loader can no longer use connector or callbacks. For any additional operations with
* this instance of IFS a new DataLoader will be created using createDataLoader method.
*/
public abstract void onDestroy();
/**
* IFS reports a pending read each time the page needs to be loaded, e.g. missing.
*
* @param pendingReads array of blocks to load.
*
* TODO(b/136132412): avoid using collections
*/
public abstract void onPendingReads(
@NonNull Collection<FileSystemConnector.PendingReadInfo> pendingReads);
/**
* IFS tracks all reads and reports them using onPageReads.
*
* @param reads array of blocks.
*
* TODO(b/136132412): avoid using collections
*/
public abstract void onPageReads(@NonNull Collection<FileSystemConnector.ReadInfo> reads);
/**
* IFS informs data loader that a new file has been created.
* <p>
* This can be used to prepare the data loader before it starts loading data. For example,
* the data loader can keep a list of newly created files, so that it knows what files to
* download from the server.
*
* @param inode The inode value of the new file.
* @param metadata The metadata of the new file.
*/
public abstract void onFileCreated(long inode, byte[] metadata);
}
/**
* DataLoader factory method.
*
* @return An instance of a DataLoader.
*/
public abstract @Nullable DataLoader onCreateDataLoader();
/**
* @hide
*/
public final @NonNull IBinder onBind(@NonNull Intent intent) {
return (IBinder) mBinder;
}
private class DataLoaderBinderService extends IDataLoader.Stub {
private int mId;
@Override
public void create(int id, @NonNull Bundle options,
@NonNull IDataLoaderStatusListener listener)
throws IllegalArgumentException, RuntimeException {
mId = id;
final IncrementalDataLoaderParamsParcel params = options.getParcelable("params");
if (params == null) {
throw new IllegalArgumentException("Must specify Incremental data loader params");
}
final IncrementalFileSystemControlParcel control =
options.getParcelable("control");
if (control == null) {
throw new IllegalArgumentException("Must specify Incremental control parcel");
}
mStatusListener = listener;
try {
if (!nativeCreateDataLoader(id, control, params, listener)) {
Slog.e(TAG, "Failed to create native loader for " + mId);
}
} catch (Exception ex) {
destroy();
throw new RuntimeException(ex);
} finally {
// Closing FDs.
if (control.cmd != null) {
try {
control.cmd.close();
} catch (IOException e) {
Slog.e(TAG, "Failed to close IncFs CMD file descriptor " + e);
}
}
if (control.log != null) {
try {
control.log.close();
} catch (IOException e) {
Slog.e(TAG, "Failed to close IncFs LOG file descriptor " + e);
}
}
NamedParcelFileDescriptor[] fds = params.dynamicArgs;
for (NamedParcelFileDescriptor nfd : fds) {
try {
nfd.fd.close();
} catch (IOException e) {
Slog.e(TAG,
"Failed to close DynamicArgs parcel file descriptor " + e);
}
}
}
}
@Override
public void start(List<InstallationFile> fileInfos) {
if (!nativeStartDataLoader(mId)) {
Slog.e(TAG, "Failed to start loader: loader not found for " + mId);
}
}
@Override
public void stop() {
if (!nativeStopDataLoader(mId)) {
Slog.w(TAG, "Failed to stop loader: loader not found for " + mId);
}
}
@Override
public void destroy() {
if (!nativeDestroyDataLoader(mId)) {
Slog.w(TAG, "Failed to destroy loader: loader not found for " + mId);
}
}
@Override
// TODO(b/136132412): remove this
public void onFileCreated(long inode, byte[] metadata) {
if (!nativeOnFileCreated(mId, inode, metadata)) {
Slog.w(TAG, "Failed to handle onFileCreated for storage:" + mId
+ " inode:" + inode);
}
}
}
/**
* IncFs API wrapper for writing pages and getting page missing info. Non-hidden methods are
* expected to be called by the IncrementalDataLoaderService implemented by developers.
*
* @hide
*
* TODO(b/136132412) Should be @SystemApi
*/
public static final class FileSystemConnector {
/**
* Defines a block address. A block is the unit of data chunk that IncFs operates with.
*
* @hide
*/
public static class BlockAddress {
/**
* Linux inode uniquely identifies file within a single IFS instance.
*/
private final long mFileIno;
/**
* Index of a 4K block within a file.
*/
private final int mBlockIndex;
public BlockAddress(long fileIno, int blockIndex) {
this.mFileIno = fileIno;
this.mBlockIndex = blockIndex;
}
public long getFileIno() {
return mFileIno;
}
public int getBlockIndex() {
return mBlockIndex;
}
}
/**
* A block is the unit of data chunk that IncFs operates with.
*
* @hide
*/
public static class Block extends BlockAddress {
/**
* Data content of the block.
*/
private final @NonNull byte[] mDataBytes;
public Block(long fileIno, int blockIndex, @NonNull byte[] dataBytes) {
super(fileIno, blockIndex);
this.mDataBytes = dataBytes;
}
}
/**
* Defines a page/block inside a file.
*/
public static class DataBlock extends Block {
/**
* Compression type of the data block.
*/
private final @CompressionType int mCompressionType;
public DataBlock(long fileIno, int blockIndex, @NonNull byte[] dataBytes,
@CompressionType int compressionType) {
super(fileIno, blockIndex, dataBytes);
this.mCompressionType = compressionType;
}
}
/**
* Defines a hash block for a certain file. A hash block index is the index in an array of
* hashes which is the 1-d representation of the hash tree. One DataBlock might be
* associated with multiple HashBlocks.
*/
public static class HashBlock extends Block {
public HashBlock(long fileIno, int blockIndex, @NonNull byte[] dataBytes) {
super(fileIno, blockIndex, dataBytes);
}
}
/**
* Information about a page that is pending to be read.
*/
public static class PendingReadInfo extends BlockAddress {
PendingReadInfo(long fileIno, int blockIndex) {
super(fileIno, blockIndex);
}
}
/**
* Information about a page that is read.
*/
public static class ReadInfo extends BlockAddress {
/**
* A monotonically increasing read timestamp.
*/
private final long mTimePoint;
/**
* Number of blocks read starting from blockIndex.
*/
private final int mBlockCount;
ReadInfo(long timePoint, long fileIno, int firstBlockIndex, int blockCount) {
super(fileIno, firstBlockIndex);
this.mTimePoint = timePoint;
this.mBlockCount = blockCount;
}
public long getTimePoint() {
return mTimePoint;
}
public int getBlockCount() {
return mBlockCount;
}
}
/**
* Defines the dynamic information about an IncFs file.
*/
public static class FileInfo {
/**
* BitSet to show if any block is available at each block index.
*/
private final @NonNull
byte[] mBlockBitmap;
/**
* @hide
*/
public FileInfo(@NonNull byte[] blockBitmap) {
this.mBlockBitmap = blockBitmap;
}
}
/**
* Creates a wrapper for a native instance.
*/
FileSystemConnector(long nativeInstance) {
mNativeInstance = nativeInstance;
}
/**
* Checks whether a range in a file if loaded.
*
* @param node inode of the file.
* @param start The starting offset of the range.
* @param end The ending offset of the range.
* @return True if the file is fully loaded.
*/
public boolean isFileRangeLoaded(long node, long start, long end) {
return nativeIsFileRangeLoadedNode(mNativeInstance, node, start, end);
}
/**
* Gets the metadata of a file.
*
* @param node inode of the file.
* @return The metadata object.
*/
@NonNull
public byte[] getFileMetadata(long node) throws IOException {
final byte[] metadata = nativeGetFileMetadataNode(mNativeInstance, node);
if (metadata == null || metadata.length == 0) {
throw new IOException(
"IncrementalFileSystem failed to obtain metadata for node: " + node);
}
return metadata;
}
/**
* Gets the dynamic information of a file, such as page bitmaps. Can be used to get missing
* page indices by the FileSystemConnector.
*
* @param node inode of the file.
* @return Dynamic file info.
*/
@NonNull
public FileInfo getDynamicFileInfo(long node) throws IOException {
final byte[] blockBitmap = nativeGetFileInfoNode(mNativeInstance, node);
if (blockBitmap == null || blockBitmap.length == 0) {
throw new IOException(
"IncrementalFileSystem failed to obtain dynamic file info for node: "
+ node);
}
return new FileInfo(blockBitmap);
}
/**
* Writes a page's data and/or hashes.
*
* @param dataBlocks the DataBlock objects that contain data block index and data bytes.
* @param hashBlocks the HashBlock objects that contain hash indices and hash bytes.
*
* TODO(b/136132412): change API to avoid dynamic allocation of data block objects
*/
public void writeMissingData(@NonNull DataBlock[] dataBlocks,
@Nullable HashBlock[] hashBlocks) throws IOException {
if (!nativeWriteMissingData(mNativeInstance, dataBlocks, hashBlocks)) {
throw new IOException("IncrementalFileSystem failed to write missing data.");
}
}
/**
* Writes the signer block of a file. Expecting the connector to call this when it got
* signing data from data loader.
*
* @param node the file to be written to.
* @param signerData the raw signer data byte array.
*/
public void writeSignerData(long node, @NonNull byte[] signerData)
throws IOException {
if (!nativeWriteSignerDataNode(mNativeInstance, node, signerData)) {
throw new IOException(
"IncrementalFileSystem failed to write signer data of node " + node);
}
}
private final long mNativeInstance;
}
/**
* Wrapper for native reporting DataLoader statuses.
*
* @hide
*
* TODO(b/136132412) Should be @SystemApi
*/
public static final class StatusListener {
/**
* Creates a wrapper for a native instance.
*
* @hide
*/
StatusListener(long nativeInstance) {
mNativeInstance = nativeInstance;
}
/**
* Report the status of DataLoader. Used for system-wide notifications e.g., disabling
* applications which rely on this data loader to function properly.
*
* @param status status to report.
* @return True if status was reported successfully.
*/
public boolean onStatusChanged(@DataLoaderStatus int status) {
return nativeReportStatus(mNativeInstance, status);
}
private final long mNativeInstance;
}
private IDataLoaderStatusListener mStatusListener = null;
/* Native methods */
private native boolean nativeCreateDataLoader(int storageId,
@NonNull IncrementalFileSystemControlParcel control,
@NonNull IncrementalDataLoaderParamsParcel params,
IDataLoaderStatusListener listener);
private native boolean nativeStartDataLoader(int storageId);
private native boolean nativeStopDataLoader(int storageId);
private native boolean nativeDestroyDataLoader(int storageId);
private static native boolean nativeOnFileCreated(int storageId,
long inode, byte[] metadata);
private static native boolean nativeIsFileRangeLoadedNode(
long nativeInstance, long node, long start, long end);
private static native boolean nativeWriteMissingData(
long nativeInstance, FileSystemConnector.DataBlock[] dataBlocks,
FileSystemConnector.HashBlock[] hashBlocks);
private static native boolean nativeWriteSignerDataNode(
long nativeInstance, long node, byte[] signerData);
private static native byte[] nativeGetFileMetadataNode(
long nativeInstance, long node);
private static native byte[] nativeGetFileInfoNode(
long nativeInstance, long node);
private static native boolean nativeReportStatus(long nativeInstance, int status);
}