/* * 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.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; 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.InstallationFile; import android.content.pm.InstallationFileParcel; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.util.ExceptionUtils; import android.util.Slog; import libcore.io.IoUtils; import java.io.IOException; import java.util.Collection; /** * The base class for implementing a data loader service. *

* After calling commit() on the install session, the DataLoaderService is started and bound to * provide the actual data bytes for the streaming session. * The service will automatically be rebound until the streaming session has enough data to * proceed with the installation. * * @see android.content.pm.DataLoaderParams * @see android.content.pm.PackageInstaller.SessionParams#setDataLoaderParams * * @hide */ @SystemApi public abstract class DataLoaderService extends Service { private static final String TAG = "DataLoaderService"; private final DataLoaderBinderService mBinder = new DataLoaderBinderService(); /** * DataLoader interface. Each instance corresponds to a single installation session. * @hide */ @SystemApi public interface DataLoader { /** * A virtual constructor. * * @param dataLoaderParams parameters set in the installation session * {@link android.content.pm.PackageInstaller.SessionParams#setDataLoaderParams} * @param connector Wrapper providing access to the installation image. * @return true if initialization of a DataLoader was successful. False will notify the * Installer {@link android.content.pm.PackageInstaller#STATUS_PENDING_STREAMING} and * interrupt the session commit. The Installer is supposed to make sure DataLoader can * proceed and then commit the session * {@link android.content.pm.PackageInstaller.Session#commit}. */ boolean onCreate(@NonNull DataLoaderParams dataLoaderParams, @NonNull FileSystemConnector connector); /** * Prepare installation image. After this method succeeds installer will validate the files * and continue installation. * The method should block until the files are prepared for installation. * This can take up to session lifetime (~day). If the session lifetime is exceeded then * any attempts to write new data will fail. * * Example implementation: * * String localPath = "/data/local/tmp/base.apk"; * session.addFile(LOCATION_DATA_APP, "base", 123456, localPath.getBytes(UTF_8), null); * ... * // onPrepareImage * for (InstallationFile file : addedFiles) { * String localPath = new String(file.getMetadata(), UTF_8); * File source = new File(localPath); * ParcelFileDescriptor fd = ParcelFileDescriptor.open(source, MODE_READ_ONLY); * try { * mConnector.writeData(file.getName(), 0, fd.getStatSize(), fd); * } finally { * IoUtils.closeQuietly(fd); * } * } * * It is recommended to stream data into installation session directly from source, e.g. * cloud data storage, to save local disk space. * * @param addedFiles list of files created in this installation session * {@link android.content.pm.PackageInstaller.Session#addFile} * @param removedFiles list of files removed in this installation session * {@link android.content.pm.PackageInstaller.Session#removeFile} * @return false if unable to create and populate all addedFiles. Installation will fail. */ boolean onPrepareImage(@NonNull Collection addedFiles, @NonNull Collection removedFiles); } /** * DataLoader factory method. * An installation session uses it to create an instance of DataLoader. * @hide */ @SystemApi public @Nullable DataLoader onCreateDataLoader(@NonNull DataLoaderParams dataLoaderParams) { return null; } /** * @hide */ public final @NonNull IBinder onBind(@NonNull Intent intent) { return (IBinder) mBinder; } private class DataLoaderBinderService extends IDataLoader.Stub { @Override public void create(int id, @NonNull DataLoaderParamsParcel params, @NonNull FileSystemControlParcel control, @NonNull IDataLoaderStatusListener listener) throws RuntimeException { try { nativeCreateDataLoader(id, control, params, listener); } catch (Exception ex) { Slog.e(TAG, "Failed to create native loader for " + id, ex); destroy(id); throw new RuntimeException(ex); } finally { if (control.incremental != null) { IoUtils.closeQuietly(control.incremental.cmd); IoUtils.closeQuietly(control.incremental.pendingReads); IoUtils.closeQuietly(control.incremental.log); IoUtils.closeQuietly(control.incremental.blocksWritten); } } } @Override public void start(int id) { if (!nativeStartDataLoader(id)) { Slog.e(TAG, "Failed to start loader: " + id); } } @Override public void stop(int id) { if (!nativeStopDataLoader(id)) { Slog.w(TAG, "Failed to stop loader: " + id); } } @Override public void destroy(int id) { if (!nativeDestroyDataLoader(id)) { Slog.w(TAG, "Failed to destroy loader: " + id); } } @Override public void prepareImage(int id, InstallationFileParcel[] addedFiles, String[] removedFiles) { if (!nativePrepareImage(id, addedFiles, removedFiles)) { Slog.w(TAG, "Failed to prepare image for data loader: " + id); } } } /** * Provides access to the installation image. * * @hide */ @SystemApi public static final class FileSystemConnector { /** * Create a wrapper for a native instance. * * @hide */ FileSystemConnector(long nativeInstance) { mNativeInstance = nativeInstance; } /** * Write data to an installation file from an arbitrary FD. * * @param name name of file previously added to the installation session * {@link InstallationFile#getName()}. * @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. */ @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES) public void writeData(@NonNull String name, long offsetBytes, long lengthBytes, @NonNull ParcelFileDescriptor incomingFd) throws IOException { try { nativeWriteData(mNativeInstance, name, offsetBytes, lengthBytes, incomingFd); } catch (RuntimeException e) { ExceptionUtils.maybeUnwrapIOException(e); throw e; } } private final long mNativeInstance; } /* 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 native boolean nativePrepareImage(int storageId, InstallationFileParcel[] addedFiles, String[] removedFiles); private static native void nativeWriteData(long nativeInstance, String name, long offsetBytes, long lengthBytes, ParcelFileDescriptor incomingFd); }