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.downloader; 17 18 import static com.google.common.util.concurrent.Futures.immediateFailedFuture; 19 import static com.google.common.util.concurrent.Futures.immediateFuture; 20 import static com.google.common.util.concurrent.Futures.immediateVoidFuture; 21 import static java.lang.Math.min; 22 23 import android.content.Context; 24 import android.net.Uri; 25 import android.os.StatFs; 26 import android.util.Pair; 27 import androidx.annotation.VisibleForTesting; 28 import com.google.android.libraries.mobiledatadownload.DownloadException; 29 import com.google.android.libraries.mobiledatadownload.DownloadException.DownloadResultCode; 30 import com.google.android.libraries.mobiledatadownload.FileSource; 31 import com.google.android.libraries.mobiledatadownload.Flags; 32 import com.google.android.libraries.mobiledatadownload.downloader.DownloadConstraints; 33 import com.google.android.libraries.mobiledatadownload.downloader.DownloadRequest; 34 import com.google.android.libraries.mobiledatadownload.downloader.FileDownloader; 35 import com.google.android.libraries.mobiledatadownload.downloader.InlineDownloadParams; 36 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage; 37 import com.google.android.libraries.mobiledatadownload.internal.ApplicationContext; 38 import com.google.android.libraries.mobiledatadownload.internal.annotations.SequentialControlExecutor; 39 import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil; 40 import com.google.android.libraries.mobiledatadownload.internal.logging.LoggingStateStore; 41 import com.google.android.libraries.mobiledatadownload.internal.util.DownloadFutureMap; 42 import com.google.android.libraries.mobiledatadownload.internal.util.FileGroupUtil; 43 import com.google.android.libraries.mobiledatadownload.monitor.DownloadProgressMonitor; 44 import com.google.android.libraries.mobiledatadownload.monitor.NetworkUsageMonitor; 45 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFluentFuture; 46 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures; 47 import com.google.common.base.Optional; 48 import com.google.common.base.Supplier; 49 import com.google.common.collect.ImmutableList; 50 import com.google.common.util.concurrent.AsyncFunction; 51 import com.google.common.util.concurrent.ListenableFuture; 52 import com.google.common.util.concurrent.ListenableFutureTask; 53 import com.google.mobiledatadownload.internal.MetadataProto.DownloadConditions; 54 import com.google.mobiledatadownload.internal.MetadataProto.DownloadConditions.DeviceNetworkPolicy; 55 import com.google.mobiledatadownload.internal.MetadataProto.ExtraHttpHeader; 56 import com.google.mobiledatadownload.internal.MetadataProto.GroupKey; 57 import java.io.IOException; 58 import java.util.HashMap; 59 import java.util.List; 60 import java.util.concurrent.Executor; 61 import javax.annotation.Nullable; 62 import javax.inject.Inject; 63 64 /** 65 * Responsible for downloading files in MDD. 66 * 67 * <p>Provides methods to start and stop downloading a file. The stop method can be called if the 68 * file is no longer needed, or the file was already downloaded to the device. 69 * 70 * <p>This class supports both standard downloads (over https) or inline files (from a ByteString), 71 * using {@link #startDownloading} and {@link #startCopying}, respectively. 72 */ 73 // TODO(b/129497867): Add tracking for on-going download to dedup download request from 74 // FileDownloader. 75 public class MddFileDownloader { 76 77 private static final String TAG = "MddFileDownloader"; 78 79 // These should only be accessed through the getters and never directly. 80 private final Context context; 81 private final Supplier<FileDownloader> fileDownloaderSupplier; 82 private final SynchronousFileStorage fileStorage; 83 private final NetworkUsageMonitor networkUsageMonitor; 84 private final Optional<DownloadProgressMonitor> downloadMonitorOptional; 85 private final LoggingStateStore loggingStateStore; 86 private final Executor sequentialControlExecutor; 87 private final Flags flags; 88 89 // Cache for all on-going downloads. This will be used to de-dup download requests. 90 // NOTE: all operations are internally sequenced through an ExecutionSequencer. 91 // NOTE: this map and fileUriToDownloadFutureMap are mutually exclusive and the use of 92 // one or the other is based on an MDD feature flag (enableFileDownloadDedupByFileKey). Once the 93 // flag is fully rolled out, this map will be used exclusively. 94 private final DownloadFutureMap<Void> downloadOrCopyFutureMap; 95 96 // Cache for all on-going downloads. This will be used to de-dup download requests. 97 // NOTE: currently we assume that this map will only be accessed through the 98 // SequentialControlExecutor, so we don't need synchronization here. 99 // NOTE: this map and downloadOrCopyFutureMap are mutually exclusive and the use of 100 // one or the other is based on an MDD feature flag (enableFileDownloadDedupByFileKey). Once the 101 // flag is fully rolled out, this map will not be used. 102 @VisibleForTesting 103 final HashMap<Uri, ListenableFuture<Void>> fileUriToDownloadFutureMap = new HashMap<>(); 104 105 @Inject MddFileDownloader( @pplicationContext Context context, Supplier<FileDownloader> fileDownloaderSupplier, SynchronousFileStorage fileStorage, NetworkUsageMonitor networkUsageMonitor, Optional<DownloadProgressMonitor> downloadMonitor, LoggingStateStore loggingStateStore, @SequentialControlExecutor Executor sequentialControlExecutor, Flags flags)106 public MddFileDownloader( 107 @ApplicationContext Context context, 108 Supplier<FileDownloader> fileDownloaderSupplier, 109 SynchronousFileStorage fileStorage, 110 NetworkUsageMonitor networkUsageMonitor, 111 Optional<DownloadProgressMonitor> downloadMonitor, 112 LoggingStateStore loggingStateStore, 113 @SequentialControlExecutor Executor sequentialControlExecutor, 114 Flags flags) { 115 this.context = context; 116 this.fileDownloaderSupplier = fileDownloaderSupplier; 117 this.fileStorage = fileStorage; 118 this.networkUsageMonitor = networkUsageMonitor; 119 this.downloadMonitorOptional = downloadMonitor; 120 this.loggingStateStore = loggingStateStore; 121 this.sequentialControlExecutor = sequentialControlExecutor; 122 this.flags = flags; 123 this.downloadOrCopyFutureMap = DownloadFutureMap.create(sequentialControlExecutor); 124 } 125 126 /** 127 * Start downloading the file. 128 * 129 * @param fileKey key that identifies the shared file to download. 130 * @param groupKey GroupKey that contains the file to download. 131 * @param fileGroupVersionNumber version number of the group that contains the file to download. 132 * @param buildId build id of the group that contains the file to download. 133 * @param variantId variant id of the group that contains the file to download. 134 * @param fileUri - the File Uri to download the file at. 135 * @param urlToDownload - The url of the file to download. 136 * @param fileSize - the expected size of the file to download. 137 * @param downloadConditions - conditions under which this file should be downloaded. 138 * @param callback - callback called when the download either completes or fails. 139 * @param trafficTag - Tag for the network traffic to download this dataFile. 140 * @param extraHttpHeaders - Extra Headers for this request. 141 * @return - ListenableFuture representing the download result of a file. 142 */ startDownloading( String fileKey, GroupKey groupKey, int fileGroupVersionNumber, long buildId, String variantId, Uri fileUri, String urlToDownload, int fileSize, @Nullable DownloadConditions downloadConditions, DownloaderCallback callback, int trafficTag, List<ExtraHttpHeader> extraHttpHeaders)143 public ListenableFuture<Void> startDownloading( 144 String fileKey, 145 GroupKey groupKey, 146 int fileGroupVersionNumber, 147 long buildId, 148 String variantId, 149 Uri fileUri, 150 String urlToDownload, 151 int fileSize, 152 @Nullable DownloadConditions downloadConditions, 153 DownloaderCallback callback, 154 int trafficTag, 155 List<ExtraHttpHeader> extraHttpHeaders) { 156 return PropagatedFutures.transformAsync( 157 getInProgressFuture(fileKey, fileUri), 158 inProgressFuture -> { 159 if (inProgressFuture.isPresent()) { 160 return inProgressFuture.get(); 161 } 162 return addCallbackAndRegister( 163 fileKey, 164 fileUri, 165 callback, 166 unused -> 167 startDownloadingInternal( 168 groupKey, 169 fileGroupVersionNumber, 170 buildId, 171 variantId, 172 fileUri, 173 urlToDownload, 174 fileSize, 175 downloadConditions, 176 trafficTag, 177 extraHttpHeaders)); 178 }, 179 sequentialControlExecutor); 180 } 181 182 /** 183 * Adds Callback to given Future and Registers future in in-progress cache. 184 * 185 * <p>Contains shared logic of connecting {@code callback} to {@code downloadOrCopyFunction} and 186 * registers future in the internal in-progress cache. This cache allows similar download/copy 187 * requests to be deduped instead of being performed twice. 188 * 189 * <p>NOTE: this method assumes the cache has already been checked for an in-progress operation 190 * and no in-progress operation exists for {@code fileUri}. 191 * 192 * @param fileKey key that identifies the shared file. 193 * @param fileUri the destination of the download/copy (used as Key in in-progress cache) 194 * @param callback the callback that should be run after the given download/copy future 195 * @param downloadOrCopyFunction an AsyncFunction that will perform the download/copy 196 * @return a ListenableFuture that calls the correct callback after {@code downloadOrCopyFuture 197 * completes} 198 */ 199 private ListenableFuture<Void> addCallbackAndRegister( 200 String fileKey, 201 Uri fileUri, 202 DownloaderCallback callback, 203 AsyncFunction<Void, Void> downloadOrCopyFunction) { 204 // Use ListenableFutureTask to create a future without starting it. This ensures we can 205 // successfully add our future to download/copy before the operation starts. 206 ListenableFutureTask<Void> startTask = ListenableFutureTask.create(() -> null); 207 208 // Use transform & catching to ensure that we correctly chain everything. 209 PropagatedFluentFuture<Void> downloadOrCopyFuture = 210 PropagatedFluentFuture.from(startTask) 211 .transformAsync(downloadOrCopyFunction, sequentialControlExecutor) 212 .transformAsync( 213 voidArg -> callback.onDownloadComplete(fileUri), 214 sequentialControlExecutor /*Run callbacks on @SequentialControlExecutor*/) 215 .catchingAsync( 216 Exception.class, 217 e -> 218 // Rethrow exception so the failure is passed back up the future chain. 219 PropagatedFutures.transformAsync( 220 callback.onDownloadFailed(asDownloadException(e)), 221 voidArg -> { 222 throw e; 223 }, 224 sequentialControlExecutor), 225 sequentialControlExecutor /*Run callbacks on @SequentialControlExecutor*/); 226 227 // Add this future to the future map, then start startTask to unblock download/copy. The order 228 // ensures that the download/copy happens only if we were able to add the future to the map. 229 PropagatedFluentFuture<Void> transformedFuture = 230 PropagatedFluentFuture.from(addFutureToMap(downloadOrCopyFuture, fileKey, fileUri)) 231 .transformAsync( 232 unused -> { 233 startTask.run(); 234 return downloadOrCopyFuture; 235 }, 236 sequentialControlExecutor); 237 238 // We want to remove the future from the cache when the transformedFuture finishes. 239 // However there may be a race condition and transformedFuture may finish before we put it into 240 // the cache. 241 // To prevent this race condition, we add a callback to transformedFuture to make sure the 242 // removal happens after the putting it in the map. 243 // A transform would not work since we want to run the removal even when the transform failed. 244 transformedFuture.addListener( 245 () -> { 246 ListenableFuture<Void> unused = removeFutureFromMap(fileKey, fileUri); 247 }, 248 sequentialControlExecutor); 249 250 return transformedFuture; 251 } 252 253 private ListenableFuture<Void> addFutureToMap( 254 ListenableFuture<Void> downloadOrCopyFuture, String fileKey, Uri fileUri) { 255 if (!flags.enableFileDownloadDedupByFileKey()) { 256 fileUriToDownloadFutureMap.put(fileUri, downloadOrCopyFuture); 257 return immediateVoidFuture(); 258 } else { 259 return downloadOrCopyFutureMap.add(fileKey, downloadOrCopyFuture); 260 } 261 } 262 263 private ListenableFuture<Void> removeFutureFromMap(String fileKey, Uri fileUri) { 264 if (!flags.enableFileDownloadDedupByFileKey()) { 265 // Return the removed future if it exists, otherwise return immediately (Extra check added to 266 // satisfy nullness checker). 267 ListenableFuture<Void> removedFuture = fileUriToDownloadFutureMap.remove(fileUri); 268 if (removedFuture != null) { 269 return removedFuture; 270 } 271 return immediateVoidFuture(); 272 } else { 273 return downloadOrCopyFutureMap.remove(fileKey); 274 } 275 } 276 277 private ListenableFuture<Void> startDownloadingInternal( 278 GroupKey groupKey, 279 int fileGroupVersionNumber, 280 long buildId, 281 String variantId, 282 Uri fileUri, 283 String urlToDownload, 284 int fileSize, 285 @Nullable DownloadConditions downloadConditions, 286 int trafficTag, 287 List<ExtraHttpHeader> extraHttpHeaders) { 288 if (urlToDownload.startsWith("http") 289 && flags.downloaderEnforceHttps() 290 && !urlToDownload.startsWith("https")) { 291 LogUtil.e("%s: File url = %s is not secure", TAG, urlToDownload); 292 return immediateFailedFuture( 293 DownloadException.builder() 294 .setDownloadResultCode(DownloadResultCode.INSECURE_URL_ERROR) 295 .build()); 296 } 297 298 long currentFileSize = 0; 299 try { 300 currentFileSize = fileStorage.fileSize(fileUri); 301 } catch (IOException e) { 302 // Proceed with 0 as the current file size. It is only used for deciding whether we should 303 // download the file or not. 304 } 305 306 try { 307 checkStorageConstraints( 308 context, urlToDownload, fileSize - currentFileSize, downloadConditions, flags); 309 } catch (DownloadException e) { 310 // Wrap exception in future to break future chain. 311 LogUtil.e("%s: Not enough space to download file %s", TAG, urlToDownload); 312 return immediateFailedFuture(e); 313 } 314 315 if (flags.logNetworkStats()) { 316 networkUsageMonitor.monitorUri( 317 fileUri, groupKey, buildId, variantId, fileGroupVersionNumber, loggingStateStore); 318 } else { 319 LogUtil.w("%s: NetworkUsageMonitor is disabled", TAG); 320 } 321 322 if (downloadMonitorOptional.isPresent()) { 323 downloadMonitorOptional.get().monitorUri(fileUri, groupKey.getGroupName()); 324 } 325 326 DownloadRequest.Builder downloadRequestBuilder = 327 DownloadRequest.newBuilder().setFileUri(fileUri).setUrlToDownload(urlToDownload); 328 329 // TODO: consider to do this conversion upstream and we can pass in the 330 // DownloadConstraints. 331 if (downloadConditions != null 332 && downloadConditions.getDeviceNetworkPolicy() 333 == DeviceNetworkPolicy.DOWNLOAD_ON_ANY_NETWORK) { 334 downloadRequestBuilder.setDownloadConstraints(DownloadConstraints.NETWORK_CONNECTED); 335 } else { 336 downloadRequestBuilder.setDownloadConstraints(DownloadConstraints.NETWORK_UNMETERED); 337 } 338 339 if (trafficTag > 0) { 340 downloadRequestBuilder.setTrafficTag(trafficTag); 341 } 342 343 ImmutableList.Builder<Pair<String, String>> headerBuilder = ImmutableList.builder(); 344 for (ExtraHttpHeader header : extraHttpHeaders) { 345 headerBuilder.add(Pair.create(header.getKey(), header.getValue())); 346 } 347 348 downloadRequestBuilder.setExtraHttpHeaders(headerBuilder.build()); 349 350 return fileDownloaderSupplier.get().startDownloading(downloadRequestBuilder.build()); 351 } 352 353 /** 354 * Gets an in-progress future (if it exists), otherwise returns absent. 355 * 356 * <p>This method allows easier deduplication of file downloads/copies, by allowing callers to 357 * query against the internal download future map. This method is assumed to be called when a 358 * SharedFile state is DOWNLOAD_IN_PROGRESS. 359 * 360 * @param fileKey key that identifies the shared file. 361 * @param fileUri - the File Uri to download the file at. 362 * @return - ListenableFuture representing an in-progress download/copy for the given file. 363 */ 364 public ListenableFuture<Optional<ListenableFuture<Void>>> getInProgressFuture( 365 String fileKey, Uri fileUri) { 366 if (!flags.enableFileDownloadDedupByFileKey()) { 367 return immediateFuture(Optional.fromNullable(fileUriToDownloadFutureMap.get(fileUri))); 368 } else { 369 return downloadOrCopyFutureMap.get(fileKey); 370 } 371 } 372 373 /** 374 * Start Copying a file to internal storage 375 * 376 * @param fileKey key that identifies the shared file to copy. 377 * @param fileUri the File Uri where content should be copied. 378 * @param urlToDownload the url to copy, should be inlinefile: scheme. 379 * @param fileSize the size of the file to copy. 380 * @param downloadConditions conditions under which this file should be copied. 381 * @param downloaderCallback callback called when the copy either completes or fails. 382 * @param inlineFileSource Source of file content to copy. 383 * @return ListenableFuture representing the result of a file copy. 384 */ 385 public ListenableFuture<Void> startCopying( 386 String fileKey, 387 Uri fileUri, 388 String urlToDownload, 389 int fileSize, 390 @Nullable DownloadConditions downloadConditions, 391 DownloaderCallback downloaderCallback, 392 FileSource inlineFileSource) { 393 return PropagatedFutures.transformAsync( 394 getInProgressFuture(fileKey, fileUri), 395 inProgressFuture -> { 396 if (inProgressFuture.isPresent()) { 397 return inProgressFuture.get(); 398 } 399 return addCallbackAndRegister( 400 fileKey, 401 fileUri, 402 downloaderCallback, 403 unused -> 404 startCopyingInternal( 405 fileUri, urlToDownload, fileSize, downloadConditions, inlineFileSource)); 406 }, 407 sequentialControlExecutor); 408 } 409 410 private ListenableFuture<Void> startCopyingInternal( 411 Uri fileUri, 412 String urlToCopy, 413 int fileSize, 414 @Nullable DownloadConditions downloadConditions, 415 FileSource inlineFileSource) { 416 417 int finalFileSize = fileSize; 418 if (inlineFileSource.getKind().equals(FileSource.Kind.BYTESTRING)) { 419 int sourceFileSize = inlineFileSource.byteString().size(); 420 if (sourceFileSize != fileSize) { 421 LogUtil.w( 422 "%s: expected file size (%d) does not match source file size (%d) -- using source file" 423 + " size for storage check; file: %s", 424 TAG, fileSize, sourceFileSize, urlToCopy); 425 finalFileSize = sourceFileSize; 426 } 427 } 428 429 try { 430 checkStorageConstraints(context, urlToCopy, finalFileSize, downloadConditions, flags); 431 } catch (DownloadException e) { 432 // Wrap exception in future to break future chain. 433 LogUtil.e("%s: Not enough space to download file %s", TAG, urlToCopy); 434 return immediateFailedFuture(e); 435 } 436 437 // TODO(b/177361344): Only monitor file if download listener is supported 438 439 DownloadRequest downloadRequest = 440 DownloadRequest.newBuilder() 441 .setUrlToDownload(urlToCopy) 442 .setFileUri(fileUri) 443 .setInlineDownloadParamsOptional( 444 InlineDownloadParams.newBuilder().setInlineFileContent(inlineFileSource).build()) 445 .build(); 446 447 // Use file download supplier to perform inline file download 448 return fileDownloaderSupplier.get().startDownloading(downloadRequest); 449 } 450 451 /** 452 * Stop downloading the file. 453 * 454 * @param fileKey - key that identifies the file to stop downloading. 455 * @param fileUri - the File Uri of the file to stop downloading. 456 */ 457 public void stopDownloading(String fileKey, Uri fileUri) { 458 ListenableFuture<Void> unused = 459 PropagatedFutures.transformAsync( 460 getInProgressFuture(fileKey, fileUri), 461 inProgressFuture -> { 462 if (inProgressFuture.isPresent()) { 463 LogUtil.d("%s: Cancel download file %s", TAG, fileUri); 464 inProgressFuture.get().cancel(/* mayInterruptIfRunning= */ true); 465 return removeFutureFromMap(fileKey, fileUri); 466 } else { 467 LogUtil.w("%s: stopDownloading on non-existent download", TAG); 468 return immediateVoidFuture(); 469 } 470 }, 471 sequentialControlExecutor); 472 } 473 474 /** 475 * Checks if storage constraints are enabled and if so, performs storage check. 476 * 477 * <p>If low storage enforcement is enabled, this method will check if a file with {@code 478 * bytesNeeded} can be stored on disk without hitting the storage threshold defined in {@code 479 * downloadConditions}. 480 * 481 * <p>If low storage enforcement is not enabled, this method is a no-op. 482 * 483 * <p>If {@code bytesNeeded} does hit the given storage threshold, a {@link DownloadException} 484 * will be thrown with the {@code DownloadResultCode.LOW_DISK_ERROR} error code. 485 * 486 * @param context Context in which storage should be checked 487 * @param bytesNeeded expected size of the file to store on disk 488 * @param downloadConditions conditions that contain the type of storage threshold to check 489 * @throws DownloadException when storing a file with the given size would hit the given storage 490 * thresholds 491 */ 492 private static void checkStorageConstraints( 493 Context context, 494 String url, 495 long bytesNeeded, 496 @Nullable DownloadConditions downloadConditions, 497 Flags flags) 498 throws DownloadException { 499 if (flags.enforceLowStorageBehavior() 500 && !shouldDownload(context, url, bytesNeeded, downloadConditions, flags)) { 501 throw DownloadException.builder() 502 .setDownloadResultCode(DownloadResultCode.LOW_DISK_ERROR) 503 .build(); 504 } 505 } 506 507 /** 508 * This calculates if the file should be downloaded. It checks that after download you have at 509 * least a certain fraction of free space or an absolute minimum space still available. 510 * 511 * <p>This is in parity with what the DownloadApi does- <internal> 512 */ 513 private static boolean shouldDownload( 514 Context context, 515 String url, 516 long bytesNeeded, 517 @Nullable DownloadConditions downloadConditions, 518 Flags flags) { 519 // If we are using a placeholder (inline file + 0 byte size), bypass storage checks. 520 if (FileGroupUtil.isInlineFile(url) && bytesNeeded == 0L) { 521 return true; 522 } 523 524 StatFs stats = new StatFs(context.getFilesDir().getAbsolutePath()); 525 526 long totalBytes = (long) stats.getBlockCount() * stats.getBlockSize(); 527 long freeBytes = (long) stats.getAvailableBlocks() * stats.getBlockSize(); 528 529 double remainingBytesAfterDownload = freeBytes - bytesNeeded; 530 531 double minBytes = 532 min(totalBytes * flags.fractionFreeSpaceAfterDownload(), flags.absFreeSpaceAfterDownload()); 533 534 if (downloadConditions != null) { 535 switch (downloadConditions.getDeviceStoragePolicy()) { 536 case BLOCK_DOWNLOAD_LOWER_THRESHOLD: 537 minBytes = 538 min( 539 totalBytes * flags.fractionFreeSpaceAfterDownload(), 540 flags.absFreeSpaceAfterDownloadLowStorageAllowed()); 541 break; 542 543 case EXTREMELY_LOW_THRESHOLD: 544 minBytes = 545 min( 546 totalBytes * flags.fractionFreeSpaceAfterDownload(), 547 flags.absFreeSpaceAfterDownloadExtremelyLowStorageAllowed()); 548 break; 549 default: 550 // fallthrough. 551 } 552 } 553 554 return remainingBytesAfterDownload > minBytes; 555 } 556 557 /** 558 * Wraps throwable as DownloadException if it isn't one already. 559 * 560 * <p>This method doesn't check the incoming throwable besides the type and defaults the download 561 * result code to UNKNOWN_ERROR. 562 */ 563 private static DownloadException asDownloadException(Throwable t) { 564 if (t instanceof DownloadException) { 565 return (DownloadException) t; 566 } 567 568 return DownloadException.builder() 569 .setCause(t) 570 .setDownloadResultCode(DownloadResultCode.UNKNOWN_ERROR) 571 .build(); 572 } 573 574 /** Interface called by the downloader when download either completes or fails. */ 575 public static interface DownloaderCallback { 576 /** Called on download complete. */ 577 // TODO(b/123424546): Consider to drop fileUri. 578 ListenableFuture<Void> onDownloadComplete(Uri fileUri); 579 580 /** Called on download failed. */ 581 ListenableFuture<Void> onDownloadFailed(DownloadException exception); 582 } 583 } 584