Migrating Incremental* APIs to PackageManager APIs.
This is the first step, migrating java parts. CleanSpec.mk added as a workaround for b/146502407 Test: builds and flashes Bug: b/136132412 Change-Id: Id0a26aa011b555ea457b5aafe7f5789c36d25bcc
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.os.incremental;
|
||||
package android.content.pm;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
@@ -29,12 +29,12 @@ import java.util.stream.Collectors;
|
||||
* Hide for now.
|
||||
* @hide
|
||||
*/
|
||||
public class IncrementalDataLoaderParams {
|
||||
@NonNull private final IncrementalDataLoaderParamsParcel mData;
|
||||
public class DataLoaderParams {
|
||||
@NonNull private final DataLoaderParamsParcel mData;
|
||||
|
||||
public IncrementalDataLoaderParams(@NonNull String url, @NonNull String packageName,
|
||||
public DataLoaderParams(@NonNull String url, @NonNull String packageName,
|
||||
@Nullable Map<String, ParcelFileDescriptor> namedFds) {
|
||||
IncrementalDataLoaderParamsParcel data = new IncrementalDataLoaderParamsParcel();
|
||||
DataLoaderParamsParcel data = new DataLoaderParamsParcel();
|
||||
data.staticArgs = url;
|
||||
data.packageName = packageName;
|
||||
if (namedFds == null || namedFds.isEmpty()) {
|
||||
@@ -52,7 +52,7 @@ public class IncrementalDataLoaderParams {
|
||||
mData = data;
|
||||
}
|
||||
|
||||
public IncrementalDataLoaderParams(@NonNull IncrementalDataLoaderParamsParcel data) {
|
||||
public DataLoaderParams(@NonNull DataLoaderParamsParcel data) {
|
||||
mData = data;
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ public class IncrementalDataLoaderParams {
|
||||
return mData.packageName;
|
||||
}
|
||||
|
||||
public final @NonNull IncrementalDataLoaderParamsParcel getData() {
|
||||
public final @NonNull DataLoaderParamsParcel getData() {
|
||||
return mData;
|
||||
}
|
||||
|
||||
@@ -14,15 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.os.incremental;
|
||||
package android.content.pm;
|
||||
|
||||
import android.os.incremental.NamedParcelFileDescriptor;
|
||||
import android.content.pm.NamedParcelFileDescriptor;
|
||||
|
||||
/**
|
||||
* Class for holding data loader configuration parameters.
|
||||
* @hide
|
||||
*/
|
||||
parcelable IncrementalDataLoaderParamsParcel {
|
||||
parcelable DataLoaderParamsParcel {
|
||||
@utf8InCpp String packageName;
|
||||
@utf8InCpp String staticArgs;
|
||||
NamedParcelFileDescriptor[] dynamicArgs;
|
||||
31
core/java/android/content/pm/FileSystemControlParcel.aidl
Normal file
31
core/java/android/content/pm/FileSystemControlParcel.aidl
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.content.pm;
|
||||
|
||||
import android.content.pm.IPackageInstallerSessionFileSystemConnector;
|
||||
import android.os.incremental.IncrementalFileSystemControlParcel;
|
||||
|
||||
/**
|
||||
* Wraps info needed for DataLoader to provide data.
|
||||
* @hide
|
||||
*/
|
||||
parcelable FileSystemControlParcel {
|
||||
// Incremental FS control descriptors.
|
||||
@nullable IncrementalFileSystemControlParcel incremental;
|
||||
// Callback-based installation connector.
|
||||
@nullable IPackageInstallerSessionFileSystemConnector callback;
|
||||
}
|
||||
@@ -30,5 +30,4 @@ oneway interface IDataLoader {
|
||||
void start(in List<InstallationFile> fileInfos);
|
||||
void stop();
|
||||
void destroy();
|
||||
void onFileCreated(long inode, in byte[] metadata);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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.content.pm;
|
||||
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
/** {@hide} */
|
||||
interface IPackageInstallerSessionFileSystemConnector {
|
||||
void writeData(String name, long offsetBytes, long lengthBytes, in ParcelFileDescriptor fd);
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.os.incremental;
|
||||
package android.content.pm;
|
||||
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
@@ -50,8 +50,6 @@ import android.os.ParcelableException;
|
||||
import android.os.RemoteException;
|
||||
import android.os.SystemProperties;
|
||||
import android.os.UserHandle;
|
||||
import android.os.incremental.IncrementalDataLoaderParams;
|
||||
import android.os.incremental.IncrementalDataLoaderParamsParcel;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.util.ArraySet;
|
||||
@@ -1459,7 +1457,7 @@ public class PackageInstaller {
|
||||
/** {@hide} */
|
||||
public long requiredInstalledVersionCode = PackageManager.VERSION_CODE_HIGHEST;
|
||||
/** {@hide} */
|
||||
public IncrementalDataLoaderParams incrementalParams;
|
||||
public DataLoaderParams incrementalParams;
|
||||
/** TODO(b/146080380): add a class name to make it fully compatible with ComponentName.
|
||||
* {@hide} */
|
||||
public String dataLoaderPackageName;
|
||||
@@ -1496,10 +1494,10 @@ public class PackageInstaller {
|
||||
isMultiPackage = source.readBoolean();
|
||||
isStaged = source.readBoolean();
|
||||
requiredInstalledVersionCode = source.readLong();
|
||||
IncrementalDataLoaderParamsParcel dataLoaderParamsParcel = source.readParcelable(
|
||||
IncrementalDataLoaderParamsParcel.class.getClassLoader());
|
||||
DataLoaderParamsParcel dataLoaderParamsParcel = source.readParcelable(
|
||||
DataLoaderParamsParcel.class.getClassLoader());
|
||||
if (dataLoaderParamsParcel != null) {
|
||||
incrementalParams = new IncrementalDataLoaderParams(
|
||||
incrementalParams = new DataLoaderParams(
|
||||
dataLoaderParamsParcel);
|
||||
}
|
||||
dataLoaderPackageName = source.readString();
|
||||
@@ -1863,7 +1861,7 @@ public class PackageInstaller {
|
||||
* {@hide}
|
||||
*/
|
||||
@RequiresPermission(Manifest.permission.INSTALL_PACKAGES)
|
||||
public void setIncrementalParams(@NonNull IncrementalDataLoaderParams incrementalParams) {
|
||||
public void setIncrementalParams(@NonNull DataLoaderParams incrementalParams) {
|
||||
this.incrementalParams = incrementalParams;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
package android.os.incremental;
|
||||
|
||||
import android.os.incremental.IncrementalFileSystemControlParcel;
|
||||
import android.os.incremental.IncrementalDataLoaderParamsParcel;
|
||||
import android.content.pm.FileSystemControlParcel;
|
||||
import android.content.pm.DataLoaderParamsParcel;
|
||||
import android.content.pm.IDataLoaderStatusListener;
|
||||
|
||||
/**
|
||||
@@ -27,8 +27,8 @@ import android.content.pm.IDataLoaderStatusListener;
|
||||
*/
|
||||
interface IIncrementalManager {
|
||||
boolean prepareDataLoader(int mountId,
|
||||
in IncrementalFileSystemControlParcel control,
|
||||
in IncrementalDataLoaderParamsParcel params,
|
||||
in FileSystemControlParcel control,
|
||||
in DataLoaderParamsParcel params,
|
||||
in IDataLoaderStatusListener listener);
|
||||
boolean startDataLoader(int mountId);
|
||||
void showHealthBlockedUI(int mountId);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
package android.os.incremental;
|
||||
|
||||
import android.os.incremental.IncrementalDataLoaderParamsParcel;
|
||||
import android.content.pm.DataLoaderParamsParcel;
|
||||
|
||||
/** @hide */
|
||||
interface IIncrementalManagerNative {
|
||||
@@ -32,7 +32,7 @@ interface IIncrementalManagerNative {
|
||||
* Opens or creates a storage given a target path and data loader params. Returns the storage ID.
|
||||
*/
|
||||
int openStorage(in @utf8InCpp String path);
|
||||
int createStorage(in @utf8InCpp String path, in IncrementalDataLoaderParamsParcel params, int createMode);
|
||||
int createStorage(in @utf8InCpp String path, in DataLoaderParamsParcel params, int createMode);
|
||||
int createLinkedStorage(in @utf8InCpp String path, int otherStorageId, int createMode);
|
||||
|
||||
/**
|
||||
|
||||
@@ -35,6 +35,7 @@ import static dalvik.system.VMRuntime.getInstructionSet;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.content.pm.DataLoaderParams;
|
||||
import android.content.pm.InstallationFile;
|
||||
import android.os.IVold;
|
||||
import android.os.RemoteException;
|
||||
@@ -82,12 +83,12 @@ public final class IncrementalFileStorages {
|
||||
public IncrementalFileStorages(@NonNull String packageName,
|
||||
@NonNull File stageDir,
|
||||
@NonNull IncrementalManager incrementalManager,
|
||||
@NonNull IncrementalDataLoaderParams incrementalDataLoaderParams) {
|
||||
@NonNull DataLoaderParams dataLoaderParams) {
|
||||
mPackageName = packageName;
|
||||
mStageDir = stageDir;
|
||||
mIncrementalManager = incrementalManager;
|
||||
if (incrementalDataLoaderParams.getPackageName().equals("local")) {
|
||||
final String incrementalPath = incrementalDataLoaderParams.getStaticArgs();
|
||||
if (dataLoaderParams.getPackageName().equals("local")) {
|
||||
final String incrementalPath = dataLoaderParams.getStaticArgs();
|
||||
mDefaultStorage = mIncrementalManager.openStorage(incrementalPath);
|
||||
mDefaultDir = incrementalPath;
|
||||
return;
|
||||
@@ -97,7 +98,7 @@ public final class IncrementalFileStorages {
|
||||
return;
|
||||
}
|
||||
mDefaultStorage = mIncrementalManager.createStorage(mDefaultDir,
|
||||
incrementalDataLoaderParams,
|
||||
dataLoaderParams,
|
||||
IncrementalManager.CREATE_MODE_CREATE
|
||||
| IncrementalManager.CREATE_MODE_TEMPORARY_BIND, false);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.annotation.SystemService;
|
||||
import android.content.Context;
|
||||
import android.content.pm.DataLoaderParams;
|
||||
import android.os.RemoteException;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
@@ -104,7 +105,7 @@ public final class IncrementalManager {
|
||||
*/
|
||||
@Nullable
|
||||
public IncrementalStorage createStorage(@NonNull String path,
|
||||
@NonNull IncrementalDataLoaderParams params, @CreateMode int createMode,
|
||||
@NonNull DataLoaderParams params, @CreateMode int createMode,
|
||||
boolean autoStartDataLoader) {
|
||||
try {
|
||||
final int id = mNativeService.createStorage(path, params.getData(), createMode);
|
||||
|
||||
307
core/java/android/service/dataloader/DataLoaderService.java
Normal file
307
core/java/android/service/dataloader/DataLoaderService.java
Normal file
@@ -0,0 +1,307 @@
|
||||
/*
|
||||
* 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.dataloader;
|
||||
|
||||
import android.annotation.IntDef;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.DataLoaderParams;
|
||||
import android.content.pm.DataLoaderParamsParcel;
|
||||
import android.content.pm.FileSystemControlParcel;
|
||||
import android.content.pm.IDataLoader;
|
||||
import android.content.pm.IDataLoaderStatusListener;
|
||||
import android.content.pm.IPackageInstallerSessionFileSystemConnector;
|
||||
import android.content.pm.InstallationFile;
|
||||
import android.content.pm.NamedParcelFileDescriptor;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.RemoteException;
|
||||
import android.util.ExceptionUtils;
|
||||
import android.util.Slog;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
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 DataLoaderService 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 {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 DataLoaderParams 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 DataLoaderParamsParcel params = options.getParcelable("params");
|
||||
if (params == null) {
|
||||
throw new IllegalArgumentException("Must specify Incremental data loader params");
|
||||
}
|
||||
final FileSystemControlParcel 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.incremental.cmd != null) {
|
||||
try {
|
||||
control.incremental.cmd.close();
|
||||
} catch (IOException e) {
|
||||
Slog.e(TAG, "Failed to close IncFs CMD file descriptor " + e);
|
||||
}
|
||||
}
|
||||
if (control.incremental.log != null) {
|
||||
try {
|
||||
control.incremental.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Used by the DataLoaderService implementations.
|
||||
*
|
||||
* @hide
|
||||
*
|
||||
* TODO(b/136132412) Should be @SystemApi
|
||||
*/
|
||||
public static final class FileSystemConnector {
|
||||
/**
|
||||
* Creates a wrapper for an installation session connector.
|
||||
* @hide
|
||||
*/
|
||||
FileSystemConnector(IPackageInstallerSessionFileSystemConnector connector) {
|
||||
mConnector = connector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write data to an installation file from an arbitrary FD.
|
||||
*
|
||||
* @param name name of file previously added to the installation session.
|
||||
* @param offsetBytes offset into the file to begin writing at, or 0 to
|
||||
* start at the beginning of the file.
|
||||
* @param lengthBytes total size of the file being written, used to
|
||||
* preallocate the underlying disk space, or -1 if unknown.
|
||||
* The system may clear various caches as needed to allocate
|
||||
* this space.
|
||||
* @param incomingFd FD to read bytes from.
|
||||
* @throws IOException if trouble opening the file for writing, such as
|
||||
* lack of disk space or unavailable media.
|
||||
*/
|
||||
public void writeData(String name, long offsetBytes, long lengthBytes,
|
||||
ParcelFileDescriptor incomingFd) throws IOException {
|
||||
try {
|
||||
mConnector.writeData(name, offsetBytes, lengthBytes, incomingFd);
|
||||
} catch (RuntimeException e) {
|
||||
ExceptionUtils.maybeUnwrapIOException(e);
|
||||
throw e;
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
|
||||
private final IPackageInstallerSessionFileSystemConnector mConnector;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 FileSystemControlParcel control,
|
||||
@NonNull DataLoaderParamsParcel 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 nativeReportStatus(long nativeInstance, int status);
|
||||
}
|
||||
@@ -1,563 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
Reference in New Issue
Block a user