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.testing; 17 18 import android.accounts.Account; 19 import android.net.Uri; 20 import androidx.test.core.app.ApplicationProvider; 21 import com.google.mobiledatadownload.internal.MetadataProto.GroupKey; 22 import com.google.android.libraries.mobiledatadownload.AddFileGroupRequest; 23 import com.google.android.libraries.mobiledatadownload.DownloadException; 24 import com.google.android.libraries.mobiledatadownload.DownloadException.DownloadResultCode; 25 import com.google.android.libraries.mobiledatadownload.DownloadFileGroupRequest; 26 import com.google.android.libraries.mobiledatadownload.GetFileGroupRequest; 27 import com.google.android.libraries.mobiledatadownload.GetFileGroupsByFilterRequest; 28 import com.google.android.libraries.mobiledatadownload.ImportFilesRequest; 29 import com.google.android.libraries.mobiledatadownload.MobileDataDownload; 30 import com.google.android.libraries.mobiledatadownload.ReadDataFileGroupRequest; 31 import com.google.android.libraries.mobiledatadownload.RemoveFileGroupRequest; 32 import com.google.android.libraries.mobiledatadownload.RemoveFileGroupsByFilterRequest; 33 import com.google.android.libraries.mobiledatadownload.RemoveFileGroupsByFilterResponse; 34 import com.google.android.libraries.mobiledatadownload.SingleFileDownloadRequest; 35 import com.google.android.libraries.mobiledatadownload.TaskScheduler.ConstraintOverrides; 36 import com.google.android.libraries.mobiledatadownload.UsageEvent; 37 import com.google.android.libraries.mobiledatadownload.account.AccountUtil; 38 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage; 39 import com.google.android.libraries.mobiledatadownload.file.backends.AndroidUri; 40 import com.google.android.libraries.mobiledatadownload.file.openers.WriteStreamOpener; 41 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures; 42 import com.google.common.base.Optional; 43 import com.google.common.collect.HashBasedTable; 44 import com.google.common.collect.ImmutableList; 45 import com.google.common.collect.Iterables; 46 import com.google.common.collect.Table; 47 import com.google.common.util.concurrent.Futures; 48 import com.google.common.util.concurrent.ListenableFuture; 49 import com.google.common.util.concurrent.MoreExecutors; 50 import com.google.mobiledatadownload.ClientConfigProto.ClientFile; 51 import com.google.mobiledatadownload.ClientConfigProto.ClientFileGroup; 52 import com.google.mobiledatadownload.DownloadConfigProto.DataFile; 53 import com.google.mobiledatadownload.DownloadConfigProto.DataFileGroup; 54 import java.io.IOException; 55 import java.io.OutputStream; 56 import java.util.ArrayList; 57 import java.util.EnumMap; 58 import java.util.HashMap; 59 import java.util.List; 60 import java.util.Map; 61 import java.util.concurrent.Executor; 62 63 /** 64 * Fake implementation of {@link MobileDataDownload}. 65 * 66 * <p>FakeMobileDataDownload is thread-safe. All the apis part of MobileDataDownload interface can 67 * be invoked from multiple threads safely. Thread safety for helper functions (like setUpFileGroup, 68 * setThrowable, setThrowableOnFileGroup, get*Params apis etc) is not provided. To avoid race 69 * conditions, all the set up functions should be invoked at the beginning of the test before 70 * testing the business logic and get*Params apis should be invoked only after all the pending tasks 71 * are done. Refer <internal> to wait for all the pending background asynchronous tasks to complete. 72 */ 73 public final class FakeMobileDataDownload implements MobileDataDownload { 74 75 // private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); 76 77 private final List<AddFileGroupRequest> addFileGroupParamsList = new ArrayList<>(); 78 private final List<ClientFileGroup> downloadedFileGroupList = new ArrayList<>(); 79 private final List<GetFileGroupRequest> getFileGroupParamsList = new ArrayList<>(); 80 private final List<String> handleTaskParamsList = new ArrayList<>(); 81 private final List<ClientFileGroup> pendingFileGroupList = new ArrayList<>(); 82 private final Map<MethodType, Throwable> throwableMap = new EnumMap<>(MethodType.class); 83 private final Table<MethodType, GroupKey, Throwable> methodTypeGroupKeyToThrowableTable = 84 HashBasedTable.create(); 85 private final List<DownloadFileGroupRequest> downloadFileGroupParamsList = new ArrayList<>(); 86 private final List<DownloadFileGroupRequest> downloadFileGroupWithForegroundServiceParamsList = 87 new ArrayList<>(); 88 private final List<RemoveFileGroupRequest> removeFileGroupParamsList = new ArrayList<>(); 89 private final Map<String, byte[]> remoteFilesMap = new HashMap<>(); 90 91 private final Optional<SynchronousFileStorage> storageOptional; 92 private final Executor sequentialControlExecutor; 93 94 /** Enum for different MDD methods. Used to set Throwable. */ 95 public enum MethodType { 96 ADD_FILE_GROUP, 97 GET_FILE_GROUP, 98 REMOVE_FILE_GROUP, 99 DOWNLOAD_FILE, 100 DOWNLOAD_FILE_FOREGROUND, 101 } 102 103 /** {@code storageOptional} must be present to download files set through setUpRemoteFile. */ FakeMobileDataDownload(Optional<SynchronousFileStorage> storageOptional, Executor executor)104 FakeMobileDataDownload(Optional<SynchronousFileStorage> storageOptional, Executor executor) { 105 this.storageOptional = storageOptional; 106 this.sequentialControlExecutor = MoreExecutors.newSequentialExecutor(executor); 107 } 108 createFakeMddWithFileStorage( SynchronousFileStorage storage)109 public static FakeMobileDataDownload createFakeMddWithFileStorage( 110 SynchronousFileStorage storage) { 111 return new FakeMobileDataDownload( 112 Optional.of(storage), MoreExecutors.newSequentialExecutor(MoreExecutors.directExecutor())); 113 } 114 getMatchingFileGroups( GroupKey groupKey, List<ClientFileGroup> fileGroupList)115 private static List<ClientFileGroup> getMatchingFileGroups( 116 GroupKey groupKey, List<ClientFileGroup> fileGroupList) { 117 // logger.atConfig().log("#getMatchingFileGroups: %s, %s", groupKey, fileGroupList); 118 List<ClientFileGroup> filteredFileGroupList = new ArrayList<>(); 119 for (ClientFileGroup fileGroup : fileGroupList) { 120 // Check for group name match. 121 if (groupKey.hasGroupName() && !groupKey.getGroupName().equals(fileGroup.getGroupName())) { 122 continue; 123 } 124 125 // Check for owner_package match. 126 if (groupKey.hasOwnerPackage() 127 && !groupKey.getOwnerPackage().equals(fileGroup.getOwnerPackage())) { 128 continue; 129 } 130 131 // Check for account match. 132 if (groupKey.hasAccount() && !groupKey.getAccount().equals(fileGroup.getAccount())) { 133 continue; 134 } 135 136 // Check for variant id match. 137 if (groupKey.hasVariantId() && !groupKey.getVariantId().equals(fileGroup.getVariantId())) { 138 continue; 139 } 140 141 filteredFileGroupList.add(fileGroup); 142 } 143 144 return filteredFileGroupList; 145 } 146 147 /** 148 * Sets {@link ClientFileGroup} instance to use in getFileGroup, getFileGroupsByFilter and 149 * downloadFileGroup methods. 150 * 151 * <p>getFileGroup, getFileGroupsByFilter, downloadFileGroup methods will look for a match in all 152 * the file groups set using this api before returning the result. 153 * 154 * @param clientFileGroup ClientFileGroup instance. 155 * @param downloaded if true, assumes the ClientFileGroup instance is downloaded, else download is 156 * pending. 157 */ setUpFileGroup(ClientFileGroup clientFileGroup, boolean downloaded)158 public void setUpFileGroup(ClientFileGroup clientFileGroup, boolean downloaded) { 159 if (downloaded) { 160 downloadedFileGroupList.add( 161 clientFileGroup.toBuilder().setStatus(ClientFileGroup.Status.DOWNLOADED).build()); 162 } else { 163 pendingFileGroupList.add( 164 clientFileGroup.toBuilder().setStatus(ClientFileGroup.Status.PENDING).build()); 165 } 166 } 167 168 /** 169 * Returns the list of parameters that addFileGroup method was invocated with. 170 * 171 * @return List of all the requests of type {@link AddFileGroupRequest} that addFileGroup method 172 * was called with. 173 */ getAddFileGroupParamsList()174 public ImmutableList<AddFileGroupRequest> getAddFileGroupParamsList() { 175 return ImmutableList.copyOf(addFileGroupParamsList); 176 } 177 178 /** 179 * Returns the list of parameters that removeFileGroup method was invocated with. 180 * 181 * @return List of all the requests of type {@link RemoveFileGroupRequest} that removeFileGroup 182 * method was called with. 183 */ getRemoveFileGroupParamsList()184 public ImmutableList<RemoveFileGroupRequest> getRemoveFileGroupParamsList() { 185 return ImmutableList.copyOf(removeFileGroupParamsList); 186 } 187 188 /** 189 * Returns the list of parameters that downloadFileGroup method was invocated with. 190 * 191 * @return List of all the requests of type {@link DownloadFileGroupRequest} that 192 * downloadFileGroup method was called with. 193 */ getDownloadFileGroupParamsList()194 public ImmutableList<DownloadFileGroupRequest> getDownloadFileGroupParamsList() { 195 return ImmutableList.copyOf(downloadFileGroupParamsList); 196 } 197 198 /** 199 * Returns the list of parameters that downloadFileGroupWithForegroundService method was invocated 200 * with. 201 * 202 * @return List of all the requests of type {@link DownloadFileGroupRequest} that 203 * downloadFileGroup method was called with. 204 */ 205 public ImmutableList<DownloadFileGroupRequest> getDownloadFileGroupWithForegroundServiceParamsList()206 getDownloadFileGroupWithForegroundServiceParamsList() { 207 return ImmutableList.copyOf(downloadFileGroupWithForegroundServiceParamsList); 208 } 209 210 /** 211 * Returns the list of parameters that getFileGroup method was invocated with. 212 * 213 * @return List of all the requests of type {@link GetFileGroupRequest} that getFileGroup method 214 * was called with. 215 */ getGetFileGroupParamsList()216 public ImmutableList<GetFileGroupRequest> getGetFileGroupParamsList() { 217 return ImmutableList.copyOf(getFileGroupParamsList); 218 } 219 220 /** Returns the list of parameters that handleTask method was invocated with. */ getHandleTaskParamsList()221 public ImmutableList<String> getHandleTaskParamsList() { 222 return ImmutableList.copyOf(handleTaskParamsList); 223 } 224 225 /** 226 * Sets {@code throwable} to throw on invocation of a method identified by {@code methodType} 227 * 228 * @param methodType enum to identify method. 229 * @param throwable Throwable to throw on method's invocation. 230 */ setThrowable(MethodType methodType, Throwable throwable)231 public void setThrowable(MethodType methodType, Throwable throwable) { 232 this.throwableMap.put(methodType, throwable); 233 } 234 235 /** 236 * Sets {@code throwable} to throw on invocation of method identified by {@code methodType} when 237 * the properties set using {@code groupName}, {@code variantIdOptional}, {@code accountOptional} 238 * matches with the filegroup on which the method is invoked. 239 * 240 * @param methodType enum to identify method. 241 * @param groupName Name of the file group. 242 * @param accountOptional Account of the file group. Setting this is optional. 243 * @param variantIdOptional Variant Id of the file group. Setting this is optional. 244 * @param throwable Throwable to throw. 245 * <p>If throwable is set using both #setThrowable and #setThrowableOnFileGroup for a method, 246 * priority is given to throwable set through the latter. 247 */ setThrowableOnFileGroup( MethodType methodType, String groupName, Optional<Account> accountOptional, Optional<String> variantIdOptional, Throwable throwable)248 public void setThrowableOnFileGroup( 249 MethodType methodType, 250 String groupName, 251 Optional<Account> accountOptional, 252 Optional<String> variantIdOptional, 253 Throwable throwable) { 254 if (methodType != MethodType.GET_FILE_GROUP) { 255 throw new IllegalArgumentException( 256 "setThrowableOnFileGroup is currently only supported for getFileGroup method."); 257 } 258 GroupKey.Builder groupKeyBuilder = GroupKey.newBuilder(); 259 groupKeyBuilder.setGroupName(groupName); 260 if (accountOptional.isPresent()) { 261 groupKeyBuilder.setAccount(AccountUtil.serialize(accountOptional.get())); 262 } 263 if (variantIdOptional.isPresent()) { 264 groupKeyBuilder.setVariantId(variantIdOptional.get()); 265 } 266 methodTypeGroupKeyToThrowableTable.put(methodType, groupKeyBuilder.build(), throwable); 267 } 268 269 /** 270 * Set file corresponding to a url. 271 * 272 * <p>Used by downloadFile and downloadFileWithForegroundService. If the 273 * SingleFileDownloadRequest#urlToDownload matches any of the set url, file is created at 274 * SingleFileDownloadRequest#destinationFileUri with the corresponding set content. 275 * 276 * <p>Setting content for an already existing url will replace the existing contents. 277 */ setUpRemoteFile(String urlToDownload, byte[] content)278 public void setUpRemoteFile(String urlToDownload, byte[] content) { 279 // NOTE: If a client is using AssetFileBackend, then the corresponding test assets can be 280 // used here if the parameter type is Uri instead of byte[]. 281 // Note: Here byte[] will be stored in memory. Uri avoids this and supports large files cleanly. 282 remoteFilesMap.put(urlToDownload, content); 283 } 284 285 @Override addFileGroup(AddFileGroupRequest addFileGroupRequest)286 public ListenableFuture<Boolean> addFileGroup(AddFileGroupRequest addFileGroupRequest) { 287 // logger.atInfo().log("#addFileGroup: %s", addFileGroupRequest); 288 Throwable addFileGroupThrowable = throwableMap.get(MethodType.ADD_FILE_GROUP); 289 if (addFileGroupThrowable != null) { 290 return Futures.immediateFailedFuture(addFileGroupThrowable); 291 } 292 addFileGroupParamsList.add(addFileGroupRequest); 293 294 // Let addFileGroup induce realistic behavior. 295 // Wrap in background executor because this might do disk reads. 296 return PropagatedFutures.submitAsync( 297 () -> { 298 setUpFileGroup(toClientFileGroup(addFileGroupRequest), false); 299 return Futures.immediateFuture(true); 300 }, 301 sequentialControlExecutor); 302 } 303 toClientFileGroup(AddFileGroupRequest addFileGroupRequest)304 private ClientFileGroup toClientFileGroup(AddFileGroupRequest addFileGroupRequest) { 305 ClientFileGroup.Builder clientFileGroupBuilder = 306 ClientFileGroup.newBuilder() 307 .setGroupName(addFileGroupRequest.dataFileGroup().getGroupName()); 308 if (addFileGroupRequest.accountOptional().isPresent()) { 309 clientFileGroupBuilder.setAccount( 310 AccountUtil.serialize(addFileGroupRequest.accountOptional().get())); 311 } 312 if (addFileGroupRequest.dataFileGroup().hasOwnerPackage()) { 313 clientFileGroupBuilder.setOwnerPackage(addFileGroupRequest.dataFileGroup().getOwnerPackage()); 314 } 315 if (addFileGroupRequest.variantIdOptional().isPresent()) { 316 clientFileGroupBuilder.setVariantId(addFileGroupRequest.variantIdOptional().get()); 317 } 318 for (DataFile dataFile : addFileGroupRequest.dataFileGroup().getFileList()) { 319 ClientFile.Builder clientFileBuilder = 320 ClientFile.newBuilder().setFileId(dataFile.getFileId()); 321 if (dataFile.hasUrlToDownload()) { 322 String urlToDownload = dataFile.getUrlToDownload(); 323 clientFileBuilder.setFileUri(getMobstoreUriForRemoteFile(urlToDownload).toString()); 324 maybeSetUpFileAtUri(urlToDownload); 325 } 326 clientFileGroupBuilder.addFile(clientFileBuilder); 327 } 328 329 return clientFileGroupBuilder.build(); 330 } 331 maybeSetUpFileAtUri(String urlToDownload)332 private void maybeSetUpFileAtUri(String urlToDownload) { 333 if (storageOptional.isPresent() && remoteFilesMap.containsKey(urlToDownload)) { 334 try { 335 Uri mobstoreUri = getMobstoreUriForRemoteFile(urlToDownload); 336 storageOptional 337 .get() 338 .open(mobstoreUri, WriteStreamOpener.create()) 339 .write(remoteFilesMap.get(urlToDownload)); 340 // logger.atInfo().log( 341 // "Writing file for URL %s to Mobstore URI: %s", urlToDownload, mobstoreUri); 342 } catch (IOException e) { 343 // logger.atSevere().withCause(e).log("Mobstore file write failed"); 344 } 345 } else { 346 // logger.atConfig().log( 347 // "No file set for %s. Consider using #setUpRemoteFile if a download is requested.", 348 // urlToDownload); 349 } 350 } 351 getMobstoreUriForRemoteFile(String urlToDownload)352 private static Uri getMobstoreUriForRemoteFile(String urlToDownload) { 353 return AndroidUri.builder(ApplicationProvider.getApplicationContext()) 354 .setModule("fakemddtest") 355 .setRelativePath(String.valueOf(Integer.valueOf(urlToDownload.hashCode()))) 356 .build(); 357 } 358 359 @Override removeFileGroup(RemoveFileGroupRequest removeFileGroupRequest)360 public ListenableFuture<Boolean> removeFileGroup(RemoveFileGroupRequest removeFileGroupRequest) { 361 Throwable removeFileGroupThrowable = throwableMap.get(MethodType.REMOVE_FILE_GROUP); 362 if (removeFileGroupThrowable != null) { 363 return Futures.immediateFailedFuture(removeFileGroupThrowable); 364 } 365 removeFileGroupParamsList.add(removeFileGroupRequest); 366 return PropagatedFutures.submitAsync( 367 () -> Futures.immediateFuture(true), sequentialControlExecutor); 368 } 369 370 @Override removeFileGroupsByFilter( RemoveFileGroupsByFilterRequest removeFileGroupsByFilterRequest)371 public ListenableFuture<RemoveFileGroupsByFilterResponse> removeFileGroupsByFilter( 372 RemoveFileGroupsByFilterRequest removeFileGroupsByFilterRequest) { 373 return PropagatedFutures.submitAsync( 374 () -> 375 Futures.immediateFuture( 376 RemoveFileGroupsByFilterResponse.newBuilder().setRemovedFileGroupsCount(0).build()), 377 sequentialControlExecutor); 378 } 379 380 @Override readDataFileGroup( ReadDataFileGroupRequest readDataFileGroupRequest)381 public ListenableFuture<DataFileGroup> readDataFileGroup( 382 ReadDataFileGroupRequest readDataFileGroupRequest) { 383 return Futures.immediateFailedFuture(new UnsupportedOperationException()); 384 } 385 386 @Override getFileGroup(GetFileGroupRequest getFileGroupRequest)387 public ListenableFuture<ClientFileGroup> getFileGroup(GetFileGroupRequest getFileGroupRequest) { 388 // Construct GroupKey from getFileGroupRequest. 389 GroupKey.Builder groupKeyBuilder = GroupKey.newBuilder(); 390 groupKeyBuilder.setGroupName(getFileGroupRequest.groupName()); 391 if (getFileGroupRequest.accountOptional().isPresent()) { 392 groupKeyBuilder.setAccount( 393 AccountUtil.serialize(getFileGroupRequest.accountOptional().get())); 394 } 395 if (getFileGroupRequest.variantIdOptional().isPresent()) { 396 groupKeyBuilder.setVariantId(getFileGroupRequest.variantIdOptional().get()); 397 } 398 GroupKey groupKey = groupKeyBuilder.build(); 399 400 // Throw exception if a throwable is set. 401 Throwable getFileGroupThrowable = 402 methodTypeGroupKeyToThrowableTable.get(MethodType.GET_FILE_GROUP, groupKey); 403 if (getFileGroupThrowable == null) { 404 getFileGroupThrowable = throwableMap.get(MethodType.GET_FILE_GROUP); 405 } 406 if (getFileGroupThrowable != null) { 407 return Futures.immediateFailedFuture(getFileGroupThrowable); 408 } 409 getFileGroupParamsList.add(getFileGroupRequest); 410 return PropagatedFutures.submitAsync( 411 () -> { 412 List<ClientFileGroup> fileGroupList = 413 getMatchingFileGroups(groupKeyBuilder.build(), downloadedFileGroupList); 414 return Futures.immediateFuture(Iterables.getFirst(fileGroupList, null)); 415 }, 416 sequentialControlExecutor); 417 } 418 419 @Override 420 public ListenableFuture<ImmutableList<ClientFileGroup>> getFileGroupsByFilter( 421 GetFileGroupsByFilterRequest getFileGroupsByFilterRequest) { 422 return PropagatedFutures.submitAsync( 423 () -> { 424 List<ClientFileGroup> allFileGroups = new ArrayList<>(downloadedFileGroupList); 425 allFileGroups.addAll(pendingFileGroupList); 426 427 if (getFileGroupsByFilterRequest.includeAllGroups()) { 428 return Futures.immediateFuture(ImmutableList.copyOf(allFileGroups)); 429 } 430 431 GroupKey.Builder groupKeyBuilder = GroupKey.newBuilder(); 432 if (getFileGroupsByFilterRequest.groupNameOptional().isPresent()) { 433 groupKeyBuilder.setGroupName(getFileGroupsByFilterRequest.groupNameOptional().get()); 434 } 435 if (getFileGroupsByFilterRequest.accountOptional().isPresent()) { 436 groupKeyBuilder.setAccount( 437 AccountUtil.serialize(getFileGroupsByFilterRequest.accountOptional().get())); 438 } 439 440 return Futures.immediateFuture( 441 ImmutableList.copyOf(getMatchingFileGroups(groupKeyBuilder.build(), allFileGroups))); 442 }, 443 sequentialControlExecutor); 444 } 445 446 @Override 447 public ListenableFuture<Void> importFiles(ImportFilesRequest importFilesRequest) { 448 return Futures.immediateVoidFuture(); 449 } 450 451 /** 452 * If a file is set using setUpRemoteFile for {@code urlToDownload}, the contents will be copied 453 * to {@code destinationFileUri}. 454 */ 455 private void downloadFileIfSet(String urlToDownload, Uri destinationFileUri) throws IOException { 456 if (!remoteFilesMap.containsKey(urlToDownload)) { 457 // logger.atWarning().log( 458 // "No file set for %s using setUpRemoteFile. Download request is a no-op.", urlToDownload); 459 return; 460 } 461 462 if (!storageOptional.isPresent()) { 463 // logger.atSevere().log("Storage not set."); 464 return; 465 } 466 467 try (OutputStream out = 468 storageOptional.get().open(destinationFileUri, WriteStreamOpener.create())) { 469 out.write(remoteFilesMap.get(urlToDownload)); 470 } 471 } 472 473 /** 474 * Copies file to the singleFileDownloadRequest#destinationFileUri if set using {@code 475 * setUpRemoteFile} 476 * 477 * <p>Storage needs to be present to copy the file to destinationFileUri and corresponding backend 478 * needs to be added to the storage. Throws UnsupportedFileStorageOperation if corresponding 479 * backend is not set. 480 */ 481 @Override 482 public ListenableFuture<Void> downloadFile(SingleFileDownloadRequest singleFileDownloadRequest) { 483 Throwable throwable = throwableMap.get(MethodType.DOWNLOAD_FILE); 484 if (throwable != null) { 485 return Futures.immediateFailedFuture(throwable); 486 } 487 return PropagatedFutures.submitAsync( 488 () -> { 489 try { 490 downloadFileIfSet( 491 singleFileDownloadRequest.urlToDownload(), 492 singleFileDownloadRequest.destinationFileUri()); 493 } catch (IOException e) { 494 return Futures.immediateFailedFuture(e); 495 } 496 497 return Futures.immediateVoidFuture(); 498 }, 499 sequentialControlExecutor); 500 } 501 502 @Override 503 public ListenableFuture<ClientFileGroup> downloadFileGroup( 504 DownloadFileGroupRequest downloadFileGroupRequest) { 505 // logger.atInfo().log("#downloadFileGroup: %s", downloadFileGroupRequest); 506 downloadFileGroupParamsList.add(downloadFileGroupRequest); 507 return PropagatedFutures.submitAsync( 508 () -> downloadFileGroupInternal(downloadFileGroupRequest), sequentialControlExecutor); 509 } 510 511 @Override 512 public ListenableFuture<ClientFileGroup> downloadFileGroupWithForegroundService( 513 DownloadFileGroupRequest downloadFileGroupRequest) { 514 // logger.atInfo().log("#downloadFileGroupWithForegroundService: %s", downloadFileGroupRequest); 515 downloadFileGroupWithForegroundServiceParamsList.add(downloadFileGroupRequest); 516 return PropagatedFutures.submitAsync( 517 () -> downloadFileGroupInternal(downloadFileGroupRequest), sequentialControlExecutor); 518 } 519 520 private ListenableFuture<ClientFileGroup> downloadFileGroupInternal( 521 DownloadFileGroupRequest downloadFileGroupRequest) { 522 // logger.atConfig().log("#downloadFileGroupInternal: %s", downloadFileGroupRequest); 523 GroupKey.Builder groupKeyBuilder = GroupKey.newBuilder(); 524 groupKeyBuilder.setGroupName(downloadFileGroupRequest.groupName()); 525 if (downloadFileGroupRequest.accountOptional().isPresent()) { 526 groupKeyBuilder.setAccount( 527 AccountUtil.serialize(downloadFileGroupRequest.accountOptional().get())); 528 } 529 if (downloadFileGroupRequest.variantIdOptional().isPresent()) { 530 groupKeyBuilder.setVariantId(downloadFileGroupRequest.variantIdOptional().get()); 531 } 532 533 GroupKey groupKey = groupKeyBuilder.build(); 534 535 List<ClientFileGroup> fileGroupList = getMatchingFileGroups(groupKey, downloadedFileGroupList); 536 if (!fileGroupList.isEmpty()) { 537 return Futures.immediateFuture(fileGroupList.get(0)); 538 } 539 540 fileGroupList = getMatchingFileGroups(groupKey, pendingFileGroupList); 541 // If there is no match found in downloaded list, look for in pending list and update the 542 // status. 543 if (!fileGroupList.isEmpty()) { 544 ClientFileGroup fileGroup = fileGroupList.get(0); 545 ClientFileGroup downloadedFileGroup = 546 fileGroup.toBuilder().setStatus(ClientFileGroup.Status.DOWNLOADED).build(); 547 pendingFileGroupList.remove(fileGroup); 548 downloadedFileGroupList.add(downloadedFileGroup); 549 return Futures.immediateFuture(downloadedFileGroup); 550 } 551 552 return Futures.immediateFailedFuture( 553 DownloadException.builder() 554 .setDownloadResultCode(DownloadResultCode.GROUP_NOT_FOUND_ERROR) 555 .build()); 556 } 557 558 /** 559 * Copies file to the singleFileDownloadRequest#destinationFileUri if set using {@code 560 * setUpRemoteFile} 561 * 562 * <p>Storage needs to present to copy the file to destinationFileUri and corresponding backend 563 * needs to be added to the storage. Throws UnsupportedFileStorageOperation if corresponding 564 * backend is not set. 565 */ 566 @Override 567 public ListenableFuture<Void> downloadFileWithForegroundService( 568 SingleFileDownloadRequest singleFileDownloadRequest) { 569 Throwable throwable = throwableMap.get(MethodType.DOWNLOAD_FILE_FOREGROUND); 570 if (throwable != null) { 571 return Futures.immediateFailedFuture(throwable); 572 } 573 return PropagatedFutures.submitAsync( 574 () -> { 575 try { 576 downloadFileIfSet( 577 singleFileDownloadRequest.urlToDownload(), 578 singleFileDownloadRequest.destinationFileUri()); 579 } catch (IOException e) { 580 return Futures.immediateFailedFuture(e); 581 } 582 583 return Futures.immediateVoidFuture(); 584 }, 585 sequentialControlExecutor); 586 } 587 588 @Override 589 public void cancelForegroundDownload(String downloadKey) {} 590 591 @Override 592 public ListenableFuture<Void> maintenance() { 593 return Futures.immediateVoidFuture(); 594 } 595 596 @Override 597 public ListenableFuture<Void> collectGarbage() { 598 return Futures.immediateVoidFuture(); 599 } 600 601 @Override 602 public void schedulePeriodicTasks() {} 603 604 @Override 605 public ListenableFuture<Void> schedulePeriodicBackgroundTasks() { 606 return Futures.immediateVoidFuture(); 607 } 608 609 @Override 610 public ListenableFuture<Void> schedulePeriodicBackgroundTasks( 611 Optional<Map<String, ConstraintOverrides>> constraintOverridesMap) { 612 return Futures.immediateVoidFuture(); 613 } 614 615 @Override 616 public ListenableFuture<Void> cancelPeriodicBackgroundTasks() { 617 return Futures.immediateVoidFuture(); 618 } 619 620 @Override 621 public ListenableFuture<Void> handleTask(String tag) { 622 handleTaskParamsList.add(tag); 623 return Futures.immediateVoidFuture(); 624 } 625 626 @Override 627 public ListenableFuture<Void> clear() { 628 return Futures.immediateVoidFuture(); 629 } 630 631 @Override 632 public String getDebugInfoAsString() { 633 return ""; 634 } 635 636 @Override 637 public ListenableFuture<Void> reportUsage(UsageEvent usageEvent) { 638 return Futures.immediateVoidFuture(); 639 } 640 } 641