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.os.incremental;
18 
19 import android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.SystemService;
23 import android.content.Context;
24 import android.content.pm.DataLoaderParams;
25 import android.content.pm.IPackageLoadingProgressCallback;
26 import android.os.RemoteCallbackList;
27 import android.os.RemoteException;
28 import android.system.ErrnoException;
29 import android.system.Os;
30 import android.system.StructStat;
31 import android.util.Slog;
32 import android.util.SparseArray;
33 
34 import com.android.internal.annotations.GuardedBy;
35 
36 import java.io.File;
37 import java.io.FileDescriptor;
38 import java.io.IOException;
39 import java.lang.annotation.Retention;
40 import java.lang.annotation.RetentionPolicy;
41 import java.nio.file.FileVisitResult;
42 import java.nio.file.Files;
43 import java.nio.file.Path;
44 import java.nio.file.Paths;
45 import java.nio.file.SimpleFileVisitor;
46 import java.nio.file.attribute.BasicFileAttributes;
47 import java.util.Objects;
48 
49 /**
50  * Provides operations to open or create an IncrementalStorage, using IIncrementalService
51  * service. Example Usage:
52  *
53  * <blockquote><pre>
54  * IncrementalManager manager = (IncrementalManager) getSystemService(Context.INCREMENTAL_SERVICE);
55  * IncrementalStorage storage = manager.openStorage("/path/to/incremental/dir");
56  * </pre></blockquote>
57  *
58  * @hide
59  */
60 @SystemService(Context.INCREMENTAL_SERVICE)
61 public final class IncrementalManager {
62     private static final String TAG = "IncrementalManager";
63 
64     private static final String ALLOWED_PROPERTY = "incremental.allowed";
65 
66     public static final int MIN_VERSION_TO_SUPPORT_FSVERITY = 2;
67 
68     public static final int CREATE_MODE_TEMPORARY_BIND =
69             IIncrementalService.CREATE_MODE_TEMPORARY_BIND;
70     public static final int CREATE_MODE_PERMANENT_BIND =
71             IIncrementalService.CREATE_MODE_PERMANENT_BIND;
72     public static final int CREATE_MODE_CREATE =
73             IIncrementalService.CREATE_MODE_CREATE;
74     public static final int CREATE_MODE_OPEN_EXISTING =
75             IIncrementalService.CREATE_MODE_OPEN_EXISTING;
76 
77     @Retention(RetentionPolicy.SOURCE)
78     @IntDef(prefix = {"CREATE_MODE_"}, value = {
79             CREATE_MODE_TEMPORARY_BIND,
80             CREATE_MODE_PERMANENT_BIND,
81             CREATE_MODE_CREATE,
82             CREATE_MODE_OPEN_EXISTING,
83     })
84     public @interface CreateMode {
85     }
86 
87     private final @Nullable IIncrementalService mService;
88 
89     private final LoadingProgressCallbacks mLoadingProgressCallbacks =
90             new LoadingProgressCallbacks();
91 
IncrementalManager(IIncrementalService service)92     public IncrementalManager(IIncrementalService service) {
93         mService = service;
94     }
95 
96     /**
97      * Opens or create an Incremental File System mounted directory and returns an
98      * IncrementalStorage object.
99      *
100      * @param path                Absolute path to mount Incremental File System on.
101      * @param params              IncrementalDataLoaderParams object to configure data loading.
102      * @param createMode          Mode for opening an old Incremental File System mount or creating
103      *                            a new mount.
104      * @return IncrementalStorage object corresponding to the mounted directory.
105      */
106     @Nullable
createStorage(@onNull String path, @NonNull DataLoaderParams params, @CreateMode int createMode)107     public IncrementalStorage createStorage(@NonNull String path,
108             @NonNull DataLoaderParams params,
109             @CreateMode int createMode) {
110         Objects.requireNonNull(path);
111         Objects.requireNonNull(params);
112         try {
113             final int id = mService.createStorage(path, params.getData(), createMode);
114             if (id < 0) {
115                 return null;
116             }
117             return new IncrementalStorage(mService, id);
118         } catch (RemoteException e) {
119             throw e.rethrowFromSystemServer();
120         }
121     }
122 
123     /**
124      * Opens an existing Incremental File System mounted directory and returns an IncrementalStorage
125      * object.
126      *
127      * @param path Absolute target path that Incremental File System has been mounted on.
128      * @return IncrementalStorage object corresponding to the mounted directory.
129      */
130     @Nullable
openStorage(@onNull String path)131     public IncrementalStorage openStorage(@NonNull String path) {
132         try {
133             final int id = mService.openStorage(path);
134             if (id < 0) {
135                 return null;
136             }
137             final IncrementalStorage storage = new IncrementalStorage(mService, id);
138             return storage;
139         } catch (RemoteException e) {
140             throw e.rethrowFromSystemServer();
141         }
142     }
143 
144     /**
145      * Opens or creates an IncrementalStorage that is linked to another IncrementalStorage.
146      *
147      * @return IncrementalStorage object corresponding to the linked storage.
148      */
149     @Nullable
createStorage(@onNull String path, @NonNull IncrementalStorage linkedStorage, @CreateMode int createMode)150     public IncrementalStorage createStorage(@NonNull String path,
151             @NonNull IncrementalStorage linkedStorage, @CreateMode int createMode) {
152         int id = -1;
153         try {
154             // Incremental service mounts its newly created storage on top of the supplied path,
155             // ensure that the original mode remains the same after mounting.
156             StructStat st = Os.stat(path);
157             id = mService.createLinkedStorage(
158                     path, linkedStorage.getId(), createMode);
159             if (id < 0) {
160                 return null;
161             }
162             Os.chmod(path, st.st_mode & 07777);
163             return new IncrementalStorage(mService, id);
164         } catch (RemoteException e) {
165             throw e.rethrowFromSystemServer();
166         } catch (ErrnoException e) {
167             if (id >= 0) {
168                 try {
169                     mService.deleteStorage(id);
170                 } catch (RemoteException re) {
171                     throw re.rethrowFromSystemServer();
172                 }
173             }
174             throw new RuntimeException(e);
175         }
176     }
177 
178     /**
179      * Link an app's files from the stage dir to the final installation location.
180      * The expected outcome of this method is:
181      * 1) The actual apk directory under /data/incremental is bind-mounted to the parent directory
182      * of {@code afterCodeFile}.
183      * 2) All the files under {@code beforeCodeFile} will show up under {@code afterCodeFile}.
184      *
185      * @param beforeCodeFile Path that is currently bind-mounted and have APKs under it.
186      *                       Example: /data/app/vmdl*tmp
187      * @param afterCodeFile Path that should will have APKs after this method is called. Its parent
188      *                      directory should be bind-mounted to a directory under /data/incremental.
189      *                      Example: /data/app/~~[randomStringA]/[packageName]-[randomStringB]
190      * @throws IllegalArgumentException
191      * @throws IOException
192      */
linkCodePath(File beforeCodeFile, File afterCodeFile)193     public void linkCodePath(File beforeCodeFile, File afterCodeFile)
194             throws IllegalArgumentException, IOException {
195         final File beforeCodeAbsolute = beforeCodeFile.getAbsoluteFile();
196         final IncrementalStorage apkStorage = openStorage(beforeCodeAbsolute.toString());
197         if (apkStorage == null) {
198             throw new IllegalArgumentException("Not an Incremental path: " + beforeCodeAbsolute);
199         }
200         final String targetStorageDir = afterCodeFile.getAbsoluteFile().getParent();
201         final IncrementalStorage linkedApkStorage =
202                 createStorage(targetStorageDir, apkStorage,
203                         IncrementalManager.CREATE_MODE_CREATE
204                                 | IncrementalManager.CREATE_MODE_PERMANENT_BIND);
205         if (linkedApkStorage == null) {
206             throw new IOException("Failed to create linked storage at dir: " + targetStorageDir);
207         }
208         try {
209             final String afterCodePathName = afterCodeFile.getName();
210             linkFiles(apkStorage, beforeCodeAbsolute, "", linkedApkStorage, afterCodePathName);
211         } catch (Exception e) {
212             linkedApkStorage.unBind(targetStorageDir);
213             throw e;
214         }
215     }
216 
217     /**
218      * Recursively set up directories and link all the files from source storage to target storage.
219      *
220      * @param sourceStorage The storage that has all the files and directories underneath.
221      * @param sourceAbsolutePath The absolute path of the directory that holds all files and dirs.
222      * @param sourceRelativePath The relative path on the source directory, e.g., "" or "lib".
223      * @param targetStorage The target storage that will have the same files and directories.
224      * @param targetRelativePath The relative path to the directory on the target storage that
225      *                           should have all the files and dirs underneath,
226      *                           e.g., "packageName-random".
227      * @throws IOException When makeDirectory or makeLink fails on the Incremental File System.
228      */
linkFiles(IncrementalStorage sourceStorage, File sourceAbsolutePath, String sourceRelativePath, IncrementalStorage targetStorage, String targetRelativePath)229     private void linkFiles(IncrementalStorage sourceStorage, File sourceAbsolutePath,
230             String sourceRelativePath, IncrementalStorage targetStorage,
231             String targetRelativePath) throws IOException {
232         final Path sourceBase = sourceAbsolutePath.toPath().resolve(sourceRelativePath);
233         final Path targetRelative = Paths.get(targetRelativePath);
234         Files.walkFileTree(sourceAbsolutePath.toPath(), new SimpleFileVisitor<Path>() {
235             @Override
236             public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
237                     throws IOException {
238                 final Path relativeDir = sourceBase.relativize(dir);
239                 targetStorage.makeDirectory(targetRelative.resolve(relativeDir).toString());
240                 return FileVisitResult.CONTINUE;
241             }
242 
243             @Override
244             public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
245                     throws IOException {
246                 final Path relativeFile = sourceBase.relativize(file);
247                 sourceStorage.makeLink(
248                         file.toAbsolutePath().toString(), targetStorage,
249                         targetRelative.resolve(relativeFile).toString());
250                 return FileVisitResult.CONTINUE;
251             }
252         });
253     }
254 
255     /**
256      * Checks if Incremental feature is enabled on this device.
257      */
isFeatureEnabled()258     public static boolean isFeatureEnabled() {
259         return nativeIsEnabled();
260     }
261 
262     /**
263      * 0 - IncFs is disabled.
264      * 1 - IncFs v1, core features, no PerUid support. Optional in R.
265      * 2 - IncFs v2, PerUid support, fs-verity support. Required in S.
266      */
getVersion()267     public static int getVersion() {
268         return nativeIsEnabled() ? nativeIsV2Available() ? 2 : 1 : 0;
269     }
270 
271     /**
272      * Checks if Incremental installations are allowed.
273      * A developer can disable Incremental installations by setting the property.
274      */
isAllowed()275     public static boolean isAllowed() {
276         return isFeatureEnabled() && android.os.SystemProperties.getBoolean(ALLOWED_PROPERTY, true);
277     }
278 
279     /**
280      * Checks if path is mounted on Incremental File System.
281      */
isIncrementalPath(@onNull String path)282     public static boolean isIncrementalPath(@NonNull String path) {
283         return nativeIsIncrementalPath(path);
284     }
285 
286     /**
287      * Checks if an fd corresponds to a file on a mounted Incremental File System.
288      */
isIncrementalFileFd(@onNull FileDescriptor fd)289     public static boolean isIncrementalFileFd(@NonNull FileDescriptor fd) {
290         return nativeIsIncrementalFd(fd.getInt$());
291     }
292 
293     /**
294      * Returns raw signature for file if it's on Incremental File System.
295      * Unsafe, use only if you are sure what you are doing.
296      */
unsafeGetFileSignature(@onNull String path)297     public static @Nullable byte[] unsafeGetFileSignature(@NonNull String path) {
298         return nativeUnsafeGetFileSignature(path);
299     }
300 
301     /**
302      * Closes a storage specified by the absolute path. If the path is not Incremental, do nothing.
303      * Unbinds the target dir and deletes the corresponding storage instance.
304      * Deletes the package name and associated storage id from maps.
305      */
rmPackageDir(@onNull File codeFile)306     public void rmPackageDir(@NonNull File codeFile) {
307         try {
308             final String codePath = codeFile.getAbsolutePath();
309             final IncrementalStorage storage = openStorage(codePath);
310             if (storage == null) {
311                 return;
312             }
313             mLoadingProgressCallbacks.cleanUpCallbacks(storage);
314             storage.unBind(codePath);
315         } catch (IOException e) {
316             Slog.w(TAG, "Failed to remove code path", e);
317         }
318     }
319 
320     /**
321      * Called when a new callback wants to listen to the loading progress of an installed package.
322      * Increment the count of callbacks associated to the corresponding storage.
323      * Only register storage listener if there hasn't been any existing callback on the storage yet.
324      * @param codePath Path of the installed package. This path is on an Incremental Storage.
325      * @param callback To report loading progress to.
326      * @return True if the package name and associated storage id are valid. False otherwise.
327      */
registerLoadingProgressCallback(@onNull String codePath, @NonNull IPackageLoadingProgressCallback callback)328     public boolean registerLoadingProgressCallback(@NonNull String codePath,
329             @NonNull IPackageLoadingProgressCallback callback) {
330         final IncrementalStorage storage = openStorage(codePath);
331         if (storage == null) {
332             // storage does not exist, package not installed
333             return false;
334         }
335         return mLoadingProgressCallbacks.registerCallback(storage, callback);
336     }
337 
338     /**
339      * Called to stop all listeners from listening to loading progress of an installed package.
340      * @param codePath Path of the installed package
341      */
unregisterLoadingProgressCallbacks(@onNull String codePath)342     public void unregisterLoadingProgressCallbacks(@NonNull String codePath) {
343         final IncrementalStorage storage = openStorage(codePath);
344         if (storage == null) {
345             // storage does not exist, package not installed
346             return;
347         }
348         mLoadingProgressCallbacks.cleanUpCallbacks(storage);
349     }
350 
351     private static class LoadingProgressCallbacks extends IStorageLoadingProgressListener.Stub {
352         @GuardedBy("mCallbacks")
353         private final SparseArray<RemoteCallbackList<IPackageLoadingProgressCallback>> mCallbacks =
354                 new SparseArray<>();
355 
cleanUpCallbacks(@onNull IncrementalStorage storage)356         public void cleanUpCallbacks(@NonNull IncrementalStorage storage) {
357             final int storageId = storage.getId();
358             final RemoteCallbackList<IPackageLoadingProgressCallback> callbacksForStorage;
359             synchronized (mCallbacks) {
360                 callbacksForStorage = mCallbacks.removeReturnOld(storageId);
361             }
362             if (callbacksForStorage == null) {
363                 return;
364             }
365             // Unregister all existing callbacks on this storage
366             callbacksForStorage.kill();
367             storage.unregisterLoadingProgressListener();
368         }
369 
registerCallback(@onNull IncrementalStorage storage, @NonNull IPackageLoadingProgressCallback callback)370         public boolean registerCallback(@NonNull IncrementalStorage storage,
371                 @NonNull IPackageLoadingProgressCallback callback) {
372             final int storageId = storage.getId();
373             synchronized (mCallbacks) {
374                 RemoteCallbackList<IPackageLoadingProgressCallback> callbacksForStorage =
375                         mCallbacks.get(storageId);
376                 if (callbacksForStorage == null) {
377                     callbacksForStorage = new RemoteCallbackList<>();
378                     mCallbacks.put(storageId, callbacksForStorage);
379                 }
380                 // Registration in RemoteCallbackList needs to be done first, such that when events
381                 // come from Incremental Service, the callback is already registered
382                 callbacksForStorage.register(callback);
383                 if (callbacksForStorage.getRegisteredCallbackCount() > 1) {
384                     // already listening for progress for this storage
385                     return true;
386                 }
387             }
388             return storage.registerLoadingProgressListener(this);
389         }
390 
391         @Override
onStorageLoadingProgressChanged(int storageId, float progress)392         public void onStorageLoadingProgressChanged(int storageId, float progress) {
393             final RemoteCallbackList<IPackageLoadingProgressCallback> callbacksForStorage;
394             synchronized (mCallbacks) {
395                 callbacksForStorage = mCallbacks.get(storageId);
396             }
397             if (callbacksForStorage == null) {
398                 // no callback has ever been registered on this storage
399                 return;
400             }
401             final int n = callbacksForStorage.beginBroadcast();
402             // RemoteCallbackList use ArrayMap internally and it's safe to iterate this way
403             for (int i = 0; i < n; i++) {
404                 final IPackageLoadingProgressCallback callback =
405                         callbacksForStorage.getBroadcastItem(i);
406                 try {
407                     callback.onPackageLoadingProgressChanged(progress);
408                 } catch (RemoteException ignored) {
409                 }
410             }
411             callbacksForStorage.finishBroadcast();
412         }
413     }
414 
415     /**
416      * Returns the metrics of an Incremental Storage.
417      */
getMetrics(@onNull String codePath)418     public IncrementalMetrics getMetrics(@NonNull String codePath) {
419         final IncrementalStorage storage = openStorage(codePath);
420         if (storage == null) {
421             // storage does not exist, package not installed
422             return null;
423         }
424         return new IncrementalMetrics(storage.getMetrics());
425     }
426 
427     /* Native methods */
nativeIsEnabled()428     private static native boolean nativeIsEnabled();
nativeIsV2Available()429     private static native boolean nativeIsV2Available();
nativeIsIncrementalPath(@onNull String path)430     private static native boolean nativeIsIncrementalPath(@NonNull String path);
nativeIsIncrementalFd(@onNull int fd)431     private static native boolean nativeIsIncrementalFd(@NonNull int fd);
nativeUnsafeGetFileSignature(@onNull String path)432     private static native byte[] nativeUnsafeGetFileSignature(@NonNull String path);
433 }
434