1 /* 2 * Copyright 2022 Google LLC 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 package com.google.android.libraries.mobiledatadownload.internal; 17 18 import static com.google.common.util.concurrent.Futures.getDone; 19 import static com.google.common.util.concurrent.Futures.immediateFailedFuture; 20 import static com.google.common.util.concurrent.Futures.immediateFuture; 21 import static com.google.common.util.concurrent.Futures.immediateVoidFuture; 22 import static com.google.common.util.concurrent.MoreExecutors.directExecutor; 23 24 import android.content.Context; 25 import android.content.SharedPreferences; 26 import android.net.Uri; 27 import android.os.Build.VERSION; 28 import android.os.Build.VERSION_CODES; 29 import androidx.annotation.VisibleForTesting; 30 import com.google.android.libraries.mobiledatadownload.DownloadException; 31 import com.google.android.libraries.mobiledatadownload.DownloadException.DownloadResultCode; 32 import com.google.android.libraries.mobiledatadownload.FileSource; 33 import com.google.android.libraries.mobiledatadownload.Flags; 34 import com.google.android.libraries.mobiledatadownload.SilentFeedback; 35 import com.google.android.libraries.mobiledatadownload.annotations.InstanceId; 36 import com.google.android.libraries.mobiledatadownload.delta.DeltaDecoder; 37 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage; 38 import com.google.android.libraries.mobiledatadownload.file.common.UnsupportedFileStorageOperation; 39 import com.google.android.libraries.mobiledatadownload.internal.Migrations.FileKeyVersion; 40 import com.google.android.libraries.mobiledatadownload.internal.annotations.SequentialControlExecutor; 41 import com.google.android.libraries.mobiledatadownload.internal.downloader.DeltaFileDownloaderCallbackImpl; 42 import com.google.android.libraries.mobiledatadownload.internal.downloader.DownloaderCallbackImpl; 43 import com.google.android.libraries.mobiledatadownload.internal.downloader.FileNameUtil; 44 import com.google.android.libraries.mobiledatadownload.internal.downloader.FileValidator; 45 import com.google.android.libraries.mobiledatadownload.internal.downloader.MddFileDownloader; 46 import com.google.android.libraries.mobiledatadownload.internal.downloader.MddFileDownloader.DownloaderCallback; 47 import com.google.android.libraries.mobiledatadownload.internal.logging.EventLogger; 48 import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil; 49 import com.google.android.libraries.mobiledatadownload.internal.util.DirectoryUtil; 50 import com.google.android.libraries.mobiledatadownload.internal.util.SharedPreferencesUtil; 51 import com.google.android.libraries.mobiledatadownload.monitor.DownloadProgressMonitor; 52 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFluentFuture; 53 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures; 54 import com.google.common.base.Optional; 55 import com.google.common.collect.ImmutableMap; 56 import com.google.common.collect.ImmutableSet; 57 import com.google.common.util.concurrent.Futures; 58 import com.google.common.util.concurrent.ListenableFuture; 59 import com.google.errorprone.annotations.CheckReturnValue; 60 import com.google.mobiledatadownload.LogEnumsProto.MddClientEvent; 61 import com.google.mobiledatadownload.internal.MetadataProto.DataFile; 62 import com.google.mobiledatadownload.internal.MetadataProto.DataFileGroupInternal; 63 import com.google.mobiledatadownload.internal.MetadataProto.DataFileGroupInternal.AllowedReaders; 64 import com.google.mobiledatadownload.internal.MetadataProto.DeltaFile; 65 import com.google.mobiledatadownload.internal.MetadataProto.DeltaFile.DiffDecoder; 66 import com.google.mobiledatadownload.internal.MetadataProto.DownloadConditions; 67 import com.google.mobiledatadownload.internal.MetadataProto.ExtraHttpHeader; 68 import com.google.mobiledatadownload.internal.MetadataProto.FileStatus; 69 import com.google.mobiledatadownload.internal.MetadataProto.GroupKey; 70 import com.google.mobiledatadownload.internal.MetadataProto.NewFileKey; 71 import com.google.mobiledatadownload.internal.MetadataProto.SharedFile; 72 import java.io.IOException; 73 import java.io.PrintWriter; 74 import java.util.ArrayList; 75 import java.util.List; 76 import java.util.concurrent.Executor; 77 import javax.annotation.Nullable; 78 import javax.inject.Inject; 79 import org.checkerframework.checker.nullness.compatqual.NullableType; 80 81 /** 82 * Manages the life cycle of files used by MDD. For each file group in MDD, the file group will 83 * subscribe for the files that it needs. The SharedFileManager will maintain a reference count to 84 * ensure that it only retains files that are being used by MDD and that multiple file groups will 85 * share a single common file. 86 * 87 * <p>Whenever MDD receives a new filegroup, it will call {@link SharedFileManager#reserveFileEntry} 88 * for each file within the group. 89 * 90 * <p>When MDD discards a file group (because a new one has been received, downloaded), it will call 91 * {@link SharedFileManager#removeFileEntry} for each file within the group. 92 * 93 * <p>Note: SharedFileManager is considered thread-compatible. Calls to methods that modify the 94 * state of SharedFileManager {@link SharedFileManager#reserveFileEntry}, {@link 95 * SharedFileManager#startDownload}, {@link SharedFileManager#getFileStatus}, and {@link 96 * SharedFileManager#removeFileEntry} require exclusive access. 97 */ 98 @CheckReturnValue 99 public class SharedFileManager { 100 101 private static final String TAG = "SharedFileManager"; 102 103 public static final String MDD_SHARED_FILE_MANAGER_METADATA = 104 "gms_icing_mdd_shared_file_manager_metadata"; 105 106 @VisibleForTesting static final String PREFS_KEY_NEXT_FILE_NAME = "next_file_name_v2"; 107 @VisibleForTesting static final String FILE_NAME_PREFIX = "datadownloadfile_"; 108 109 @VisibleForTesting 110 static final String PREFS_KEY_MIGRATED_TO_NEW_FILE_KEY = "migrated_to_new_file_key"; 111 112 private final Context context; 113 private final SilentFeedback silentFeedback; 114 private final SharedFilesMetadata sharedFilesMetadata; 115 private final MddFileDownloader fileDownloader; 116 private final SynchronousFileStorage fileStorage; 117 private final Optional<DeltaDecoder> deltaDecoderOptional; 118 private final Optional<DownloadProgressMonitor> downloadMonitorOptional; 119 private final EventLogger eventLogger; 120 private final Flags flags; 121 private final FileGroupsMetadata fileGroupsMetadata; 122 private final Optional<String> instanceId; 123 private final Executor sequentialControlExecutor; 124 125 @Inject SharedFileManager( @pplicationContext Context context, SilentFeedback silentFeedback, SharedFilesMetadata sharedFilesMetadata, SynchronousFileStorage fileStorage, MddFileDownloader fileDownloader, Optional<DeltaDecoder> deltaDecoderOptional, Optional<DownloadProgressMonitor> downloadMonitorOptional, EventLogger eventLogger, Flags flags, FileGroupsMetadata fileGroupsMetadata, @InstanceId Optional<String> instanceId, @SequentialControlExecutor Executor sequentialControlExecutor)126 public SharedFileManager( 127 @ApplicationContext Context context, 128 SilentFeedback silentFeedback, 129 SharedFilesMetadata sharedFilesMetadata, 130 SynchronousFileStorage fileStorage, 131 MddFileDownloader fileDownloader, 132 Optional<DeltaDecoder> deltaDecoderOptional, 133 Optional<DownloadProgressMonitor> downloadMonitorOptional, 134 EventLogger eventLogger, 135 Flags flags, 136 FileGroupsMetadata fileGroupsMetadata, 137 @InstanceId Optional<String> instanceId, 138 @SequentialControlExecutor Executor sequentialControlExecutor) { 139 this.context = context; 140 this.silentFeedback = silentFeedback; 141 this.sharedFilesMetadata = sharedFilesMetadata; 142 this.fileStorage = fileStorage; 143 this.fileDownloader = fileDownloader; 144 this.deltaDecoderOptional = deltaDecoderOptional; 145 this.downloadMonitorOptional = downloadMonitorOptional; 146 this.eventLogger = eventLogger; 147 this.flags = flags; 148 this.fileGroupsMetadata = fileGroupsMetadata; 149 this.instanceId = instanceId; 150 this.sequentialControlExecutor = sequentialControlExecutor; 151 } 152 153 /** 154 * Makes any changes that should be made before accessing the internal state of this class. 155 * 156 * <p>Other methods in this class do not call or check if this method was already called before 157 * trying to access internal state. It is expected from the caller to call this before anything 158 * else. 159 * 160 * @return false if init failed, signalling caller to clear internal storage. 161 */ 162 // TODO(b/124072754): Change to package private once all code is refactored. init()163 public ListenableFuture<Boolean> init() { 164 SharedPreferences sharedFileManagerMetadata = 165 SharedPreferencesUtil.getSharedPreferences( 166 context, MDD_SHARED_FILE_MANAGER_METADATA, instanceId); 167 168 // Migrations class was added in v24, whereas new file key migration done in v23. If we already 169 // migrated, check and set it in Migrations. 170 if (sharedFileManagerMetadata.contains(PREFS_KEY_MIGRATED_TO_NEW_FILE_KEY)) { 171 if (sharedFileManagerMetadata.getBoolean(PREFS_KEY_MIGRATED_TO_NEW_FILE_KEY, false)) { 172 Migrations.setMigratedToNewFileKey(context, true); 173 } 174 sharedFileManagerMetadata.edit().remove(PREFS_KEY_MIGRATED_TO_NEW_FILE_KEY).commit(); 175 } 176 177 return immediateFuture(true); 178 } 179 180 /** 181 * Adds a subscribed file entry if there is no existing entry for newFileKey. Does nothing if such 182 * an entry already exists. 183 * 184 * @param newFileKey - the file key for the enry that you wish to reserve. 185 * @return - Future resolving to false if unable to commit the reservation 186 */ 187 // TODO - refactor to throw Exception when write to SharedPreferences fails reserveFileEntry(NewFileKey newFileKey)188 public ListenableFuture<Boolean> reserveFileEntry(NewFileKey newFileKey) { 189 return PropagatedFutures.transformAsync( 190 sharedFilesMetadata.read(newFileKey), 191 sharedFile -> { 192 if (sharedFile != null) { 193 // There's already an entry for this file. Nothing to do here. 194 return immediateFuture(true); 195 } 196 // Set the file name and update the metadata file. 197 SharedPreferences sharedFileManagerMetadata = 198 SharedPreferencesUtil.getSharedPreferences( 199 context, MDD_SHARED_FILE_MANAGER_METADATA, instanceId); 200 long nextFileName = 201 sharedFileManagerMetadata.getLong( 202 PREFS_KEY_NEXT_FILE_NAME, System.currentTimeMillis()); 203 if (!sharedFileManagerMetadata 204 .edit() 205 .putLong(PREFS_KEY_NEXT_FILE_NAME, nextFileName + 1) 206 .commit()) { 207 // TODO(b/131166925): MDD dump should not use lite proto toString. 208 LogUtil.e("%s: Unable to update file name %s", TAG, newFileKey); 209 return immediateFuture(false); 210 } 211 212 String fileName = FILE_NAME_PREFIX + nextFileName; 213 sharedFile = 214 SharedFile.newBuilder() 215 .setFileStatus(FileStatus.SUBSCRIBED) 216 .setFileName(fileName) 217 .build(); 218 return PropagatedFutures.transformAsync( 219 sharedFilesMetadata.write(newFileKey, sharedFile), 220 writeSuccess -> { 221 if (!writeSuccess) { 222 // TODO(b/131166925): MDD dump should not use lite proto toString. 223 LogUtil.e( 224 "%s: Unable to write back subscription for file entry with %s", 225 TAG, newFileKey); 226 return immediateFuture(false); 227 } 228 return immediateFuture(true); 229 }, 230 sequentialControlExecutor); 231 }, 232 sequentialControlExecutor); 233 } 234 235 /** 236 * Start importing a given file source if the file has not yet been downloaded/imported. 237 * 238 * <p>This method expects {@code dataFile} to have an "inlinefile:" scheme url. A 239 * DownloadException will be returned if a non-inlinefile scheme url is given. 240 * 241 * <p>If the file has already been downloaded/imported, this method is a no-op. 242 */ 243 ListenableFuture<Void> startImport( 244 GroupKey groupKey, 245 DataFile dataFile, 246 NewFileKey newFileKey, 247 @Nullable DownloadConditions downloadConditions, 248 FileSource inlineFileSource) { 249 if (!dataFile.getUrlToDownload().startsWith(MddConstants.INLINE_FILE_URL_SCHEME)) { 250 return immediateFailedFuture( 251 DownloadException.builder() 252 .setDownloadResultCode(DownloadResultCode.INVALID_INLINE_FILE_URL_SCHEME) 253 .setMessage("Importing an inline file requires inlinefile scheme") 254 .build()); 255 } 256 return PropagatedFutures.transformAsync( 257 sharedFilesMetadata.read(newFileKey), 258 sharedFile -> { 259 if (sharedFile == null) { 260 LogUtil.e( 261 "%s: Start import called on file that doesn't exist. Id = %s", 262 TAG, dataFile.getFileId()); 263 SharedFileMissingException cause = new SharedFileMissingException(); 264 // TODO(b/167582815): Log to Clearcut 265 return immediateFailedFuture( 266 DownloadException.builder() 267 .setDownloadResultCode(DownloadResultCode.SHARED_FILE_NOT_FOUND_ERROR) 268 .setCause(cause) 269 .build()); 270 } 271 272 // If we have already downloaded the file, then return. 273 if (sharedFile.getFileStatus() == FileStatus.DOWNLOAD_COMPLETE) { 274 return immediateVoidFuture(); 275 } 276 277 // Delta files are not supported, so only check for download transforms 278 SharedFile.Builder sharedFileBuilder = sharedFile.toBuilder(); 279 String downloadFileName = 280 dataFile.hasDownloadTransforms() 281 ? FileNameUtil.getTempFileNameWithDownloadedFileChecksum( 282 sharedFile.getFileName(), dataFile.getDownloadedFileChecksum()) 283 : sharedFile.getFileName(); 284 285 return PropagatedFutures.transformAsync( 286 getDataFileGroupOrDefault(groupKey), 287 dataFileGroup -> 288 getImportFuture( 289 sharedFileBuilder, 290 newFileKey, 291 downloadFileName, 292 dataFileGroup.getFileGroupVersionNumber(), 293 dataFileGroup.getBuildId(), 294 dataFileGroup.getVariantId(), 295 groupKey, 296 dataFile, 297 downloadConditions, 298 inlineFileSource), 299 sequentialControlExecutor); 300 }, 301 sequentialControlExecutor); 302 } 303 304 /** 305 * Gets a future that will perform the import. 306 * 307 * <p>Updates the sharedFile status to in-progress and attaches a callback to the import to handle 308 * post import actions. 309 */ 310 private ListenableFuture<Void> getImportFuture( 311 SharedFile.Builder sharedFileBuilder, 312 NewFileKey newFileKey, 313 String downloadFileName, 314 int fileGroupVersionNumber, 315 long buildId, 316 String variantId, 317 GroupKey groupKey, 318 DataFile dataFile, 319 @Nullable DownloadConditions downloadConditions, 320 FileSource inlineFileSource) { 321 ListenableFuture<Uri> downloadFileOnDeviceUriFuture = 322 getDownloadFileOnDeviceUri( 323 newFileKey.getAllowedReaders(), downloadFileName, dataFile.getChecksum()); 324 return PropagatedFluentFuture.from(downloadFileOnDeviceUriFuture) 325 .transformAsync( 326 unused -> { 327 sharedFileBuilder.setFileStatus(FileStatus.DOWNLOAD_IN_PROGRESS); 328 329 // Write returns a boolean indicating if the operation was successful or not. We can 330 // ignore this because a failure to write here won't impact the import operation. We 331 // will attempt to write the final state (completed or failed) after the import 332 // operation. 333 return sharedFilesMetadata.write(newFileKey, sharedFileBuilder.build()); 334 }, 335 sequentialControlExecutor) 336 .transformAsync( 337 unused -> { 338 Uri downloadFileOnDeviceUri = getDone(downloadFileOnDeviceUriFuture); 339 DownloaderCallback downloaderCallback = 340 new DownloaderCallbackImpl( 341 sharedFilesMetadata, 342 fileStorage, 343 dataFile, 344 newFileKey.getAllowedReaders(), 345 eventLogger, 346 groupKey, 347 fileGroupVersionNumber, 348 buildId, 349 variantId, 350 flags, 351 sequentialControlExecutor); 352 // TODO: when partial import files are supported, notify monitor of partial 353 // progress here. 354 355 return fileDownloader.startCopying( 356 newFileKey.getChecksum(), 357 downloadFileOnDeviceUri, 358 dataFile.getUrlToDownload(), 359 dataFile.getByteSize(), 360 downloadConditions, 361 downloaderCallback, 362 inlineFileSource); 363 }, 364 sequentialControlExecutor); 365 } 366 367 /** 368 * Start downloading the file if the file has not yet been downloaded. If the file has been 369 * downloaded, this method is a no-op. 370 * 371 * @param groupKey - a Key that uniquely identify a file group. 372 * @param dataFile - the data file proto provided by client 373 * @param newFileKey - the file key to get the SharedFile. 374 * @param downloadConditions - conditions under which this file should be downloaded. 375 * @param trafficTag - Tag for the network traffic to download this dataFile. 376 * @param extraHttpHeaders - Extra Headers for this request. 377 * @return - ListenableFuture representing the download the file. The ListenableFuture fails with 378 * {@link DownloadException} if the download is unsuccessful. 379 */ 380 ListenableFuture<Void> startDownload( 381 GroupKey groupKey, 382 DataFile dataFile, 383 NewFileKey newFileKey, 384 @Nullable DownloadConditions downloadConditions, 385 int trafficTag, 386 List<ExtraHttpHeader> extraHttpHeaders) { 387 if (dataFile.getUrlToDownload().startsWith(MddConstants.INLINE_FILE_URL_SCHEME)) { 388 return immediateFailedFuture( 389 DownloadException.builder() 390 .setDownloadResultCode(DownloadResultCode.INVALID_INLINE_FILE_URL_SCHEME) 391 .setMessage( 392 "downloading a file with an inlinefile scheme is not supported, use importFiles" 393 + " instead.") 394 .build()); 395 } 396 397 // Start futures in parallel for various calculated properties. 398 ListenableFuture<SharedFile> sharedFileFuture = getSharedFile(newFileKey); 399 400 ListenableFuture<@NullableType DeltaFile> firstDeltaFileFuture = 401 findFirstDeltaFileWithBaseFileDownloaded(dataFile, newFileKey.getAllowedReaders()); 402 403 ListenableFuture<String> downloadFileNameFuture = 404 PropagatedFutures.whenAllSucceed(sharedFileFuture, firstDeltaFileFuture) 405 .call( 406 () -> { 407 String downloadFileName = getDone(sharedFileFuture).getFileName(); 408 DeltaFile deltaFile = getDone(firstDeltaFileFuture); 409 if (deltaFile != null) { 410 downloadFileName = 411 FileNameUtil.getTempFileNameWithDownloadedFileChecksum( 412 downloadFileName, deltaFile.getChecksum()); 413 } else if (dataFile.hasDownloadTransforms()) { 414 downloadFileName = 415 FileNameUtil.getTempFileNameWithDownloadedFileChecksum( 416 downloadFileName, dataFile.getDownloadedFileChecksum()); 417 } 418 return downloadFileName; 419 }, 420 directExecutor()); 421 422 ListenableFuture<Uri> downloadFileOnDeviceUriFuture = 423 PropagatedFutures.transformAsync( 424 downloadFileNameFuture, 425 downloadFileName -> 426 getDownloadFileOnDeviceUri( 427 newFileKey.getAllowedReaders(), downloadFileName, dataFile.getChecksum()), 428 sequentialControlExecutor); 429 430 ListenableFuture<DataFileGroupInternal> dataFileGroupFuture = 431 getDataFileGroupOrDefault(groupKey); 432 433 // Combine all futures together so all complete successfully before continuing 434 ListenableFuture<Void> combinedPropertiesFuture = 435 PropagatedFutures.whenAllSucceed( 436 sharedFileFuture, 437 firstDeltaFileFuture, 438 downloadFileNameFuture, 439 downloadFileOnDeviceUriFuture, 440 dataFileGroupFuture) 441 .callAsync(Futures::immediateVoidFuture, directExecutor()); 442 443 return PropagatedFluentFuture.from(combinedPropertiesFuture) 444 .transformAsync( 445 unused -> { 446 SharedFile sharedFile = getDone(sharedFileFuture); 447 DeltaFile deltaFile = getDone(firstDeltaFileFuture); 448 String downloadFileName = getDone(downloadFileNameFuture); 449 Uri downloadFileOnDeviceUri = getDone(downloadFileOnDeviceUriFuture); 450 DataFileGroupInternal dataFileGroup = getDone(dataFileGroupFuture); 451 452 // Check if download is complete 453 if (sharedFile.getFileStatus() == FileStatus.DOWNLOAD_COMPLETE) { 454 if (downloadMonitorOptional.isPresent()) { 455 // For the downloaded file, we don't need to monitor the file change. We just need 456 // to inform the monitor about its current size. 457 downloadMonitorOptional 458 .get() 459 .notifyCurrentFileSize(groupKey.getGroupName(), dataFile.getByteSize()); 460 } 461 return immediateVoidFuture(); 462 } 463 464 // Check if a download is already in progress 465 if (sharedFile.getFileStatus() == FileStatus.DOWNLOAD_IN_PROGRESS) { 466 return PropagatedFutures.transformAsync( 467 fileDownloader.getInProgressFuture( 468 newFileKey.getChecksum(), downloadFileOnDeviceUri), 469 inProgressFuture -> { 470 if (inProgressFuture.isPresent()) { 471 mayNotifyCurrentSizeOfPartiallyDownloadedFile( 472 groupKey, downloadFileOnDeviceUri); 473 return inProgressFuture.get(); 474 } 475 return getDownloadFuture( 476 newFileKey, 477 downloadFileName, 478 dataFileGroup.getFileGroupVersionNumber(), 479 dataFileGroup.getBuildId(), 480 dataFileGroup.getVariantId(), 481 groupKey, 482 dataFile, 483 deltaFile, 484 downloadConditions, 485 trafficTag, 486 extraHttpHeaders); 487 }, 488 sequentialControlExecutor); 489 } 490 491 // Download is not in progress, start it. 492 return getDownloadFuture( 493 newFileKey, 494 downloadFileName, 495 dataFileGroup.getFileGroupVersionNumber(), 496 dataFileGroup.getBuildId(), 497 dataFileGroup.getVariantId(), 498 groupKey, 499 dataFile, 500 deltaFile, 501 downloadConditions, 502 trafficTag, 503 extraHttpHeaders); 504 }, 505 sequentialControlExecutor) 506 .catchingAsync( 507 SharedFileMissingException.class, 508 ex -> { 509 // TODO(b/131166925): MDD dump should not use lite proto toString. 510 LogUtil.e( 511 "%s: Start download called on file that doesn't exist. Key = %s!", 512 TAG, newFileKey); 513 silentFeedback.send(ex, "Shared file not found in downloadFileGroup"); 514 return immediateFailedFuture( 515 DownloadException.builder() 516 .setDownloadResultCode(DownloadResultCode.SHARED_FILE_NOT_FOUND_ERROR) 517 .setCause(ex) 518 .build()); 519 }, 520 sequentialControlExecutor); 521 } 522 523 private ListenableFuture<Void> getDownloadFuture( 524 NewFileKey newFileKey, 525 String downloadFileName, 526 int fileGroupVersionNumber, 527 long buildId, 528 String variantId, 529 GroupKey groupKey, 530 DataFile dataFile, 531 @Nullable DeltaFile deltaFile, 532 @Nullable DownloadConditions downloadConditions, 533 int trafficTag, 534 List<ExtraHttpHeader> extraHttpHeaders) { 535 // It's possible to hit a race condition where the caller of this method sees the file as not 536 // downloaded and by the time this method is executed, the file is already downloaded. 537 // 538 // Check the shared file status before starting the download to confirm it is not downloaded and 539 // a download is not already in progress. 540 return PropagatedFutures.transformAsync( 541 getSharedFile(newFileKey), 542 latestSharedFile -> { 543 if (latestSharedFile.getFileStatus() == FileStatus.DOWNLOAD_COMPLETE) { 544 return immediateVoidFuture(); 545 } 546 547 // Download is not complete, proceed with starting the future. 548 SharedFile.Builder sharedFileBuilder = latestSharedFile.toBuilder(); 549 ListenableFuture<Uri> downloadFileOnDeviceUriFuture = 550 getDownloadFileOnDeviceUri( 551 newFileKey.getAllowedReaders(), downloadFileName, dataFile.getChecksum()); 552 return PropagatedFluentFuture.from(downloadFileOnDeviceUriFuture) 553 .transformAsync( 554 unused -> { 555 sharedFileBuilder.setFileStatus(FileStatus.DOWNLOAD_IN_PROGRESS); 556 557 // Ignoring failure to write back here, as it will just result in one 558 // extra try 559 // to download the file. 560 return sharedFilesMetadata.write(newFileKey, sharedFileBuilder.build()); 561 }, 562 sequentialControlExecutor) 563 .transformAsync( 564 unused -> { 565 Uri downloadFileOnDeviceUri = getDone(downloadFileOnDeviceUriFuture); 566 ListenableFuture<Void> fileDownloadFuture; 567 if (!deltaDecoderOptional.isPresent() || deltaFile == null) { 568 // Download full file when delta file is null 569 DownloaderCallback downloaderCallback = 570 new DownloaderCallbackImpl( 571 sharedFilesMetadata, 572 fileStorage, 573 dataFile, 574 newFileKey.getAllowedReaders(), 575 eventLogger, 576 groupKey, 577 fileGroupVersionNumber, 578 buildId, 579 variantId, 580 flags, 581 sequentialControlExecutor); 582 583 mayNotifyCurrentSizeOfPartiallyDownloadedFile( 584 groupKey, downloadFileOnDeviceUri); 585 586 fileDownloadFuture = 587 fileDownloader.startDownloading( 588 newFileKey.getChecksum(), 589 groupKey, 590 fileGroupVersionNumber, 591 buildId, 592 variantId, 593 downloadFileOnDeviceUri, 594 dataFile.getUrlToDownload(), 595 dataFile.getByteSize(), 596 downloadConditions, 597 downloaderCallback, 598 trafficTag, 599 extraHttpHeaders); 600 } else { 601 DownloaderCallback downloaderCallback = 602 new DeltaFileDownloaderCallbackImpl( 603 context, 604 sharedFilesMetadata, 605 fileStorage, 606 silentFeedback, 607 dataFile, 608 newFileKey.getAllowedReaders(), 609 deltaDecoderOptional.get(), 610 deltaFile, 611 eventLogger, 612 groupKey, 613 fileGroupVersionNumber, 614 buildId, 615 variantId, 616 instanceId, 617 flags, 618 sequentialControlExecutor); 619 620 mayNotifyCurrentSizeOfPartiallyDownloadedFile( 621 groupKey, downloadFileOnDeviceUri); 622 623 fileDownloadFuture = 624 fileDownloader.startDownloading( 625 newFileKey.getChecksum(), 626 groupKey, 627 fileGroupVersionNumber, 628 buildId, 629 variantId, 630 downloadFileOnDeviceUri, 631 deltaFile.getUrlToDownload(), 632 deltaFile.getByteSize(), 633 downloadConditions, 634 downloaderCallback, 635 trafficTag, 636 extraHttpHeaders); 637 } 638 return fileDownloadFuture; 639 }, 640 sequentialControlExecutor); 641 }, 642 sequentialControlExecutor); 643 } 644 645 /** 646 * Gets the URI where the given file should be located on-device. 647 * 648 * @param allowedReaders the allowed readers of the file 649 * @param downloadFileName the name of the file 650 * @param checksum the checksum of the file 651 */ 652 private ListenableFuture<Uri> getDownloadFileOnDeviceUri( 653 AllowedReaders allowedReaders, String downloadFileName, String checksum) { 654 Uri downloadFileOnDeviceUri = 655 DirectoryUtil.getOnDeviceUri( 656 context, 657 allowedReaders, 658 downloadFileName, 659 checksum, 660 silentFeedback, 661 instanceId, 662 /* androidShared= */ false); 663 if (downloadFileOnDeviceUri == null) { 664 LogUtil.e("%s: Failed to get file uri!", TAG); 665 return immediateFailedFuture( 666 DownloadException.builder() 667 .setDownloadResultCode(DownloadResultCode.UNABLE_TO_CREATE_FILE_URI_ERROR) 668 .build()); 669 } 670 return immediateFuture(downloadFileOnDeviceUri); 671 } 672 673 private void mayNotifyCurrentSizeOfPartiallyDownloadedFile( 674 GroupKey groupKey, Uri downloadFileOnDeviceUri) { 675 if (downloadMonitorOptional.isPresent()) { 676 // Inform the monitor about the current size of the partially downloaded file. 677 try { 678 long currentFileSize = fileStorage.fileSize(downloadFileOnDeviceUri); 679 if (currentFileSize > 0) { 680 downloadMonitorOptional 681 .get() 682 .notifyCurrentFileSize(groupKey.getGroupName(), currentFileSize); 683 } 684 } catch (IOException e) { 685 // Ignore any fileSize error. 686 } 687 } 688 } 689 690 private ListenableFuture<DataFileGroupInternal> getDataFileGroupOrDefault(GroupKey groupKey) { 691 return PropagatedFutures.transformAsync( 692 fileGroupsMetadata.read(groupKey), 693 fileGroup -> 694 immediateFuture( 695 (fileGroup == null) ? DataFileGroupInternal.getDefaultInstance() : fileGroup), 696 sequentialControlExecutor); 697 } 698 699 /** 700 * @param dataFile - a DataFile proto object 701 * @param allowedReaders - allowed readers of the file group, assuming the base file has the same 702 * readers set 703 * @return - the first Delta file which its base file is on device and its file status is download 704 * completed 705 */ 706 @VisibleForTesting 707 ListenableFuture<@NullableType DeltaFile> findFirstDeltaFileWithBaseFileDownloaded( 708 DataFile dataFile, AllowedReaders allowedReaders) { 709 if (Migrations.getCurrentVersion(context, silentFeedback).value 710 < FileKeyVersion.USE_CHECKSUM_ONLY.value 711 || !deltaDecoderOptional.isPresent() 712 || deltaDecoderOptional.get().getDecoderName() == DiffDecoder.UNSPECIFIED) { 713 return immediateFuture(null); 714 } 715 return findFirstDeltaFileWithBaseFileDownloaded( 716 dataFile.getDeltaFileList(), /* index= */ 0, allowedReaders); 717 } 718 719 // We must use recursion here since the decision to continue iterating is dependent on the result 720 // of the asynchronous SharedFilesMetadata.read() operation 721 private ListenableFuture<@NullableType DeltaFile> findFirstDeltaFileWithBaseFileDownloaded( 722 List<DeltaFile> deltaFiles, int index, AllowedReaders allowedReaders) { 723 if (index == deltaFiles.size()) { 724 return immediateFuture(null); 725 } 726 DeltaFile deltaFile = deltaFiles.get(index); 727 if (deltaFile.getDiffDecoder() != deltaDecoderOptional.get().getDecoderName()) { 728 return findFirstDeltaFileWithBaseFileDownloaded(deltaFiles, index + 1, allowedReaders); 729 } 730 NewFileKey baseFileKey = 731 NewFileKey.newBuilder() 732 .setChecksum(deltaFile.getBaseFile().getChecksum()) 733 .setAllowedReaders(allowedReaders) 734 .build(); 735 return PropagatedFutures.transformAsync( 736 sharedFilesMetadata.read(baseFileKey), 737 baseFileMetadata -> { 738 if (baseFileMetadata != null 739 && baseFileMetadata.getFileStatus() == FileStatus.DOWNLOAD_COMPLETE) { 740 Uri baseFileUri = 741 DirectoryUtil.getOnDeviceUri( 742 context, 743 baseFileKey.getAllowedReaders(), 744 baseFileMetadata.getFileName(), 745 baseFileKey.getChecksum(), 746 silentFeedback, 747 instanceId, 748 /* androidShared= */ false); 749 if (baseFileUri != null) { 750 return immediateFuture(deltaFile); 751 } 752 } 753 return findFirstDeltaFileWithBaseFileDownloaded(deltaFiles, index + 1, allowedReaders); 754 }, 755 sequentialControlExecutor); 756 } 757 /** 758 * Returns the current status of the file. 759 * 760 * @param newFileKey - the file key to get the SharedFile. 761 * @return - FileStatus representing the current state of the file. The ListenableFuture may throw 762 * a SharedFileMissingException if the shared file metadata is missing. 763 */ 764 ListenableFuture<FileStatus> getFileStatus(NewFileKey newFileKey) { 765 return PropagatedFutures.transformAsync( 766 getSharedFile(newFileKey), 767 existingSharedFile -> immediateFuture(existingSharedFile.getFileStatus()), 768 sequentialControlExecutor); 769 } 770 771 /** 772 * Verifies that the file exists in metadata and on disk. Also performs the same validation check 773 * that's performed after download to ensure the file hasn't been deleted or corrupted. 774 * 775 * @param newFileKey - the file key to get the SharedFile. 776 * @return - The ListenableFuture may throw a SharedFileMissingException if the shared file 777 * metadata is missing or the on disk file is corrupted. 778 */ 779 ListenableFuture<Void> reVerifyFile(NewFileKey newFileKey, DataFile dataFile) { 780 return PropagatedFluentFuture.from(getSharedFile(newFileKey)) 781 .transformAsync( 782 existingSharedFile -> { 783 if (existingSharedFile.getFileStatus() != FileStatus.DOWNLOAD_COMPLETE) { 784 return immediateVoidFuture(); 785 } 786 // Double check that it's really complete, and update status if it's not. 787 return PropagatedFluentFuture.from(getOnDeviceUri(newFileKey)) 788 .transformAsync( 789 uri -> { 790 if (uri == null) { 791 throw DownloadException.builder() 792 .setDownloadResultCode( 793 DownloadResultCode.DOWNLOADED_FILE_NOT_FOUND_ERROR) 794 .build(); 795 } 796 if (existingSharedFile.getAndroidShared()) { 797 // Just check for presence. BlobStoreManager is responsible for 798 // integrity. 799 if (!fileStorage.exists(uri)) { 800 throw DownloadException.builder() 801 .setDownloadResultCode( 802 DownloadResultCode.DOWNLOADED_FILE_NOT_FOUND_ERROR) 803 .build(); 804 } 805 } else { 806 FileValidator.validateDownloadedFile( 807 fileStorage, dataFile, uri, dataFile.getChecksum()); 808 } 809 return immediateVoidFuture(); 810 }, 811 sequentialControlExecutor) 812 .catchingAsync( 813 DownloadException.class, 814 e -> { 815 LogUtil.e( 816 "%s: reVerifyFile lost or corrupted code %s", 817 TAG, e.getDownloadResultCode()); 818 SharedFile updatedSharedFile = 819 existingSharedFile.toBuilder() 820 .setFileStatus(FileStatus.CORRUPTED) 821 .build(); 822 return PropagatedFluentFuture.from( 823 sharedFilesMetadata.write(newFileKey, updatedSharedFile)) 824 .transformAsync( 825 ok -> { 826 SharedFileMissingException ex = new SharedFileMissingException(); 827 if (!ok) { 828 throw new IOException("failed to save sharedFilesMetadata", ex); 829 } 830 throw ex; 831 }, 832 sequentialControlExecutor); 833 }, 834 sequentialControlExecutor); 835 }, 836 sequentialControlExecutor); 837 } 838 839 /** 840 * Returns the {@code SharedFile}. 841 * 842 * @param newFileKey - the file key to get the SharedFile. 843 * @return - the SharedFile representing the current metadata of the file. The ListenableFuture 844 * may throw a SharedFileMissingException if the shared file metadata is missing. 845 */ 846 ListenableFuture<SharedFile> getSharedFile(NewFileKey newFileKey) { 847 return PropagatedFutures.transformAsync( 848 sharedFilesMetadata.read(newFileKey), 849 existingSharedFile -> { 850 if (existingSharedFile == null) { 851 // TODO(b/131166925): MDD dump should not use lite proto toString. 852 LogUtil.e( 853 "%s: getSharedFile called on file that doesn't exist! Key = %s", TAG, newFileKey); 854 return immediateFailedFuture(new SharedFileMissingException()); 855 } 856 return immediateFuture(existingSharedFile); 857 }, 858 sequentialControlExecutor); 859 } 860 861 /** 862 * Sets a file entry as downloaded and android-shared. If there is an existing entry for {@code 863 * newFileKey}, overwrites it. 864 * 865 * @param newFileKey - the file key for the enry that you wish to store. 866 * @param androidSharingChecksum - the file checksum that represents a blob in the Android Sharing 867 * Service. 868 * @param maxExpirationDateSecs - the new maximum expiration date 869 * @return - false if unable to commit the write operation 870 */ 871 ListenableFuture<Boolean> setAndroidSharedDownloadedFileEntry( 872 NewFileKey newFileKey, String androidSharingChecksum, long maxExpirationDateSecs) { 873 SharedFile newSharedFile = 874 SharedFile.newBuilder() 875 .setFileStatus(FileStatus.DOWNLOAD_COMPLETE) 876 .setFileName("android_shared_" + androidSharingChecksum) 877 .setAndroidShared(true) 878 .setMaxExpirationDateSecs(maxExpirationDateSecs) 879 .setAndroidSharingChecksum(androidSharingChecksum) 880 .build(); 881 return sharedFilesMetadata.write(newFileKey, newSharedFile); 882 } 883 884 /** 885 * If necessary, updates the {@code max_expiration_date} date stored in the shared file metadata 886 * associated to {@code newFileKey}. No-op otherwise. 887 * 888 * @param newFileKey - the file key for the enry that you wish to update. 889 * @param fileExpirationDateSecs - the expiration date of the current file. 890 * @return - false if unable to commit the write operation. The ListenableFuture may throw a 891 * SharedFileMissingException if the shared file metadata is missing. 892 */ 893 ListenableFuture<Boolean> updateMaxExpirationDateSecs( 894 NewFileKey newFileKey, long fileExpirationDateSecs) { 895 return PropagatedFutures.transformAsync( 896 getSharedFile(newFileKey), 897 existingSharedFile -> { 898 if (fileExpirationDateSecs > existingSharedFile.getMaxExpirationDateSecs()) { 899 SharedFile updatedSharedFile = 900 existingSharedFile.toBuilder() 901 .setMaxExpirationDateSecs(fileExpirationDateSecs) 902 .build(); 903 return sharedFilesMetadata.write(newFileKey, updatedSharedFile); 904 } 905 return immediateFuture(true); 906 }, 907 sequentialControlExecutor); 908 } 909 910 /** 911 * Returns future resolving to uri for the file. 912 * 913 * @param newFileKey - the file key to get the SharedFile. 914 * @return - a future resolving to the MobStore android Uri that is associated with the file. The 915 * uri will be null if the SharedFileManager doesn't have an entry matching that file or there 916 * is an error populating the uri of the file. 917 */ 918 public ListenableFuture<@NullableType Uri> getOnDeviceUri(NewFileKey newFileKey) { 919 return PropagatedFutures.transform( 920 getOnDeviceUris(ImmutableSet.of(newFileKey)), 921 uris -> uris.get(newFileKey), 922 directExecutor()); 923 } 924 925 /** 926 * Get the known on-device uris for a given list of {@link NewFileKey}s 927 * 928 * <p>The returned map may or may not have an entry for each NewFileKey on the list, depending on 929 * if it was possible to create the uri (see {@link DirectoryUtil#getOnDeviceUri()} for more 930 * details). 931 * 932 * <p>If any {@link NewFileKey} does not map to a {@link SharedFile}, the returned future will be 933 * a failure containing {@link SharedFileMissingException}. 934 */ 935 ListenableFuture<ImmutableMap<NewFileKey, Uri>> getOnDeviceUris( 936 ImmutableSet<NewFileKey> newFileKeys) { 937 return PropagatedFluentFuture.from(sharedFilesMetadata.readAll(newFileKeys)) 938 .transformAsync( 939 sharedFileMap -> { 940 ImmutableMap.Builder<NewFileKey, Uri> uriMapBuilder = ImmutableMap.builder(); 941 for (NewFileKey newFileKey : newFileKeys) { 942 // Make sure all SharedFiles exist. 943 if (!sharedFileMap.containsKey(newFileKey)) { 944 // TODO(b/131166925): MDD dump should not use lite proto toString. 945 LogUtil.e( 946 "%s: getOnDeviceUris called on file that doesn't exist. Key = %s!", 947 TAG, newFileKey); 948 return immediateFailedFuture(new SharedFileMissingException()); 949 } 950 951 SharedFile sharedFile = sharedFileMap.get(newFileKey); 952 953 Uri onDeviceUri = 954 DirectoryUtil.getOnDeviceUri( 955 context, 956 newFileKey.getAllowedReaders(), 957 sharedFile.getFileName(), 958 sharedFile.getAndroidSharingChecksum(), 959 silentFeedback, 960 instanceId, 961 sharedFile.getAndroidShared()); 962 if (onDeviceUri != null) { 963 uriMapBuilder.put(newFileKey, onDeviceUri); 964 } 965 } 966 return immediateFuture(uriMapBuilder.buildKeepingLast()); 967 }, 968 sequentialControlExecutor); 969 } 970 971 /** 972 * Removes the file entry corresponding to newFileKey. If the file hasn't been fully downloaded, 973 * the partial file is deleted from the device and the download is cancelled. 974 * 975 * @param newFileKey - the key of the file entry to remove. 976 * @return - false if there is no entry with this key or unable to remove the entry 977 */ 978 // TODO - refactor to throw Exception when write to SharedPreferences fails 979 ListenableFuture<Boolean> removeFileEntry(NewFileKey newFileKey) { 980 return PropagatedFutures.transformAsync( 981 sharedFilesMetadata.read(newFileKey), 982 sharedFile -> { 983 if (sharedFile == null) { 984 // TODO(b/131166925): MDD dump should not use lite proto toString. 985 LogUtil.e("%s: No file entry with key %s", TAG, newFileKey); 986 return immediateFuture(false); 987 } 988 989 Uri onDeviceUri = 990 DirectoryUtil.getOnDeviceUri( 991 context, 992 newFileKey.getAllowedReaders(), 993 sharedFile.getFileName(), 994 newFileKey.getChecksum(), 995 silentFeedback, 996 instanceId, 997 /* androidShared= */ false); 998 if (onDeviceUri != null) { 999 fileDownloader.stopDownloading(newFileKey.getChecksum(), onDeviceUri); 1000 } 1001 return PropagatedFutures.transformAsync( 1002 sharedFilesMetadata.remove(newFileKey), 1003 removeSuccess -> { 1004 if (!removeSuccess) { 1005 // TODO(b/131166925): MDD dump should not use lite proto toString. 1006 LogUtil.e("%s: Unable to modify file subscription for key %s", TAG, newFileKey); 1007 return immediateFuture(false); 1008 } 1009 return immediateFuture(true); 1010 }, 1011 sequentialControlExecutor); 1012 }, 1013 sequentialControlExecutor); 1014 } 1015 1016 /** 1017 * Clears all storage used by the SharedFileManager and deletes all files that have been 1018 * downloaded to MDD's directory. 1019 */ 1020 1021 // TODO(b/124072754): Change to package private once all code is refactored. 1022 public ListenableFuture<Void> clear() { 1023 // If sdk is R+, try release all leases that the MDD Client may have acquired. This 1024 // prevents from leaving zombie files in the blob storage. 1025 if (VERSION.SDK_INT >= VERSION_CODES.R) { 1026 releaseAllAndroidSharedFiles(); 1027 } 1028 try { 1029 fileStorage.deleteRecursively(DirectoryUtil.getBaseDownloadDirectory(context, instanceId)); 1030 } catch (IOException e) { 1031 silentFeedback.send(e, "Failure while deleting mdd storage during clear"); 1032 } 1033 return immediateVoidFuture(); 1034 } 1035 1036 private void releaseAllAndroidSharedFiles() { 1037 try { 1038 Uri allLeasesUri = DirectoryUtil.getBlobStoreAllLeasesUri(context); 1039 fileStorage.deleteFile(allLeasesUri); 1040 eventLogger.logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED); 1041 } catch (UnsupportedFileStorageOperation e) { 1042 LogUtil.v( 1043 "%s: Failed to release the leases in the android shared storage." 1044 + " UnsupportedFileStorageOperation was thrown", 1045 TAG); 1046 } catch (IOException e) { 1047 LogUtil.e(e, "%s: Failed to release the leases in the android shared storage", TAG); 1048 eventLogger.logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED); 1049 } 1050 } 1051 1052 public ListenableFuture<Void> cancelDownloadAndClear() { 1053 return PropagatedFutures.transformAsync( 1054 sharedFilesMetadata.getAllFileKeys(), 1055 newFileKeyList -> { 1056 List<ListenableFuture<Void>> cancelDownloadFutures = new ArrayList<>(); 1057 try { 1058 // Clear is called in case something fails and we want to clear all of MDD internal 1059 // storage. Catching any exception in cancelling downloads is better than clear failing, 1060 // as it can leave the system in a non-recoverable state. 1061 for (NewFileKey newFileKey : newFileKeyList) { 1062 cancelDownloadFutures.add(cancelDownload(newFileKey)); 1063 } 1064 } catch (Exception e) { 1065 silentFeedback.send(e, "Failed to cancel all downloads during clear"); 1066 } 1067 return PropagatedFutures.whenAllComplete(cancelDownloadFutures) 1068 .callAsync(this::clear, sequentialControlExecutor); 1069 }, 1070 sequentialControlExecutor); 1071 } 1072 1073 public ListenableFuture<Void> cancelDownload(NewFileKey newFileKey) { 1074 return PropagatedFutures.transformAsync( 1075 sharedFilesMetadata.read(newFileKey), 1076 sharedFile -> { 1077 if (sharedFile == null) { 1078 LogUtil.e("%s: Unable to read sharedFile from shared preferences.", TAG); 1079 return immediateFailedFuture(new SharedFileMissingException()); 1080 } 1081 if (sharedFile.getFileStatus() != FileStatus.DOWNLOAD_COMPLETE) { 1082 Uri onDeviceUri = 1083 DirectoryUtil.getOnDeviceUri( 1084 context, 1085 newFileKey.getAllowedReaders(), 1086 sharedFile.getFileName(), 1087 newFileKey.getChecksum(), 1088 silentFeedback, 1089 instanceId, 1090 /* androidShared= */ false); // while downloading androidShared is always false 1091 if (onDeviceUri != null) { 1092 fileDownloader.stopDownloading(newFileKey.getChecksum(), onDeviceUri); 1093 } 1094 // If the download was in progress, reset it back to subscribed, so it can be properly 1095 // restarted. 1096 if (sharedFile.getFileStatus() == FileStatus.DOWNLOAD_IN_PROGRESS) { 1097 return PropagatedFutures.transformAsync( 1098 sharedFilesMetadata.write( 1099 newFileKey, 1100 sharedFile.toBuilder().setFileStatus(FileStatus.SUBSCRIBED).build()), 1101 unused -> immediateVoidFuture(), 1102 sequentialControlExecutor); 1103 } 1104 } 1105 return immediateVoidFuture(); 1106 }, 1107 sequentialControlExecutor); 1108 } 1109 1110 /** Dumps the current internal state of the SharedFileManager. */ 1111 public ListenableFuture<Void> dump(final PrintWriter writer) { 1112 writer.println("==== MDD_SHARED_FILES ===="); 1113 return PropagatedFutures.transformAsync( 1114 sharedFilesMetadata.getAllFileKeys(), 1115 allFileKeys -> { 1116 ListenableFuture<Void> writeFilesFuture = immediateVoidFuture(); 1117 for (NewFileKey newFileKey : allFileKeys) { 1118 writeFilesFuture = 1119 PropagatedFutures.transformAsync( 1120 writeFilesFuture, 1121 voidArg -> 1122 PropagatedFutures.transformAsync( 1123 sharedFilesMetadata.read(newFileKey), 1124 sharedFile -> { 1125 if (sharedFile == null) { 1126 LogUtil.e( 1127 "%s: Unable to read sharedFile from shared preferences.", TAG); 1128 return immediateVoidFuture(); 1129 } 1130 // TODO(b/131166925): MDD dump should not use lite proto toString. 1131 writer.format( 1132 "FileKey: %s\nFileName: %s\nSharedFile: %s\n", 1133 newFileKey, sharedFile.getFileName(), sharedFile.toString()); 1134 if (sharedFile.getAndroidShared()) { 1135 writer.format( 1136 "Checksum Android-shared file: %s\n", 1137 sharedFile.getAndroidSharingChecksum()); 1138 } else { 1139 Uri serializedUri = 1140 DirectoryUtil.getOnDeviceUri( 1141 context, 1142 newFileKey.getAllowedReaders(), 1143 sharedFile.getFileName(), 1144 newFileKey.getChecksum(), 1145 silentFeedback, 1146 instanceId, 1147 /* androidShared= */ false); 1148 if (serializedUri != null) { 1149 writer.format( 1150 "Checksum downloaded file: %s\n", 1151 FileValidator.computeSha1Digest(fileStorage, serializedUri)); 1152 } 1153 } 1154 return immediateVoidFuture(); 1155 }, 1156 sequentialControlExecutor), 1157 sequentialControlExecutor); 1158 } 1159 return writeFilesFuture; 1160 }, 1161 sequentialControlExecutor); 1162 } 1163 } 1164