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