• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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