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