1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.service.dataloader;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.RequiresPermission;
22 import android.annotation.SystemApi;
23 import android.app.Service;
24 import android.content.Intent;
25 import android.content.pm.DataLoaderParams;
26 import android.content.pm.DataLoaderParamsParcel;
27 import android.content.pm.FileSystemControlParcel;
28 import android.content.pm.IDataLoader;
29 import android.content.pm.IDataLoaderStatusListener;
30 import android.content.pm.InstallationFile;
31 import android.content.pm.InstallationFileParcel;
32 import android.os.IBinder;
33 import android.os.ParcelFileDescriptor;
34 import android.util.ExceptionUtils;
35 import android.util.Slog;
36 
37 import libcore.io.IoUtils;
38 
39 import java.io.IOException;
40 import java.util.Collection;
41 
42 /**
43  * The base class for implementing a data loader service.
44  * <p>
45  * After calling commit() on the install session, the DataLoaderService is started and bound to
46  * provide the actual data bytes for the streaming session.
47  * The service will automatically be rebound until the streaming session has enough data to
48  * proceed with the installation.
49  *
50  * @see android.content.pm.DataLoaderParams
51  * @see android.content.pm.PackageInstaller.SessionParams#setDataLoaderParams
52  *
53  * @hide
54  */
55 @SystemApi
56 public abstract class DataLoaderService extends Service {
57     private static final String TAG = "DataLoaderService";
58     private final DataLoaderBinderService mBinder = new DataLoaderBinderService();
59 
60     /**
61      * DataLoader interface. Each instance corresponds to a single installation session.
62      * @hide
63      */
64     @SystemApi
65     public interface DataLoader {
66         /**
67          * A virtual constructor.
68          *
69          * @param dataLoaderParams parameters set in the installation session
70          * {@link android.content.pm.PackageInstaller.SessionParams#setDataLoaderParams}
71          * @param connector Wrapper providing access to the installation image.
72          * @return true if initialization of a DataLoader was successful. False will notify the
73          * Installer {@link android.content.pm.PackageInstaller#STATUS_PENDING_STREAMING} and
74          * interrupt the session commit. The Installer is supposed to make sure DataLoader can
75          * proceed and then commit the session
76          * {@link android.content.pm.PackageInstaller.Session#commit}.
77          */
onCreate(@onNull DataLoaderParams dataLoaderParams, @NonNull FileSystemConnector connector)78         boolean onCreate(@NonNull DataLoaderParams dataLoaderParams,
79                 @NonNull FileSystemConnector connector);
80 
81         /**
82          * Prepare installation image. After this method succeeds installer will validate the files
83          * and continue installation.
84          * The method should block until the files are prepared for installation.
85          * This can take up to session lifetime (~day). If the session lifetime is exceeded then
86          * any attempts to write new data will fail.
87          *
88          * Example implementation:
89          * <code>
90          *     String localPath = "/data/local/tmp/base.apk";
91          *     session.addFile(LOCATION_DATA_APP, "base", 123456, localPath.getBytes(UTF_8), null);
92          *     ...
93          *     // onPrepareImage
94          *     for (InstallationFile file : addedFiles) {
95          *         String localPath = new String(file.getMetadata(), UTF_8);
96          *         File source = new File(localPath);
97          *         ParcelFileDescriptor fd = ParcelFileDescriptor.open(source, MODE_READ_ONLY);
98          *         try {
99          *             mConnector.writeData(file.getName(), 0, fd.getStatSize(), fd);
100          *         } finally {
101          *             IoUtils.closeQuietly(fd);
102          *         }
103          *     }
104          * </code>
105          * It is recommended to stream data into installation session directly from source, e.g.
106          * cloud data storage, to save local disk space.
107          *
108          * @param addedFiles   list of files created in this installation session
109          * {@link android.content.pm.PackageInstaller.Session#addFile}
110          * @param removedFiles list of files removed in this installation session
111          * {@link android.content.pm.PackageInstaller.Session#removeFile}
112          * @return false if unable to create and populate all addedFiles. Installation will fail.
113          */
onPrepareImage(@onNull Collection<InstallationFile> addedFiles, @NonNull Collection<String> removedFiles)114         boolean onPrepareImage(@NonNull Collection<InstallationFile> addedFiles,
115                 @NonNull Collection<String> removedFiles);
116     }
117 
118     /**
119      * DataLoader factory method.
120      * An installation session uses it to create an instance of DataLoader.
121      * @hide
122      */
123     @SystemApi
onCreateDataLoader(@onNull DataLoaderParams dataLoaderParams)124     public @Nullable DataLoader onCreateDataLoader(@NonNull DataLoaderParams dataLoaderParams) {
125         return null;
126     }
127 
128     /**
129      * @hide
130      */
onBind(@onNull Intent intent)131     public final @NonNull IBinder onBind(@NonNull Intent intent) {
132         return (IBinder) mBinder;
133     }
134 
135     private class DataLoaderBinderService extends IDataLoader.Stub {
136         @Override
create(int id, @NonNull DataLoaderParamsParcel params, @NonNull FileSystemControlParcel control, @NonNull IDataLoaderStatusListener listener)137         public void create(int id, @NonNull DataLoaderParamsParcel params,
138                 @NonNull FileSystemControlParcel control,
139                 @NonNull IDataLoaderStatusListener listener)
140                 throws RuntimeException {
141             try {
142                 nativeCreateDataLoader(id, control, params, listener);
143             } catch (Exception ex) {
144                 Slog.e(TAG, "Failed to create native loader for " + id, ex);
145                 destroy(id);
146                 throw new RuntimeException(ex);
147             } finally {
148                 if (control.incremental != null) {
149                     IoUtils.closeQuietly(control.incremental.cmd);
150                     IoUtils.closeQuietly(control.incremental.pendingReads);
151                     IoUtils.closeQuietly(control.incremental.log);
152                     IoUtils.closeQuietly(control.incremental.blocksWritten);
153                 }
154             }
155         }
156 
157         @Override
start(int id)158         public void start(int id) {
159             if (!nativeStartDataLoader(id)) {
160                 Slog.e(TAG, "Failed to start loader: " + id);
161             }
162         }
163 
164         @Override
stop(int id)165         public void stop(int id) {
166             if (!nativeStopDataLoader(id)) {
167                 Slog.w(TAG, "Failed to stop loader: " + id);
168             }
169         }
170 
171         @Override
destroy(int id)172         public void destroy(int id) {
173             if (!nativeDestroyDataLoader(id)) {
174                 Slog.w(TAG, "Failed to destroy loader: " + id);
175             }
176         }
177 
178         @Override
prepareImage(int id, InstallationFileParcel[] addedFiles, String[] removedFiles)179         public void prepareImage(int id, InstallationFileParcel[] addedFiles,
180                 String[] removedFiles) {
181             if (!nativePrepareImage(id, addedFiles, removedFiles)) {
182                 Slog.w(TAG, "Failed to prepare image for data loader: " + id);
183             }
184         }
185     }
186 
187     /**
188      * Provides access to the installation image.
189      *
190      * @hide
191      */
192     @SystemApi
193     public static final class FileSystemConnector {
194         /**
195          * Create a wrapper for a native instance.
196          *
197          * @hide
198          */
FileSystemConnector(long nativeInstance)199         FileSystemConnector(long nativeInstance) {
200             mNativeInstance = nativeInstance;
201         }
202 
203         /**
204          * Write data to an installation file from an arbitrary FD.
205          *
206          * @param name        name of file previously added to the installation session
207          * {@link InstallationFile#getName()}.
208          * @param offsetBytes offset into the file to begin writing at, or 0 to start at the
209          *                    beginning of the file.
210          * @param lengthBytes total size of the file being written, used to preallocate the
211          *                    underlying disk space, or -1 if unknown. The system may clear various
212          *                    caches as needed to allocate this space.
213          * @param incomingFd  FD to read bytes from.
214          * @throws IOException if trouble opening the file for writing, such as lack of disk space
215          *                     or unavailable media.
216          */
217         @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES)
writeData(@onNull String name, long offsetBytes, long lengthBytes, @NonNull ParcelFileDescriptor incomingFd)218         public void writeData(@NonNull String name, long offsetBytes, long lengthBytes,
219                 @NonNull ParcelFileDescriptor incomingFd) throws IOException {
220             try {
221                 nativeWriteData(mNativeInstance, name, offsetBytes, lengthBytes, incomingFd);
222             } catch (RuntimeException e) {
223                 ExceptionUtils.maybeUnwrapIOException(e);
224                 throw e;
225             }
226         }
227 
228         private final long mNativeInstance;
229     }
230 
231     /* Native methods */
nativeCreateDataLoader(int storageId, @NonNull FileSystemControlParcel control, @NonNull DataLoaderParamsParcel params, IDataLoaderStatusListener listener)232     private native boolean nativeCreateDataLoader(int storageId,
233             @NonNull FileSystemControlParcel control,
234             @NonNull DataLoaderParamsParcel params,
235             IDataLoaderStatusListener listener);
236 
nativeStartDataLoader(int storageId)237     private native boolean nativeStartDataLoader(int storageId);
238 
nativeStopDataLoader(int storageId)239     private native boolean nativeStopDataLoader(int storageId);
240 
nativeDestroyDataLoader(int storageId)241     private native boolean nativeDestroyDataLoader(int storageId);
242 
nativePrepareImage(int storageId, InstallationFileParcel[] addedFiles, String[] removedFiles)243     private native boolean nativePrepareImage(int storageId,
244             InstallationFileParcel[] addedFiles, String[] removedFiles);
245 
nativeWriteData(long nativeInstance, String name, long offsetBytes, long lengthBytes, ParcelFileDescriptor incomingFd)246     private static native void nativeWriteData(long nativeInstance, String name, long offsetBytes,
247             long lengthBytes, ParcelFileDescriptor incomingFd);
248 
249 }
250