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.NonNull; 20 import android.annotation.Nullable; 21 import android.content.pm.DataLoaderParams; 22 import android.content.pm.IDataLoaderStatusListener; 23 import android.os.PersistableBundle; 24 import android.os.RemoteException; 25 26 import java.io.File; 27 import java.io.IOException; 28 import java.nio.ByteBuffer; 29 import java.util.Objects; 30 import java.util.UUID; 31 32 /** 33 * Provides operations on an Incremental File System directory, using IncrementalServiceNative. 34 * Example usage: 35 * 36 * <blockquote><pre> 37 * IncrementalManager manager = (IncrementalManager) getSystemService(Context.INCREMENTAL_SERVICE); 38 * IncrementalStorage storage = manager.openStorage("/path/to/incremental/dir"); 39 * storage.makeDirectory("subdir"); 40 * </pre></blockquote> 41 * 42 * @hide 43 */ 44 public final class IncrementalStorage { 45 private static final String TAG = "IncrementalStorage"; 46 private final int mId; 47 private final IIncrementalService mService; 48 49 IncrementalStorage(@onNull IIncrementalService is, int id)50 public IncrementalStorage(@NonNull IIncrementalService is, int id) { 51 mService = is; 52 mId = id; 53 } 54 getId()55 public int getId() { 56 return mId; 57 } 58 59 /** 60 * Temporarily bind-mounts the current storage directory to a target directory. The bind-mount 61 * will NOT be preserved between device reboots. 62 * 63 * @param targetPath Absolute path to the target directory. 64 */ bind(@onNull String targetPath)65 public void bind(@NonNull String targetPath) throws IOException { 66 bind("", targetPath); 67 } 68 69 /** 70 * Temporarily bind-mounts a subdir under the current storage directory to a target directory. 71 * The bind-mount will NOT be preserved between device reboots. 72 * 73 * @param sourcePath Source path as a relative path under current storage 74 * directory. 75 * @param targetPath Absolute path to the target directory. 76 */ bind(@onNull String sourcePath, @NonNull String targetPath)77 public void bind(@NonNull String sourcePath, @NonNull String targetPath) 78 throws IOException { 79 try { 80 int res = mService.makeBindMount(mId, sourcePath, targetPath, 81 IIncrementalService.BIND_TEMPORARY); 82 if (res < 0) { 83 throw new IOException("bind() failed with errno " + -res); 84 } 85 } catch (RemoteException e) { 86 e.rethrowFromSystemServer(); 87 } 88 } 89 90 91 /** 92 * Permanently bind-mounts the current storage directory to a target directory. The bind-mount 93 * WILL be preserved between device reboots. 94 * 95 * @param targetPath Absolute path to the target directory. 96 */ bindPermanent(@onNull String targetPath)97 public void bindPermanent(@NonNull String targetPath) throws IOException { 98 bindPermanent("", targetPath); 99 } 100 101 /** 102 * Permanently bind-mounts a subdir under the current storage directory to a target directory. 103 * The bind-mount WILL be preserved between device reboots. 104 * 105 * @param sourcePath Relative path under the current storage directory. 106 * @param targetPath Absolute path to the target directory. 107 */ bindPermanent(@onNull String sourcePath, @NonNull String targetPath)108 public void bindPermanent(@NonNull String sourcePath, @NonNull String targetPath) 109 throws IOException { 110 try { 111 int res = mService.makeBindMount(mId, sourcePath, targetPath, 112 IIncrementalService.BIND_PERMANENT); 113 if (res < 0) { 114 throw new IOException("bind() permanent failed with errno " + -res); 115 } 116 } catch (RemoteException e) { 117 e.rethrowFromSystemServer(); 118 } 119 } 120 121 /** 122 * Unbinds a bind mount. 123 * 124 * @param targetPath Absolute path to the target directory. 125 */ unBind(@onNull String targetPath)126 public void unBind(@NonNull String targetPath) throws IOException { 127 try { 128 int res = mService.deleteBindMount(mId, targetPath); 129 if (res < 0) { 130 throw new IOException("unbind() failed with errno " + -res); 131 } 132 } catch (RemoteException e) { 133 e.rethrowFromSystemServer(); 134 } 135 } 136 137 /** 138 * Creates a sub-directory under the current storage directory. 139 * 140 * @param path Relative path of the sub-directory, e.g., "subdir" 141 */ makeDirectory(@onNull String path)142 public void makeDirectory(@NonNull String path) throws IOException { 143 try { 144 int res = mService.makeDirectory(mId, path); 145 if (res < 0) { 146 throw new IOException("makeDirectory() failed with errno " + -res); 147 } 148 } catch (RemoteException e) { 149 e.rethrowFromSystemServer(); 150 } 151 } 152 153 /** 154 * Creates a sub-directory under the current storage directory. If its parent dirs do not exist, 155 * create the parent dirs as well. 156 * 157 * @param path Full path. 158 */ makeDirectories(@onNull String path)159 public void makeDirectories(@NonNull String path) throws IOException { 160 try { 161 int res = mService.makeDirectories(mId, path); 162 if (res < 0) { 163 throw new IOException("makeDirectory() failed with errno " + -res); 164 } 165 } catch (RemoteException e) { 166 e.rethrowFromSystemServer(); 167 } 168 } 169 170 /** 171 * Creates a file under the current storage directory. 172 * 173 * @param path Relative path of the new file. 174 * @param size Size of the new file in bytes. 175 * @param metadata Metadata bytes. 176 * @param v4signatureBytes Serialized V4SignatureProto. 177 * @param content Optionally set file content. 178 */ makeFile(@onNull String path, long size, @Nullable UUID id, @Nullable byte[] metadata, @Nullable byte[] v4signatureBytes, @Nullable byte[] content)179 public void makeFile(@NonNull String path, long size, @Nullable UUID id, 180 @Nullable byte[] metadata, @Nullable byte[] v4signatureBytes, @Nullable byte[] content) 181 throws IOException { 182 try { 183 if (id == null && metadata == null) { 184 throw new IOException("File ID and metadata cannot both be null"); 185 } 186 validateV4Signature(v4signatureBytes); 187 final IncrementalNewFileParams params = new IncrementalNewFileParams(); 188 params.size = size; 189 params.metadata = (metadata == null ? new byte[0] : metadata); 190 params.fileId = idToBytes(id); 191 params.signature = v4signatureBytes; 192 int res = mService.makeFile(mId, path, params, content); 193 if (res != 0) { 194 throw new IOException("makeFile() failed with errno " + -res); 195 } 196 } catch (RemoteException e) { 197 e.rethrowFromSystemServer(); 198 } 199 } 200 201 202 /** 203 * Creates a file in Incremental storage. The content of the file is mapped from a range inside 204 * a source file in the same storage. 205 * 206 * @param destPath Target full path. 207 * @param sourcePath Source full path. 208 * @param rangeStart Starting offset (in bytes) in the source file. 209 * @param rangeEnd Ending offset (in bytes) in the source file. 210 */ makeFileFromRange(@onNull String destPath, @NonNull String sourcePath, long rangeStart, long rangeEnd)211 public void makeFileFromRange(@NonNull String destPath, 212 @NonNull String sourcePath, long rangeStart, long rangeEnd) throws IOException { 213 try { 214 int res = mService.makeFileFromRange(mId, destPath, sourcePath, 215 rangeStart, rangeEnd); 216 if (res < 0) { 217 throw new IOException("makeFileFromRange() failed, errno " + -res); 218 } 219 } catch (RemoteException e) { 220 e.rethrowFromSystemServer(); 221 } 222 } 223 224 /** 225 * Creates a hard-link between two paths, which can be under different storages but in the same 226 * Incremental File System. 227 * 228 * @param sourcePath The absolute path of the source. 229 * @param destStorage The target storage of the link target. 230 * @param destPath The absolute path of the target. 231 */ makeLink(@onNull String sourcePath, IncrementalStorage destStorage, @NonNull String destPath)232 public void makeLink(@NonNull String sourcePath, IncrementalStorage destStorage, 233 @NonNull String destPath) throws IOException { 234 try { 235 int res = mService.makeLink(mId, sourcePath, destStorage.getId(), 236 destPath); 237 if (res < 0) { 238 throw new IOException("makeLink() failed with errno " + -res); 239 } 240 } catch (RemoteException e) { 241 e.rethrowFromSystemServer(); 242 } 243 } 244 245 /** 246 * Deletes a hard-link under the current storage directory. 247 * 248 * @param path The absolute path of the target. 249 */ unlink(@onNull String path)250 public void unlink(@NonNull String path) throws IOException { 251 try { 252 int res = mService.unlink(mId, path); 253 if (res < 0) { 254 throw new IOException("unlink() failed with errno " + -res); 255 } 256 } catch (RemoteException e) { 257 e.rethrowFromSystemServer(); 258 } 259 } 260 261 /** 262 * Rename an old file name to a new file name under the current storage directory. 263 * 264 * @param sourcepath Old file path as a full path to the storage directory. 265 * @param destpath New file path as a full path to the storage directory. 266 */ moveFile(@onNull String sourcepath, @NonNull String destpath)267 public void moveFile(@NonNull String sourcepath, 268 @NonNull String destpath) throws IOException { 269 //TODO(zyy): implement using rename(2) when confirmed that IncFS supports it. 270 try { 271 int res = mService.makeLink(mId, sourcepath, mId, destpath); 272 if (res < 0) { 273 throw new IOException("moveFile() failed at makeLink(), errno " + -res); 274 } 275 } catch (RemoteException e) { 276 e.rethrowFromSystemServer(); 277 } 278 try { 279 mService.unlink(mId, sourcepath); 280 } catch (RemoteException ignored) { 281 } 282 } 283 284 /** 285 * Move a directory, which is bind-mounted to a given storage, to a new location. The bind mount 286 * will be persistent between reboots. 287 * 288 * @param sourcePath The old path of the directory as an absolute path. 289 * @param destPath The new path of the directory as an absolute path, expected to already 290 * exist. 291 */ moveDir(@onNull String sourcePath, @NonNull String destPath)292 public void moveDir(@NonNull String sourcePath, @NonNull String destPath) throws IOException { 293 if (!new File(destPath).exists()) { 294 throw new IOException("moveDir() requires that destination dir already exists."); 295 } 296 try { 297 int res = mService.makeBindMount(mId, sourcePath, destPath, 298 IIncrementalService.BIND_PERMANENT); 299 if (res < 0) { 300 throw new IOException("moveDir() failed at making bind mount, errno " + -res); 301 } 302 } catch (RemoteException e) { 303 e.rethrowFromSystemServer(); 304 } 305 try { 306 mService.deleteBindMount(mId, sourcePath); 307 } catch (RemoteException ignored) { 308 } 309 } 310 311 /** 312 * Checks whether a file under the current storage directory is fully loaded. 313 * 314 * @param path The relative path of the file. 315 * @return True if the file is fully loaded. 316 */ isFileFullyLoaded(@onNull String path)317 public boolean isFileFullyLoaded(@NonNull String path) throws IOException { 318 try { 319 int res = mService.isFileFullyLoaded(mId, path); 320 if (res < 0) { 321 throw new IOException("isFileFullyLoaded() failed, errno " + -res); 322 } 323 return res == 0; 324 } catch (RemoteException e) { 325 e.rethrowFromSystemServer(); 326 return false; 327 } 328 } 329 330 331 /** 332 * Checks if all files in the storage are fully loaded. 333 */ isFullyLoaded()334 public boolean isFullyLoaded() throws IOException { 335 try { 336 final int res = mService.isFullyLoaded(mId); 337 if (res < 0) { 338 throw new IOException( 339 "isFullyLoaded() failed at querying loading progress, errno " + -res); 340 } 341 return res == 0; 342 } catch (RemoteException e) { 343 e.rethrowFromSystemServer(); 344 return false; 345 } 346 } 347 348 /** 349 * Returns the loading progress of a storage 350 * 351 * @return progress value between [0, 1]. 352 */ getLoadingProgress()353 public float getLoadingProgress() throws IOException { 354 try { 355 final float res = mService.getLoadingProgress(mId); 356 if (res < 0) { 357 throw new IOException( 358 "getLoadingProgress() failed at querying loading progress, errno " + -res); 359 } 360 return res; 361 } catch (RemoteException e) { 362 e.rethrowFromSystemServer(); 363 return 0; 364 } 365 } 366 367 /** 368 * Returns the metadata object of an IncFs File. 369 * 370 * @param path The relative path of the file. 371 * @return Byte array that contains metadata bytes. 372 */ 373 @Nullable getFileMetadata(@onNull String path)374 public byte[] getFileMetadata(@NonNull String path) { 375 try { 376 return mService.getMetadataByPath(mId, path); 377 } catch (RemoteException e) { 378 e.rethrowFromSystemServer(); 379 return null; 380 } 381 } 382 383 /** 384 * Returns the metadata object of an IncFs File. 385 * 386 * @param id The file id. 387 * @return Byte array that contains metadata bytes. 388 */ 389 @Nullable getFileMetadata(@onNull UUID id)390 public byte[] getFileMetadata(@NonNull UUID id) { 391 try { 392 final byte[] rawId = idToBytes(id); 393 return mService.getMetadataById(mId, rawId); 394 } catch (RemoteException e) { 395 e.rethrowFromSystemServer(); 396 return null; 397 } 398 } 399 400 /** 401 * Initializes and starts the DataLoader. 402 * This makes sure all install-time parameters are applied. 403 * Does not affect persistent DataLoader params. 404 * @return True if start request was successfully queued. 405 */ startLoading( @onNull DataLoaderParams dataLoaderParams, @Nullable IDataLoaderStatusListener statusListener, @Nullable StorageHealthCheckParams healthCheckParams, @Nullable IStorageHealthListener healthListener, @NonNull PerUidReadTimeouts[] perUidReadTimeouts)406 public boolean startLoading( 407 @NonNull DataLoaderParams dataLoaderParams, 408 @Nullable IDataLoaderStatusListener statusListener, 409 @Nullable StorageHealthCheckParams healthCheckParams, 410 @Nullable IStorageHealthListener healthListener, 411 @NonNull PerUidReadTimeouts[] perUidReadTimeouts) { 412 Objects.requireNonNull(perUidReadTimeouts); 413 try { 414 return mService.startLoading(mId, dataLoaderParams.getData(), statusListener, 415 healthCheckParams, healthListener, perUidReadTimeouts); 416 } catch (RemoteException e) { 417 e.rethrowFromSystemServer(); 418 return false; 419 } 420 } 421 422 /** 423 * Marks the completion of installation. 424 */ onInstallationComplete()425 public void onInstallationComplete() { 426 try { 427 mService.onInstallationComplete(mId); 428 } catch (RemoteException e) { 429 e.rethrowFromSystemServer(); 430 } 431 } 432 433 434 private static final int UUID_BYTE_SIZE = 16; 435 436 /** 437 * Converts UUID to a byte array usable for Incremental API calls 438 * 439 * @param id The id to convert 440 * @return Byte array that contains the same ID. 441 */ 442 @NonNull idToBytes(@ullable UUID id)443 public static byte[] idToBytes(@Nullable UUID id) { 444 if (id == null) { 445 return new byte[0]; 446 } 447 final ByteBuffer buf = ByteBuffer.wrap(new byte[UUID_BYTE_SIZE]); 448 buf.putLong(id.getMostSignificantBits()); 449 buf.putLong(id.getLeastSignificantBits()); 450 return buf.array(); 451 } 452 453 /** 454 * Converts UUID from a byte array usable for Incremental API calls 455 * 456 * @param bytes The id in byte array format, 16 bytes long 457 * @return UUID constructed from the byte array. 458 */ 459 @NonNull bytesToId(byte[] bytes)460 public static UUID bytesToId(byte[] bytes) throws IllegalArgumentException { 461 if (bytes.length != UUID_BYTE_SIZE) { 462 throw new IllegalArgumentException("Expected array of size " + UUID_BYTE_SIZE 463 + ", got " + bytes.length); 464 } 465 final ByteBuffer buf = ByteBuffer.wrap(bytes); 466 long msb = buf.getLong(); 467 long lsb = buf.getLong(); 468 return new UUID(msb, lsb); 469 } 470 471 private static final int INCFS_MAX_HASH_SIZE = 32; // SHA256 472 private static final int INCFS_MAX_ADD_DATA_SIZE = 128; 473 474 /** 475 * Permanently disable readlogs collection. 476 */ disallowReadLogs()477 public void disallowReadLogs() { 478 try { 479 mService.disallowReadLogs(mId); 480 } catch (RemoteException e) { 481 e.rethrowFromSystemServer(); 482 } 483 } 484 485 /** 486 * Deserialize and validate v4 signature bytes. 487 */ validateV4Signature(@ullable byte[] v4signatureBytes)488 private static void validateV4Signature(@Nullable byte[] v4signatureBytes) 489 throws IOException { 490 if (v4signatureBytes == null || v4signatureBytes.length == 0) { 491 return; 492 } 493 494 final V4Signature signature; 495 try { 496 signature = V4Signature.readFrom(v4signatureBytes); 497 } catch (IOException e) { 498 throw new IOException("Failed to read v4 signature:", e); 499 } 500 501 if (!signature.isVersionSupported()) { 502 throw new IOException("v4 signature version " + signature.version 503 + " is not supported"); 504 } 505 506 final V4Signature.HashingInfo hashingInfo = V4Signature.HashingInfo.fromByteArray( 507 signature.hashingInfo); 508 final V4Signature.SigningInfo signingInfo = V4Signature.SigningInfo.fromByteArray( 509 signature.signingInfo); 510 511 if (hashingInfo.hashAlgorithm != V4Signature.HASHING_ALGORITHM_SHA256) { 512 throw new IOException("Unsupported hashAlgorithm: " + hashingInfo.hashAlgorithm); 513 } 514 if (hashingInfo.log2BlockSize != V4Signature.LOG2_BLOCK_SIZE_4096_BYTES) { 515 throw new IOException("Unsupported log2BlockSize: " + hashingInfo.log2BlockSize); 516 } 517 if (hashingInfo.salt != null && hashingInfo.salt.length > 0) { 518 throw new IOException("Unsupported salt: " + hashingInfo.salt); 519 } 520 if (hashingInfo.rawRootHash.length != INCFS_MAX_HASH_SIZE) { 521 throw new IOException("rawRootHash has to be " + INCFS_MAX_HASH_SIZE + " bytes"); 522 } 523 if (signingInfo.additionalData.length > INCFS_MAX_ADD_DATA_SIZE) { 524 throw new IOException( 525 "additionalData has to be at most " + INCFS_MAX_ADD_DATA_SIZE + " bytes"); 526 } 527 } 528 529 /** 530 * Configure all the lib files inside Incremental Service, e.g., create lib dirs, create new lib 531 * files, extract original lib file data from zip and then write data to the lib files on the 532 * Incremental File System. 533 * 534 * @param apkFullPath Source APK to extract native libs from. 535 * @param libDirRelativePath Target dir to put lib files, e.g., "lib" or "lib/arm". 536 * @param abi Target ABI of the native lib files. Only extract native libs of this ABI. 537 * @param extractNativeLibs If true, extract native libraries; otherwise just setup directories 538 * without extracting. 539 * @return Success of not. 540 */ configureNativeBinaries(String apkFullPath, String libDirRelativePath, String abi, boolean extractNativeLibs)541 public boolean configureNativeBinaries(String apkFullPath, String libDirRelativePath, 542 String abi, boolean extractNativeLibs) { 543 try { 544 return mService.configureNativeBinaries(mId, apkFullPath, libDirRelativePath, abi, 545 extractNativeLibs); 546 } catch (RemoteException e) { 547 e.rethrowFromSystemServer(); 548 return false; 549 } 550 } 551 552 /** 553 * Waits for all native binary extraction operations to complete on the storage. 554 * 555 * @return Success of not. 556 */ waitForNativeBinariesExtraction()557 public boolean waitForNativeBinariesExtraction() { 558 try { 559 return mService.waitForNativeBinariesExtraction(mId); 560 } catch (RemoteException e) { 561 e.rethrowFromSystemServer(); 562 return false; 563 } 564 } 565 566 /** 567 * Register to listen to loading progress of all the files on this storage. 568 * @param listener To report progress from Incremental Service to the caller. 569 */ registerLoadingProgressListener(IStorageLoadingProgressListener listener)570 public boolean registerLoadingProgressListener(IStorageLoadingProgressListener listener) { 571 try { 572 return mService.registerLoadingProgressListener(mId, listener); 573 } catch (RemoteException e) { 574 e.rethrowFromSystemServer(); 575 return false; 576 } 577 } 578 579 /** 580 * Unregister to stop listening to storage loading progress. 581 */ unregisterLoadingProgressListener()582 public boolean unregisterLoadingProgressListener() { 583 try { 584 return mService.unregisterLoadingProgressListener(mId); 585 } catch (RemoteException e) { 586 e.rethrowFromSystemServer(); 587 return false; 588 } 589 } 590 591 /** 592 * Returns the metrics of the current storage. 593 * {@see IIncrementalService} for metrics keys. 594 */ getMetrics()595 public PersistableBundle getMetrics() { 596 try { 597 return mService.getMetrics(mId); 598 } catch (RemoteException e) { 599 e.rethrowFromSystemServer(); 600 return null; 601 } 602 } 603 } 604