• 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;
17 
18 import static com.google.android.libraries.mobiledatadownload.tracing.TracePropagation.propagateAsyncFunction;
19 import static com.google.android.libraries.mobiledatadownload.tracing.TracePropagation.propagateRunnable;
20 import static com.google.common.base.Preconditions.checkNotNull;
21 import static com.google.common.util.concurrent.Futures.getDone;
22 import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
23 import static com.google.common.util.concurrent.Futures.immediateFuture;
24 import static com.google.common.util.concurrent.Futures.immediateVoidFuture;
25 import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
26 
27 import android.accounts.Account;
28 import android.content.Context;
29 import android.net.Uri;
30 import android.text.TextUtils;
31 import androidx.core.app.NotificationCompat;
32 import androidx.core.app.NotificationManagerCompat;
33 import com.google.android.libraries.mobiledatadownload.DownloadException.DownloadResultCode;
34 import com.google.android.libraries.mobiledatadownload.TaskScheduler.ConstraintOverrides;
35 import com.google.android.libraries.mobiledatadownload.TaskScheduler.NetworkState;
36 import com.google.android.libraries.mobiledatadownload.account.AccountUtil;
37 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage;
38 import com.google.android.libraries.mobiledatadownload.foreground.ForegroundDownloadKey;
39 import com.google.android.libraries.mobiledatadownload.foreground.NotificationUtil;
40 import com.google.android.libraries.mobiledatadownload.internal.DownloadGroupState;
41 import com.google.android.libraries.mobiledatadownload.internal.ExceptionToMddResultMapper;
42 import com.google.android.libraries.mobiledatadownload.internal.MddConstants;
43 import com.google.android.libraries.mobiledatadownload.internal.MobileDataDownloadManager;
44 import com.google.android.libraries.mobiledatadownload.internal.collect.GroupKeyAndGroup;
45 import com.google.android.libraries.mobiledatadownload.internal.collect.GroupPair;
46 import com.google.android.libraries.mobiledatadownload.internal.logging.EventLogger;
47 import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil;
48 import com.google.android.libraries.mobiledatadownload.internal.util.DownloadFutureMap;
49 import com.google.android.libraries.mobiledatadownload.internal.util.MddLiteConversionUtil;
50 import com.google.android.libraries.mobiledatadownload.internal.util.ProtoConversionUtil;
51 import com.google.android.libraries.mobiledatadownload.lite.Downloader;
52 import com.google.android.libraries.mobiledatadownload.monitor.DownloadProgressMonitor;
53 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedExecutionSequencer;
54 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFluentFuture;
55 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures;
56 import com.google.common.base.Optional;
57 import com.google.common.base.Preconditions;
58 import com.google.common.collect.ImmutableList;
59 import com.google.common.collect.ImmutableSet;
60 import com.google.common.util.concurrent.AsyncFunction;
61 import com.google.common.util.concurrent.FutureCallback;
62 import com.google.common.util.concurrent.Futures;
63 import com.google.common.util.concurrent.ListenableFuture;
64 import com.google.common.util.concurrent.ListenableFutureTask;
65 import com.google.mobiledatadownload.ClientConfigProto.ClientFile;
66 import com.google.mobiledatadownload.ClientConfigProto.ClientFileGroup;
67 import com.google.mobiledatadownload.DownloadConfigProto;
68 import com.google.mobiledatadownload.DownloadConfigProto.DataFileGroup;
69 import com.google.mobiledatadownload.internal.MetadataProto.DataFile;
70 import com.google.mobiledatadownload.internal.MetadataProto.DataFileGroupInternal;
71 import com.google.mobiledatadownload.internal.MetadataProto.DownloadConditions;
72 import com.google.mobiledatadownload.internal.MetadataProto.DownloadConditions.DeviceNetworkPolicy;
73 import com.google.mobiledatadownload.internal.MetadataProto.GroupKey;
74 import com.google.mobiledatadownload.LogProto.DataDownloadFileGroupStats;
75 import com.google.protobuf.Any;
76 import com.google.protobuf.InvalidProtocolBufferException;
77 import java.io.ByteArrayOutputStream;
78 import java.io.IOException;
79 import java.io.PrintWriter;
80 import java.util.ArrayList;
81 import java.util.List;
82 import java.util.Map;
83 import java.util.concurrent.ExecutionException;
84 import java.util.concurrent.Executor;
85 import java.util.concurrent.TimeUnit;
86 import java.util.concurrent.TimeoutException;
87 import javax.annotation.Nullable;
88 
89 /**
90  * Default implementation for {@link
91  * com.google.android.libraries.mobiledatadownload.MobileDataDownload}.
92  */
93 class MobileDataDownloadImpl implements MobileDataDownload {
94 
95   private static final String TAG = "MobileDataDownload";
96   private static final long DUMP_DEBUG_INFO_TIMEOUT = 3;
97 
98   private final Context context;
99   private final EventLogger eventLogger;
100   private final List<FileGroupPopulator> fileGroupPopulatorList;
101   private final Optional<TaskScheduler> taskSchedulerOptional;
102   private final MobileDataDownloadManager mobileDataDownloadManager;
103   private final SynchronousFileStorage fileStorage;
104   private final Flags flags;
105   private final Downloader singleFileDownloader;
106 
107   // Track all the on-going foreground downloads. This map is keyed by ForegroundDownloadKey.
108   private final DownloadFutureMap<ClientFileGroup> foregroundDownloadFutureMap;
109 
110   // Track all on-going background download requests started by downloadFileGroup. This map is keyed
111   // by ForegroundDownloadKey so request can be kept in sync with foregroundDownloadFutureMap.
112   private final DownloadFutureMap<ClientFileGroup> downloadFutureMap;
113 
114   // This executor will execute tasks sequentially.
115   private final Executor sequentialControlExecutor;
116   // ExecutionSequencer will execute a ListenableFuture and its Futures.transforms before taking the
117   // next task (<internal>). Most of MDD API should use
118   // ExecutionSequencer to guarantee Metadata synchronization. Currently only downloadFileGroup and
119   // handleTask APIs do not use ExecutionSequencer since their execution could take long time and
120   // using ExecutionSequencer would block other APIs.
121   private final PropagatedExecutionSequencer futureSerializer =
122       PropagatedExecutionSequencer.create();
123   private final Optional<DownloadProgressMonitor> downloadMonitorOptional;
124   private final Optional<Class<?>> foregroundDownloadServiceClassOptional;
125   private final AsyncFunction<DataFileGroupInternal, Boolean> customFileGroupValidator;
126   private final TimeSource timeSource;
127 
MobileDataDownloadImpl( Context context, EventLogger eventLogger, MobileDataDownloadManager mobileDataDownloadManager, Executor sequentialControlExecutor, List<FileGroupPopulator> fileGroupPopulatorList, Optional<TaskScheduler> taskSchedulerOptional, SynchronousFileStorage fileStorage, Optional<DownloadProgressMonitor> downloadMonitorOptional, Optional<Class<?>> foregroundDownloadServiceClassOptional, Flags flags, Downloader singleFileDownloader, Optional<CustomFileGroupValidator> customValidatorOptional, TimeSource timeSource)128   MobileDataDownloadImpl(
129       Context context,
130       EventLogger eventLogger,
131       MobileDataDownloadManager mobileDataDownloadManager,
132       Executor sequentialControlExecutor,
133       List<FileGroupPopulator> fileGroupPopulatorList,
134       Optional<TaskScheduler> taskSchedulerOptional,
135       SynchronousFileStorage fileStorage,
136       Optional<DownloadProgressMonitor> downloadMonitorOptional,
137       Optional<Class<?>> foregroundDownloadServiceClassOptional,
138       Flags flags,
139       Downloader singleFileDownloader,
140       Optional<CustomFileGroupValidator> customValidatorOptional,
141       TimeSource timeSource) {
142     this.context = context;
143     this.eventLogger = eventLogger;
144     this.fileGroupPopulatorList = fileGroupPopulatorList;
145     this.taskSchedulerOptional = taskSchedulerOptional;
146     this.sequentialControlExecutor = sequentialControlExecutor;
147     this.mobileDataDownloadManager = mobileDataDownloadManager;
148     this.fileStorage = fileStorage;
149     this.downloadMonitorOptional = downloadMonitorOptional;
150     this.foregroundDownloadServiceClassOptional = foregroundDownloadServiceClassOptional;
151     this.flags = flags;
152     this.singleFileDownloader = singleFileDownloader;
153     this.customFileGroupValidator =
154         createCustomFileGroupValidator(
155             customValidatorOptional,
156             mobileDataDownloadManager,
157             sequentialControlExecutor,
158             fileStorage);
159     this.downloadFutureMap = DownloadFutureMap.create(sequentialControlExecutor);
160     this.foregroundDownloadFutureMap =
161         DownloadFutureMap.create(
162             sequentialControlExecutor,
163             createCallbacksForForegroundService(context, foregroundDownloadServiceClassOptional));
164     this.timeSource = timeSource;
165   }
166 
167   // Wraps the custom validator because the validation at a lower level of the stack where
168   // the ClientFileGroup is not available, yet ClientFileGroup is the client-facing API we'd
169   // like to expose.
createCustomFileGroupValidator( Optional<CustomFileGroupValidator> validatorOptional, MobileDataDownloadManager mobileDataDownloadManager, Executor executor, SynchronousFileStorage fileStorage)170   private static AsyncFunction<DataFileGroupInternal, Boolean> createCustomFileGroupValidator(
171       Optional<CustomFileGroupValidator> validatorOptional,
172       MobileDataDownloadManager mobileDataDownloadManager,
173       Executor executor,
174       SynchronousFileStorage fileStorage) {
175     if (!validatorOptional.isPresent()) {
176       return unused -> immediateFuture(true);
177     }
178 
179     return internalFileGroup ->
180         PropagatedFutures.transformAsync(
181             createClientFileGroup(
182                 internalFileGroup,
183                 /* account= */ null,
184                 ClientFileGroup.Status.PENDING_CUSTOM_VALIDATION,
185                 /* preserveZipDirectories= */ false,
186                 /* verifyIsolatedStructure= */ true,
187                 mobileDataDownloadManager,
188                 executor,
189                 fileStorage),
190             propagateAsyncFunction(
191                 clientFileGroup -> validatorOptional.get().validateFileGroup(clientFileGroup)),
192             executor);
193   }
194 
195   /**
196    * Functional interface used as callback for logging file group stats. Used to create file group
197    * stats from the result of the future.
198    *
199    * @see attachMddApiLogging
200    */
201   private interface StatsFromApiResultCreator<T> {
create(T result)202     DataDownloadFileGroupStats create(T result);
203   }
204 
205   /**
206    * Functional interface used as callback when logging API result. Used to get the API result code
207    * from the result of the API future if it succeeds.
208    *
209    * <p>Note: The need for this is due to {@link addFileGroup} returning false instead of an
210    * exception if it fails. For other APIs with proper exception handling, it should suffice to
211    * immediately return the success code.
212    *
213    * <p>TODO(b/143572409): Remove once addGroupForDownload is updated to return void.
214    *
215    * @see attachMddApiLogging
216    */
217   private interface ResultCodeFromApiResultGetter<T> {
get(T result)218     int get(T result);
219   }
220 
221   /**
222    * Helper function used to log mdd api stats. Adds FutureCallback to the {@code resultFuture}
223    * which is the result of mdd api call and logs in onSuccess and onFailure functions of callback.
224    *
225    * @param apiName Code of the api being logged.
226    * @param resultFuture Future result of the api call.
227    * @param startTimeNs start time in ns.
228    * @param defaultFileGroupStats Initial file group stats.
229    * @param statsCreator This functional interface is invoked from the onSuccess of FutureCallback
230    *     with the result of the future. File group stats returned here is merged with the initial
231    *     stats and logged.
232    */
attachMddApiLogging( int apiName, ListenableFuture<T> resultFuture, long startTimeNs, DataDownloadFileGroupStats defaultFileGroupStats, StatsFromApiResultCreator<T> statsCreator, ResultCodeFromApiResultGetter<T> resultCodeGetter)233   private <T> void attachMddApiLogging(
234       int apiName,
235       ListenableFuture<T> resultFuture,
236       long startTimeNs,
237       DataDownloadFileGroupStats defaultFileGroupStats,
238       StatsFromApiResultCreator<T> statsCreator,
239       ResultCodeFromApiResultGetter<T> resultCodeGetter) {
240     // Using listener instead of transform since we need to log even if the future fails.
241     // Note: Listener is being registered on directexecutor for accurate latency measurement.
242     resultFuture.addListener(
243         propagateRunnable(
244             () -> {
245               long latencyNs = timeSource.elapsedRealtimeNanos() - startTimeNs;
246               // Log the stats asynchronously.
247               // Note: To avoid adding latency to mdd api calls, log asynchronously.
248               var unused =
249                   PropagatedFutures.submit(
250                       () -> {
251                         int resultCode;
252                         T result = null;
253                         DataDownloadFileGroupStats fileGroupStats = defaultFileGroupStats;
254                         try {
255                           result = Futures.getDone(resultFuture);
256                           resultCode = resultCodeGetter.get(result);
257                         } catch (Throwable t) {
258                           resultCode = ExceptionToMddResultMapper.map(t);
259                         }
260 
261                         // Merge stats created from result of api with the default stats.
262                         if (result != null) {
263                           fileGroupStats =
264                               fileGroupStats.toBuilder()
265                                   .mergeFrom(statsCreator.create(result))
266                                   .build();
267                         }
268 
269                         Void resultLog = null;
270 
271                         eventLogger.logMddLibApiResultLog(resultLog);
272                       },
273                       sequentialControlExecutor);
274             }),
275         directExecutor());
276   }
277 
278   @Override
addFileGroup(AddFileGroupRequest addFileGroupRequest)279   public ListenableFuture<Boolean> addFileGroup(AddFileGroupRequest addFileGroupRequest) {
280     long startTimeNs = timeSource.elapsedRealtimeNanos();
281 
282     ListenableFuture<Boolean> resultFuture =
283         futureSerializer.submitAsync(
284             () -> addFileGroupHelper(addFileGroupRequest), sequentialControlExecutor);
285 
286     DataDownloadFileGroupStats defaultFileGroupStats =
287         DataDownloadFileGroupStats.newBuilder()
288             .setFileGroupName(addFileGroupRequest.dataFileGroup().getGroupName())
289             .setBuildId(addFileGroupRequest.dataFileGroup().getBuildId())
290             .setVariantId(addFileGroupRequest.dataFileGroup().getVariantId())
291             .setHasAccount(addFileGroupRequest.accountOptional().isPresent())
292             .setFileGroupVersionNumber(
293                 addFileGroupRequest.dataFileGroup().getFileGroupVersionNumber())
294             .setOwnerPackage(addFileGroupRequest.dataFileGroup().getOwnerPackage())
295             .setFileCount(addFileGroupRequest.dataFileGroup().getFileCount())
296             .build();
297     attachMddApiLogging(
298         0,
299         resultFuture,
300         startTimeNs,
301         defaultFileGroupStats,
302         /* statsCreator= */ unused -> defaultFileGroupStats,
303         /* resultCodeGetter= */ succeeded -> succeeded ? 0 : 0);
304 
305     return resultFuture;
306   }
307 
addFileGroupHelper(AddFileGroupRequest addFileGroupRequest)308   private ListenableFuture<Boolean> addFileGroupHelper(AddFileGroupRequest addFileGroupRequest) {
309     LogUtil.d(
310         "%s: Adding for download group = '%s', variant = '%s', buildId = '%d' and"
311             + " associating it with account = '%s', variant = '%s'",
312         TAG,
313         addFileGroupRequest.dataFileGroup().getGroupName(),
314         addFileGroupRequest.dataFileGroup().getVariantId(),
315         addFileGroupRequest.dataFileGroup().getBuildId(),
316         String.valueOf(addFileGroupRequest.accountOptional().orNull()),
317         String.valueOf(addFileGroupRequest.variantIdOptional().orNull()));
318 
319     DataFileGroup dataFileGroup = addFileGroupRequest.dataFileGroup();
320 
321     // Ensure that the owner package is always set as the host app.
322     if (!dataFileGroup.hasOwnerPackage()) {
323       dataFileGroup = dataFileGroup.toBuilder().setOwnerPackage(context.getPackageName()).build();
324     } else if (!context.getPackageName().equals(dataFileGroup.getOwnerPackage())) {
325       LogUtil.e(
326           "%s: Added group = '%s' with wrong owner package: '%s' v.s. '%s' ",
327           TAG,
328           dataFileGroup.getGroupName(),
329           context.getPackageName(),
330           dataFileGroup.getOwnerPackage());
331       return immediateFuture(false);
332     }
333 
334     GroupKey.Builder groupKeyBuilder =
335         GroupKey.newBuilder()
336             .setGroupName(dataFileGroup.getGroupName())
337             .setOwnerPackage(dataFileGroup.getOwnerPackage());
338 
339     if (addFileGroupRequest.accountOptional().isPresent()) {
340       groupKeyBuilder.setAccount(
341           AccountUtil.serialize(addFileGroupRequest.accountOptional().get()));
342     }
343 
344     if (addFileGroupRequest.variantIdOptional().isPresent()) {
345       groupKeyBuilder.setVariantId(addFileGroupRequest.variantIdOptional().get());
346     }
347 
348     try {
349       DataFileGroupInternal dataFileGroupInternal = ProtoConversionUtil.convert(dataFileGroup);
350       return mobileDataDownloadManager.addGroupForDownloadInternal(
351           groupKeyBuilder.build(), dataFileGroupInternal, customFileGroupValidator);
352     } catch (InvalidProtocolBufferException e) {
353       // TODO(b/118137672): Consider rethrow exception instead of returning false.
354       LogUtil.e(e, "%s: Unable to convert from DataFileGroup to DataFileGroupInternal.", TAG);
355       return immediateFuture(false);
356     }
357   }
358 
359   // TODO: Change to return ListenableFuture<Void>.
360   @Override
removeFileGroup(RemoveFileGroupRequest removeFileGroupRequest)361   public ListenableFuture<Boolean> removeFileGroup(RemoveFileGroupRequest removeFileGroupRequest) {
362     return futureSerializer.submitAsync(
363         () -> {
364           GroupKey.Builder groupKeyBuilder =
365               GroupKey.newBuilder()
366                   .setGroupName(removeFileGroupRequest.groupName())
367                   .setOwnerPackage(context.getPackageName());
368           if (removeFileGroupRequest.accountOptional().isPresent()) {
369             groupKeyBuilder.setAccount(
370                 AccountUtil.serialize(removeFileGroupRequest.accountOptional().get()));
371           }
372           if (removeFileGroupRequest.variantIdOptional().isPresent()) {
373             groupKeyBuilder.setVariantId(removeFileGroupRequest.variantIdOptional().get());
374           }
375 
376           GroupKey groupKey = groupKeyBuilder.build();
377           return PropagatedFutures.transform(
378               mobileDataDownloadManager.removeFileGroup(
379                   groupKey, removeFileGroupRequest.pendingOnly()),
380               voidArg -> true,
381               sequentialControlExecutor);
382         },
383         sequentialControlExecutor);
384   }
385 
386   @Override
removeFileGroupsByFilter( RemoveFileGroupsByFilterRequest removeFileGroupsByFilterRequest)387   public ListenableFuture<RemoveFileGroupsByFilterResponse> removeFileGroupsByFilter(
388       RemoveFileGroupsByFilterRequest removeFileGroupsByFilterRequest) {
389     return futureSerializer.submitAsync(
390         () ->
391             PropagatedFluentFuture.from(mobileDataDownloadManager.getAllFreshGroups())
392                 .transformAsync(
393                     allFreshGroupKeyAndGroups -> {
394                       ImmutableSet.Builder<GroupKey> groupKeysToRemoveBuilder =
395                           ImmutableSet.builder();
396                       for (GroupKeyAndGroup groupKeyAndGroup : allFreshGroupKeyAndGroups) {
397                         if (applyRemoveFileGroupsFilter(
398                             removeFileGroupsByFilterRequest, groupKeyAndGroup)) {
399                           // Remove downloaded status so pending/downloaded versions of the same
400                           // group are treated as one.
401                           groupKeysToRemoveBuilder.add(
402                               groupKeyAndGroup.groupKey().toBuilder().clearDownloaded().build());
403                         }
404                       }
405                       ImmutableSet<GroupKey> groupKeysToRemove = groupKeysToRemoveBuilder.build();
406                       if (groupKeysToRemove.isEmpty()) {
407                         return immediateFuture(
408                             RemoveFileGroupsByFilterResponse.newBuilder()
409                                 .setRemovedFileGroupsCount(0)
410                                 .build());
411                       }
412                       return PropagatedFutures.transform(
413                           mobileDataDownloadManager.removeFileGroups(groupKeysToRemove.asList()),
414                           unused ->
415                               RemoveFileGroupsByFilterResponse.newBuilder()
416                                   .setRemovedFileGroupsCount(groupKeysToRemove.size())
417                                   .build(),
418                           sequentialControlExecutor);
419                     },
420                     sequentialControlExecutor),
421         sequentialControlExecutor);
422   }
423 
424   // Perform filtering using options from RemoveFileGroupsByFilterRequest
425   private static boolean applyRemoveFileGroupsFilter(
426       RemoveFileGroupsByFilterRequest removeFileGroupsByFilterRequest,
427       GroupKeyAndGroup groupKeyAndGroup) {
428     // If request filters by account, ensure account is present and is equal
429     Optional<Account> accountOptional = removeFileGroupsByFilterRequest.accountOptional();
430     if (!accountOptional.isPresent() && groupKeyAndGroup.groupKey().hasAccount()) {
431       // Account must explicitly be provided in order to remove account associated file groups.
432       return false;
433     }
434     if (accountOptional.isPresent()
435         && !AccountUtil.serialize(accountOptional.get())
436             .equals(groupKeyAndGroup.groupKey().getAccount())) {
437       return false;
438     }
439 
440     return true;
441   }
442 
443   /**
444    * Helper function to create {@link DataDownloadFileGroupStats} object from {@link
445    * GetFileGroupRequest} for getFileGroup() logging.
446    *
447    * <p>Used when the matching file group is not found or a failure occurred.
448    * file_group_version_number and build_id are set to -1 by default.
449    */
450   private DataDownloadFileGroupStats createFileGroupStatsFromGetFileGroupRequest(
451       GetFileGroupRequest getFileGroupRequest) {
452     DataDownloadFileGroupStats.Builder fileGroupStatsBuilder =
453         DataDownloadFileGroupStats.newBuilder();
454     fileGroupStatsBuilder.setFileGroupName(getFileGroupRequest.groupName());
455     if (getFileGroupRequest.variantIdOptional().isPresent()) {
456       fileGroupStatsBuilder.setVariantId(getFileGroupRequest.variantIdOptional().get());
457     }
458     if (getFileGroupRequest.accountOptional().isPresent()) {
459       fileGroupStatsBuilder.setHasAccount(true);
460     } else {
461       fileGroupStatsBuilder.setHasAccount(false);
462     }
463 
464     fileGroupStatsBuilder.setFileGroupVersionNumber(
465         MddConstants.FILE_GROUP_NOT_FOUND_FILE_GROUP_VERSION_NUMBER);
466     fileGroupStatsBuilder.setBuildId(MddConstants.FILE_GROUP_NOT_FOUND_BUILD_ID);
467 
468     return fileGroupStatsBuilder.build();
469   }
470 
471   // TODO: Futures.immediateFuture(null) uses a different annotation for Nullable.
472   @SuppressWarnings("nullness")
473   @Override
474   public ListenableFuture<ClientFileGroup> getFileGroup(GetFileGroupRequest getFileGroupRequest) {
475     long startTimeNs = timeSource.elapsedRealtimeNanos();
476 
477     ListenableFuture<ClientFileGroup> resultFuture =
478         futureSerializer.submitAsync(
479             () -> {
480               GroupKey groupKey =
481                   createGroupKey(
482                       getFileGroupRequest.groupName(),
483                       getFileGroupRequest.accountOptional(),
484                       getFileGroupRequest.variantIdOptional());
485               return PropagatedFutures.transformAsync(
486                   mobileDataDownloadManager.getFileGroup(groupKey, /* downloaded= */ true),
487                   dataFileGroup ->
488                       createClientFileGroupAndLogQueryStats(
489                           groupKey,
490                           dataFileGroup,
491                           /* downloaded= */ true,
492                           getFileGroupRequest.preserveZipDirectories(),
493                           getFileGroupRequest.verifyIsolatedStructure()),
494                   sequentialControlExecutor);
495             },
496             sequentialControlExecutor);
497 
498     attachMddApiLogging(
499         0,
500         resultFuture,
501         startTimeNs,
502         createFileGroupStatsFromGetFileGroupRequest(getFileGroupRequest),
503         /* statsCreator= */ result -> createFileGroupDetails(result),
504         /* resultCodeGetter= */ unused -> 0);
505     return resultFuture;
506   }
507 
508   @SuppressWarnings("nullness")
509   @Override
510   public ListenableFuture<DataFileGroup> readDataFileGroup(
511       ReadDataFileGroupRequest readDataFileGroupRequest) {
512     return futureSerializer.submitAsync(
513         () -> {
514           GroupKey groupKey =
515               createGroupKey(
516                   readDataFileGroupRequest.groupName(),
517                   readDataFileGroupRequest.accountOptional(),
518                   readDataFileGroupRequest.variantIdOptional());
519           return PropagatedFutures.transformAsync(
520               mobileDataDownloadManager.getFileGroup(groupKey, /* downloaded= */ true),
521               internalFileGroup -> immediateFuture(ProtoConversionUtil.reverse(internalFileGroup)),
522               sequentialControlExecutor);
523         },
524         sequentialControlExecutor);
525   }
526 
527   private GroupKey createGroupKey(
528       String groupName, Optional<Account> accountOptional, Optional<String> variantOptional) {
529     GroupKey.Builder groupKeyBuilder =
530         GroupKey.newBuilder().setGroupName(groupName).setOwnerPackage(context.getPackageName());
531 
532     if (accountOptional.isPresent()) {
533       groupKeyBuilder.setAccount(AccountUtil.serialize(accountOptional.get()));
534     }
535 
536     if (variantOptional.isPresent()) {
537       groupKeyBuilder.setVariantId(variantOptional.get());
538     }
539 
540     return groupKeyBuilder.build();
541   }
542 
543   private ListenableFuture<ClientFileGroup> createClientFileGroupAndLogQueryStats(
544       GroupKey groupKey,
545       @Nullable DataFileGroupInternal dataFileGroup,
546       boolean downloaded,
547       boolean preserveZipDirectories,
548       boolean verifyIsolatedStructure) {
549     return PropagatedFutures.transform(
550         createClientFileGroup(
551             dataFileGroup,
552             groupKey.hasAccount() ? groupKey.getAccount() : null,
553             downloaded ? ClientFileGroup.Status.DOWNLOADED : ClientFileGroup.Status.PENDING,
554             preserveZipDirectories,
555             verifyIsolatedStructure,
556             mobileDataDownloadManager,
557             sequentialControlExecutor,
558             fileStorage),
559         clientFileGroup -> {
560           if (clientFileGroup != null) {
561             eventLogger.logMddQueryStats(createFileGroupDetails(clientFileGroup));
562           }
563           return clientFileGroup;
564         },
565         sequentialControlExecutor);
566   }
567 
568   @SuppressWarnings("nullness")
569   private static ListenableFuture<ClientFileGroup> createClientFileGroup(
570       @Nullable DataFileGroupInternal dataFileGroup,
571       @Nullable String account,
572       ClientFileGroup.Status status,
573       boolean preserveZipDirectories,
574       boolean verifyIsolatedStructure,
575       MobileDataDownloadManager manager,
576       Executor executor,
577       SynchronousFileStorage fileStorage) {
578     if (dataFileGroup == null) {
579       return immediateFuture(null);
580     }
581     ClientFileGroup.Builder clientFileGroupBuilder =
582         ClientFileGroup.newBuilder()
583             .setGroupName(dataFileGroup.getGroupName())
584             .setOwnerPackage(dataFileGroup.getOwnerPackage())
585             .setVersionNumber(dataFileGroup.getFileGroupVersionNumber())
586 //            .setCustomProperty(dataFileGroup.getCustomProperty())
587             .setBuildId(dataFileGroup.getBuildId())
588             .setVariantId(dataFileGroup.getVariantId())
589             .setStatus(status)
590             .addAllLocale(dataFileGroup.getLocaleList());
591 
592     if (account != null) {
593       clientFileGroupBuilder.setAccount(account);
594     }
595 
596     if (dataFileGroup.hasCustomMetadata()) {
597       clientFileGroupBuilder.setCustomMetadata(dataFileGroup.getCustomMetadata());
598     }
599 
600     List<DataFile> dataFiles = dataFileGroup.getFileList();
601     ListenableFuture<Void> addOnDeviceUrisFuture = immediateVoidFuture();
602     if (status == ClientFileGroup.Status.DOWNLOADED
603         || status == ClientFileGroup.Status.PENDING_CUSTOM_VALIDATION) {
604       addOnDeviceUrisFuture =
605           PropagatedFluentFuture.from(
606                   manager.getDataFileUris(dataFileGroup, verifyIsolatedStructure))
607               .transformAsync(
608                   dataFileUriMap -> {
609                     for (DataFile dataFile : dataFiles) {
610                       if (!dataFileUriMap.containsKey(dataFile)) {
611                         return immediateFailedFuture(
612                             DownloadException.builder()
613                                 .setDownloadResultCode(
614                                     DownloadResultCode.DOWNLOADED_FILE_NOT_FOUND_ERROR)
615                                 .setMessage("getDataFileUris() resolved to null")
616                                 .build());
617                       }
618                       Uri uri = dataFileUriMap.get(dataFile);
619 
620                       try {
621                         if (!preserveZipDirectories && fileStorage.isDirectory(uri)) {
622                           String rootPath = uri.getPath();
623                           if (rootPath != null) {
624                             clientFileGroupBuilder.addAllFile(
625                                 listAllClientFilesOfDirectory(fileStorage, uri, rootPath));
626                           }
627                         } else {
628                           clientFileGroupBuilder.addFile(
629                               createClientFile(
630                                   dataFile.getFileId(),
631                                   dataFile.getByteSize(),
632                                   dataFile.getDownloadedFileByteSize(),
633                                   uri.toString(),
634                                   dataFile.hasCustomMetadata()
635                                       ? dataFile.getCustomMetadata()
636                                       : null));
637                         }
638                       } catch (IOException e) {
639                         LogUtil.e(e, "Failed to list files under directory:" + uri);
640                       }
641                     }
642                     return immediateVoidFuture();
643                   },
644                   executor);
645     } else {
646       for (DataFile dataFile : dataFiles) {
647         clientFileGroupBuilder.addFile(
648             createClientFile(
649                 dataFile.getFileId(),
650                 dataFile.getByteSize(),
651                 dataFile.getDownloadedFileByteSize(),
652                 /* uri= */ null,
653                 dataFile.hasCustomMetadata() ? dataFile.getCustomMetadata() : null));
654       }
655     }
656 
657     return PropagatedFluentFuture.from(addOnDeviceUrisFuture)
658         .transform(unused -> clientFileGroupBuilder.build(), executor)
659         .catching(DownloadException.class, exn -> null, executor);
660   }
661 
662   private static ClientFile createClientFile(
663       String fileId,
664       int byteSize,
665       int downloadByteSize,
666       @Nullable String uri,
667       @Nullable Any customMetadata) {
668     ClientFile.Builder clientFileBuilder =
669         ClientFile.newBuilder().setFileId(fileId).setFullSizeInBytes(byteSize);
670     if (downloadByteSize > 0) {
671       // Files with downloaded transforms like compress and zip could have different downloaded
672       // file size than the final file size on disk. Return the downloaded file size for client to
673       // track and calculate the download progress.
674       clientFileBuilder.setDownloadSizeInBytes(downloadByteSize);
675     }
676     if (uri != null) {
677       clientFileBuilder.setFileUri(uri);
678     }
679     if (customMetadata != null) {
680       clientFileBuilder.setCustomMetadata(customMetadata);
681     }
682     return clientFileBuilder.build();
683   }
684 
685   private static List<ClientFile> listAllClientFilesOfDirectory(
686       SynchronousFileStorage fileStorage, Uri dirUri, String rootDir) throws IOException {
687     List<ClientFile> clientFileList = new ArrayList<>();
688     for (Uri childUri : fileStorage.children(dirUri)) {
689       if (fileStorage.isDirectory(childUri)) {
690         clientFileList.addAll(listAllClientFilesOfDirectory(fileStorage, childUri, rootDir));
691       } else {
692         String childPath = childUri.getPath();
693         if (childPath != null) {
694           ClientFile clientFile =
695               ClientFile.newBuilder()
696                   .setFileId(childPath.replaceFirst(rootDir, ""))
697                   .setFullSizeInBytes((int) fileStorage.fileSize(childUri))
698                   .setFileUri(childUri.toString())
699                   .build();
700           clientFileList.add(clientFile);
701         }
702       }
703     }
704     return clientFileList;
705   }
706 
707   @Override
708   public ListenableFuture<ImmutableList<ClientFileGroup>> getFileGroupsByFilter(
709       GetFileGroupsByFilterRequest getFileGroupsByFilterRequest) {
710     return futureSerializer.submitAsync(
711         () ->
712             PropagatedFutures.transformAsync(
713                 mobileDataDownloadManager.getAllFreshGroups(),
714                 allFreshGroupKeyAndGroups -> {
715                   ListenableFuture<ImmutableList.Builder<ClientFileGroup>>
716                       clientFileGroupsBuilderFuture =
717                           immediateFuture(ImmutableList.<ClientFileGroup>builder());
718                   for (GroupKeyAndGroup groupKeyAndGroup : allFreshGroupKeyAndGroups) {
719                     clientFileGroupsBuilderFuture =
720                         PropagatedFutures.transformAsync(
721                             clientFileGroupsBuilderFuture,
722                             clientFileGroupsBuilder -> {
723                               GroupKey groupKey = groupKeyAndGroup.groupKey();
724                               DataFileGroupInternal dataFileGroup =
725                                   groupKeyAndGroup.dataFileGroup();
726                               if (applyFilter(
727                                   getFileGroupsByFilterRequest, groupKey, dataFileGroup)) {
728                                 return PropagatedFutures.transform(
729                                     createClientFileGroupAndLogQueryStats(
730                                         groupKey,
731                                         dataFileGroup,
732                                         groupKey.getDownloaded(),
733                                         getFileGroupsByFilterRequest.preserveZipDirectories(),
734                                         getFileGroupsByFilterRequest.verifyIsolatedStructure()),
735                                     clientFileGroup -> {
736                                       if (clientFileGroup != null) {
737                                         clientFileGroupsBuilder.add(clientFileGroup);
738                                       }
739                                       return clientFileGroupsBuilder;
740                                     },
741                                     sequentialControlExecutor);
742                               }
743                               return immediateFuture(clientFileGroupsBuilder);
744                             },
745                             sequentialControlExecutor);
746                   }
747 
748                   return PropagatedFutures.transform(
749                       clientFileGroupsBuilderFuture,
750                       ImmutableList.Builder::build,
751                       sequentialControlExecutor);
752                 },
753                 sequentialControlExecutor),
754         sequentialControlExecutor);
755   }
756 
757   // Perform filtering using options from GetFileGroupsByFilterRequest
758   private static boolean applyFilter(
759       GetFileGroupsByFilterRequest getFileGroupsByFilterRequest,
760       GroupKey groupKey,
761       DataFileGroupInternal fileGroup) {
762     if (getFileGroupsByFilterRequest.includeAllGroups()) {
763       return true;
764     }
765 
766     // If request filters by group name, ensure name is equal
767     Optional<String> groupNameOptional = getFileGroupsByFilterRequest.groupNameOptional();
768     if (groupNameOptional.isPresent()
769         && !TextUtils.equals(groupNameOptional.get(), groupKey.getGroupName())) {
770       return false;
771     }
772 
773     // When the caller requests account independent groups only.
774     if (getFileGroupsByFilterRequest.groupWithNoAccountOnly()) {
775       return !groupKey.hasAccount();
776     }
777 
778     // When the caller requests account dependent groups as well.
779     if (getFileGroupsByFilterRequest.accountOptional().isPresent()
780         && !AccountUtil.serialize(getFileGroupsByFilterRequest.accountOptional().get())
781             .equals(groupKey.getAccount())) {
782       return false;
783     }
784     return true;
785   }
786 
787   /**
788    * Creates {@link DataDownloadFileGroupStats} from {@link ClientFileGroup} for remote logging
789    * purposes.
790    */
791   private static DataDownloadFileGroupStats createFileGroupDetails(
792       ClientFileGroup clientFileGroup) {
793     return DataDownloadFileGroupStats.newBuilder()
794         .setFileGroupName(clientFileGroup.getGroupName())
795         .setOwnerPackage(clientFileGroup.getOwnerPackage())
796         .setFileGroupVersionNumber(clientFileGroup.getVersionNumber())
797         .setFileCount(clientFileGroup.getFileCount())
798         .setVariantId(clientFileGroup.getVariantId())
799         .setBuildId(clientFileGroup.getBuildId())
800         .build();
801   }
802 
803   @Override
804   public ListenableFuture<Void> importFiles(ImportFilesRequest importFilesRequest) {
805     GroupKey.Builder groupKeyBuilder =
806         GroupKey.newBuilder()
807             .setGroupName(importFilesRequest.groupName())
808             .setOwnerPackage(context.getPackageName());
809 
810     if (importFilesRequest.accountOptional().isPresent()) {
811       groupKeyBuilder.setAccount(AccountUtil.serialize(importFilesRequest.accountOptional().get()));
812     }
813 
814     GroupKey groupKey = groupKeyBuilder.build();
815 
816     ImmutableList.Builder<DataFile> updatedDataFileListBuilder =
817         ImmutableList.builderWithExpectedSize(importFilesRequest.updatedDataFileList().size());
818     for (DownloadConfigProto.DataFile dataFile : importFilesRequest.updatedDataFileList()) {
819       updatedDataFileListBuilder.add(ProtoConversionUtil.convertDataFile(dataFile));
820     }
821 
822     return futureSerializer.submitAsync(
823         () ->
824             mobileDataDownloadManager.importFiles(
825                 groupKey,
826                 importFilesRequest.buildId(),
827                 importFilesRequest.variantId(),
828                 updatedDataFileListBuilder.build(),
829                 importFilesRequest.inlineFileMap(),
830                 importFilesRequest.customPropertyOptional(),
831                 customFileGroupValidator),
832         sequentialControlExecutor);
833   }
834 
835   @Override
836   public ListenableFuture<Void> downloadFile(SingleFileDownloadRequest singleFileDownloadRequest) {
837     return singleFileDownloader.download(
838         MddLiteConversionUtil.convertToDownloadRequest(singleFileDownloadRequest));
839   }
840 
841   @Override
842   public ListenableFuture<ClientFileGroup> downloadFileGroup(
843       DownloadFileGroupRequest downloadFileGroupRequest) {
844     // Submit the call to sequentialControlExecutor, but don't use futureSerializer. This will
845     // ensure that multiple calls are enqueued to the executor in a FIFO order, but these calls
846     // won't block each other when the download is in progress.
847     return PropagatedFutures.submitAsync(
848         () ->
849             PropagatedFutures.transformAsync(
850                 // Check if requested file group has already been downloaded
851                 getDownloadGroupState(downloadFileGroupRequest),
852                 downloadGroupState -> {
853                   switch (downloadGroupState.getKind()) {
854                     case IN_PROGRESS_FUTURE:
855                       // If the file group download is in progress, return that future immediately
856                       return downloadGroupState.inProgressFuture();
857                     case DOWNLOADED_GROUP:
858                       // If the file group is already downloaded, return that immediately.
859                       return immediateFuture(downloadGroupState.downloadedGroup());
860                     case PENDING_GROUP:
861                       return downloadPendingFileGroup(downloadFileGroupRequest);
862                   }
863                   throw new AssertionError(
864                       String.format(
865                           "received unsupported DownloadGroupState kind %s",
866                           downloadGroupState.getKind()));
867                 },
868                 sequentialControlExecutor),
869         sequentialControlExecutor);
870   }
871 
872   /** Helper method to download a group after it's determined to be pending. */
873   private ListenableFuture<ClientFileGroup> downloadPendingFileGroup(
874       DownloadFileGroupRequest downloadFileGroupRequest) {
875     String groupName = downloadFileGroupRequest.groupName();
876     GroupKey.Builder groupKeyBuilder =
877         GroupKey.newBuilder().setGroupName(groupName).setOwnerPackage(context.getPackageName());
878 
879     if (downloadFileGroupRequest.accountOptional().isPresent()) {
880       groupKeyBuilder.setAccount(
881           AccountUtil.serialize(downloadFileGroupRequest.accountOptional().get()));
882     }
883     if (downloadFileGroupRequest.variantIdOptional().isPresent()) {
884       groupKeyBuilder.setVariantId(downloadFileGroupRequest.variantIdOptional().get());
885     }
886 
887     GroupKey groupKey = groupKeyBuilder.build();
888 
889     if (downloadFileGroupRequest.listenerOptional().isPresent()) {
890       if (downloadMonitorOptional.isPresent()) {
891         downloadMonitorOptional
892             .get()
893             .addDownloadListener(groupName, downloadFileGroupRequest.listenerOptional().get());
894       } else {
895         return immediateFailedFuture(
896             DownloadException.builder()
897                 .setDownloadResultCode(DownloadResultCode.DOWNLOAD_MONITOR_NOT_PROVIDED_ERROR)
898                 .setMessage(
899                     "downloadFileGroup: DownloadListener is present but Download Monitor"
900                         + " is not provided!")
901                 .build());
902       }
903     }
904 
905     Optional<DownloadConditions> downloadConditions;
906     try {
907       downloadConditions =
908           downloadFileGroupRequest.downloadConditionsOptional().isPresent()
909               ? Optional.of(
910                   ProtoConversionUtil.convert(
911                       downloadFileGroupRequest.downloadConditionsOptional().get()))
912               : Optional.absent();
913     } catch (InvalidProtocolBufferException e) {
914       return immediateFailedFuture(e);
915     }
916 
917     // Get the key used for the download future map
918     ForegroundDownloadKey downloadKey =
919         ForegroundDownloadKey.ofFileGroup(
920             downloadFileGroupRequest.groupName(),
921             downloadFileGroupRequest.accountOptional(),
922             downloadFileGroupRequest.variantIdOptional());
923 
924     // Create a ListenableFutureTask to delay starting the downloadFuture until we can add the
925     // future to our map.
926     ListenableFutureTask<Void> startTask = ListenableFutureTask.create(() -> null);
927     ListenableFuture<ClientFileGroup> downloadFuture =
928         PropagatedFluentFuture.from(startTask)
929             .transformAsync(
930                 unused ->
931                     mobileDataDownloadManager.downloadFileGroup(
932                         groupKey, downloadConditions, customFileGroupValidator),
933                 sequentialControlExecutor)
934             .transformAsync(
935                 dataFileGroup ->
936                     createClientFileGroup(
937                         dataFileGroup,
938                         downloadFileGroupRequest.accountOptional().isPresent()
939                             ? AccountUtil.serialize(
940                                 downloadFileGroupRequest.accountOptional().get())
941                             : null,
942                         ClientFileGroup.Status.DOWNLOADED,
943                         downloadFileGroupRequest.preserveZipDirectories(),
944                         downloadFileGroupRequest.verifyIsolatedStructure(),
945                         mobileDataDownloadManager,
946                         sequentialControlExecutor,
947                         fileStorage),
948                 sequentialControlExecutor)
949             .transform(Preconditions::checkNotNull, sequentialControlExecutor);
950 
951     // Get a handle on the download task so we can get the CFG during transforms
952     PropagatedFluentFuture<ClientFileGroup> downloadTaskFuture =
953         PropagatedFluentFuture.from(downloadFutureMap.add(downloadKey.toString(), downloadFuture))
954             .transformAsync(
955                 unused -> {
956                   // Now that the download future is added, start the task and return the future
957                   startTask.run();
958                   return downloadFuture;
959                 },
960                 sequentialControlExecutor);
961 
962     ListenableFuture<ClientFileGroup> transformFuture =
963         downloadTaskFuture
964             .transformAsync(
965                 unused -> downloadFutureMap.remove(downloadKey.toString()),
966                 sequentialControlExecutor)
967             .transformAsync(
968                 unused -> {
969                   ClientFileGroup clientFileGroup = getDone(downloadTaskFuture);
970 
971                   if (downloadFileGroupRequest.listenerOptional().isPresent()) {
972                     try {
973                       downloadFileGroupRequest.listenerOptional().get().onComplete(clientFileGroup);
974                     } catch (Exception e) {
975                       LogUtil.w(
976                           e,
977                           "%s: Listener onComplete failed for group %s",
978                           TAG,
979                           clientFileGroup.getGroupName());
980                     }
981                     if (downloadMonitorOptional.isPresent()) {
982                       downloadMonitorOptional.get().removeDownloadListener(groupName);
983                     }
984                   }
985                   return immediateFuture(clientFileGroup);
986                 },
987                 sequentialControlExecutor);
988 
989     PropagatedFutures.addCallback(
990         transformFuture,
991         new FutureCallback<ClientFileGroup>() {
992           @Override
993           public void onSuccess(ClientFileGroup result) {}
994 
995           @Override
996           public void onFailure(Throwable t) {
997             if (downloadFileGroupRequest.listenerOptional().isPresent()) {
998               downloadFileGroupRequest.listenerOptional().get().onFailure(t);
999 
1000               if (downloadMonitorOptional.isPresent()) {
1001                 downloadMonitorOptional.get().removeDownloadListener(groupName);
1002               }
1003             }
1004 
1005             // Remove future from map
1006             ListenableFuture<Void> unused = downloadFutureMap.remove(downloadKey.toString());
1007           }
1008         },
1009         sequentialControlExecutor);
1010 
1011     return transformFuture;
1012   }
1013 
1014   @Override
1015   public ListenableFuture<Void> downloadFileWithForegroundService(
1016       SingleFileDownloadRequest singleFileDownloadRequest) {
1017     return singleFileDownloader.downloadWithForegroundService(
1018         MddLiteConversionUtil.convertToDownloadRequest(singleFileDownloadRequest));
1019   }
1020 
1021   @Override
1022   public ListenableFuture<ClientFileGroup> downloadFileGroupWithForegroundService(
1023       DownloadFileGroupRequest downloadFileGroupRequest) {
1024     LogUtil.d("%s: downloadFileGroupWithForegroundService start.", TAG);
1025     if (!foregroundDownloadServiceClassOptional.isPresent()) {
1026       return immediateFailedFuture(
1027           new IllegalArgumentException(
1028               "downloadFileGroupWithForegroundService: ForegroundDownloadService is not"
1029                   + " provided!"));
1030     }
1031 
1032     if (!downloadMonitorOptional.isPresent()) {
1033       return immediateFailedFuture(
1034           DownloadException.builder()
1035               .setDownloadResultCode(DownloadResultCode.DOWNLOAD_MONITOR_NOT_PROVIDED_ERROR)
1036               .setMessage(
1037                   "downloadFileGroupWithForegroundService: Download Monitor is not provided!")
1038               .build());
1039     }
1040 
1041     // Submit the call to sequentialControlExecutor, but don't use futureSerializer. This will
1042     // ensure that multiple calls are enqueued to the executor in a FIFO order, but these calls
1043     // won't block each other when the download is in progress.
1044     return PropagatedFutures.submitAsync(
1045         () ->
1046             PropagatedFutures.transformAsync(
1047                 // Check if requested file group has already been downloaded
1048                 getDownloadGroupState(downloadFileGroupRequest),
1049                 downloadGroupState -> {
1050                   switch (downloadGroupState.getKind()) {
1051                     case IN_PROGRESS_FUTURE:
1052                       // If the file group download is in progress, return that future immediately
1053                       return downloadGroupState.inProgressFuture();
1054                     case DOWNLOADED_GROUP:
1055                       // If the file group is already downloaded, return that immediately
1056                       return immediateFuture(downloadGroupState.downloadedGroup());
1057                     case PENDING_GROUP:
1058                       return downloadPendingFileGroupWithForegroundService(
1059                           downloadFileGroupRequest, downloadGroupState.pendingGroup());
1060                   }
1061                   throw new AssertionError(
1062                       String.format(
1063                           "received unsupported DownloadGroupState kind %s",
1064                           downloadGroupState.getKind()));
1065                 },
1066                 sequentialControlExecutor),
1067         sequentialControlExecutor);
1068   }
1069 
1070   /**
1071    * Helper method to download a file group in the foreground after it has been confirmed to be
1072    * pending.
1073    */
1074   private ListenableFuture<ClientFileGroup> downloadPendingFileGroupWithForegroundService(
1075       DownloadFileGroupRequest downloadFileGroupRequest, DataFileGroupInternal pendingGroup) {
1076     // It's OK to recreate the NotificationChannel since it can also be used to restore a
1077     // deleted channel and to update an existing channel's name, description, group, and/or
1078     // importance.
1079     NotificationUtil.createNotificationChannel(context);
1080 
1081     String groupName = downloadFileGroupRequest.groupName();
1082     GroupKey.Builder groupKeyBuilder =
1083         GroupKey.newBuilder().setGroupName(groupName).setOwnerPackage(context.getPackageName());
1084 
1085     if (downloadFileGroupRequest.accountOptional().isPresent()) {
1086       groupKeyBuilder.setAccount(
1087           AccountUtil.serialize(downloadFileGroupRequest.accountOptional().get()));
1088     }
1089     if (downloadFileGroupRequest.variantIdOptional().isPresent()) {
1090       groupKeyBuilder.setVariantId(downloadFileGroupRequest.variantIdOptional().get());
1091     }
1092 
1093     GroupKey groupKey = groupKeyBuilder.build();
1094     ForegroundDownloadKey foregroundDownloadKey =
1095         ForegroundDownloadKey.ofFileGroup(
1096             groupName,
1097             downloadFileGroupRequest.accountOptional(),
1098             downloadFileGroupRequest.variantIdOptional());
1099 
1100     DownloadListener downloadListenerWithNotification =
1101         createDownloadListenerWithNotification(downloadFileGroupRequest, pendingGroup);
1102     // The downloadMonitor will trigger the DownloadListener.
1103     downloadMonitorOptional
1104         .get()
1105         .addDownloadListener(
1106             downloadFileGroupRequest.groupName(), downloadListenerWithNotification);
1107 
1108     Optional<DownloadConditions> downloadConditions;
1109     try {
1110       downloadConditions =
1111           downloadFileGroupRequest.downloadConditionsOptional().isPresent()
1112               ? Optional.of(
1113                   ProtoConversionUtil.convert(
1114                       downloadFileGroupRequest.downloadConditionsOptional().get()))
1115               : Optional.absent();
1116     } catch (InvalidProtocolBufferException e) {
1117       return immediateFailedFuture(e);
1118     }
1119 
1120     // Create a ListenableFutureTask to delay starting the downloadFuture until we can add the
1121     // future to our map.
1122     ListenableFutureTask<Void> startTask = ListenableFutureTask.create(() -> null);
1123     PropagatedFluentFuture<ClientFileGroup> downloadFileGroupFuture =
1124         PropagatedFluentFuture.from(startTask)
1125             .transformAsync(
1126                 unused ->
1127                     mobileDataDownloadManager.downloadFileGroup(
1128                         groupKey, downloadConditions, customFileGroupValidator),
1129                 sequentialControlExecutor)
1130             .transformAsync(
1131                 dataFileGroup ->
1132                     createClientFileGroup(
1133                         dataFileGroup,
1134                         downloadFileGroupRequest.accountOptional().isPresent()
1135                             ? AccountUtil.serialize(
1136                                 downloadFileGroupRequest.accountOptional().get())
1137                             : null,
1138                         ClientFileGroup.Status.DOWNLOADED,
1139                         downloadFileGroupRequest.preserveZipDirectories(),
1140                         downloadFileGroupRequest.verifyIsolatedStructure(),
1141                         mobileDataDownloadManager,
1142                         sequentialControlExecutor,
1143                         fileStorage),
1144                 sequentialControlExecutor)
1145             .transform(Preconditions::checkNotNull, sequentialControlExecutor);
1146 
1147     ListenableFuture<ClientFileGroup> transformFuture =
1148         PropagatedFutures.transformAsync(
1149             foregroundDownloadFutureMap.add(
1150                 foregroundDownloadKey.toString(), downloadFileGroupFuture),
1151             unused -> {
1152               // Now that the download future is added, start the task and return the future
1153               startTask.run();
1154               return downloadFileGroupFuture;
1155             },
1156             sequentialControlExecutor);
1157 
1158     PropagatedFutures.addCallback(
1159         transformFuture,
1160         new FutureCallback<ClientFileGroup>() {
1161           @Override
1162           public void onSuccess(ClientFileGroup clientFileGroup) {
1163             // Currently the MobStore monitor does not support onSuccess so we have to add
1164             // callback to the download future here.
1165             try {
1166               downloadListenerWithNotification.onComplete(clientFileGroup);
1167             } catch (Exception e) {
1168               LogUtil.w(
1169                   e,
1170                   "%s: Listener onComplete failed for group %s",
1171                   TAG,
1172                   clientFileGroup.getGroupName());
1173             }
1174           }
1175 
1176           @Override
1177           public void onFailure(Throwable t) {
1178             // Currently the MobStore monitor does not support onFailure so we have to add
1179             // callback to the download future here.
1180             downloadListenerWithNotification.onFailure(t);
1181           }
1182         },
1183         sequentialControlExecutor);
1184 
1185     return transformFuture;
1186   }
1187 
1188   /** Helper method to return a {@link DownloadGroupState} for the given request. */
1189   private ListenableFuture<DownloadGroupState> getDownloadGroupState(
1190       DownloadFileGroupRequest downloadFileGroupRequest) {
1191     ForegroundDownloadKey foregroundDownloadKey =
1192         ForegroundDownloadKey.ofFileGroup(
1193             downloadFileGroupRequest.groupName(),
1194             downloadFileGroupRequest.accountOptional(),
1195             downloadFileGroupRequest.variantIdOptional());
1196 
1197     String groupName = downloadFileGroupRequest.groupName();
1198     GroupKey.Builder groupKeyBuilder =
1199         GroupKey.newBuilder().setGroupName(groupName).setOwnerPackage(context.getPackageName());
1200 
1201     if (downloadFileGroupRequest.accountOptional().isPresent()) {
1202       groupKeyBuilder.setAccount(
1203           AccountUtil.serialize(downloadFileGroupRequest.accountOptional().get()));
1204     }
1205 
1206     if (downloadFileGroupRequest.variantIdOptional().isPresent()) {
1207       groupKeyBuilder.setVariantId(downloadFileGroupRequest.variantIdOptional().get());
1208     }
1209 
1210     boolean isDownloadListenerPresent = downloadFileGroupRequest.listenerOptional().isPresent();
1211     GroupKey groupKey = groupKeyBuilder.build();
1212 
1213     return futureSerializer.submitAsync(
1214         () -> {
1215           ListenableFuture<Optional<ListenableFuture<ClientFileGroup>>>
1216               foregroundDownloadFutureOptional =
1217                   foregroundDownloadFutureMap.get(foregroundDownloadKey.toString());
1218           ListenableFuture<Optional<ListenableFuture<ClientFileGroup>>>
1219               backgroundDownloadFutureOptional =
1220                   downloadFutureMap.get(foregroundDownloadKey.toString());
1221 
1222           return PropagatedFutures.whenAllSucceed(
1223                   foregroundDownloadFutureOptional, backgroundDownloadFutureOptional)
1224               .callAsync(
1225                   () -> {
1226                     if (getDone(foregroundDownloadFutureOptional).isPresent()) {
1227                       return immediateFuture(
1228                           DownloadGroupState.ofInProgressFuture(
1229                               getDone(foregroundDownloadFutureOptional).get()));
1230                     } else if (getDone(backgroundDownloadFutureOptional).isPresent()) {
1231                       return immediateFuture(
1232                           DownloadGroupState.ofInProgressFuture(
1233                               getDone(backgroundDownloadFutureOptional).get()));
1234                     }
1235 
1236                     // Get pending and downloaded versions to tell if we should return downloaded
1237                     // version early
1238                     ListenableFuture<GroupPair> fileGroupVersionsFuture =
1239                         PropagatedFutures.transformAsync(
1240                             mobileDataDownloadManager.getFileGroup(
1241                                 groupKey, /* downloaded= */ false),
1242                             pendingDataFileGroup ->
1243                                 PropagatedFutures.transform(
1244                                     mobileDataDownloadManager.getFileGroup(
1245                                         groupKey, /* downloaded= */ true),
1246                                     downloadedDataFileGroup ->
1247                                         GroupPair.create(
1248                                             pendingDataFileGroup, downloadedDataFileGroup),
1249                                     sequentialControlExecutor),
1250                             sequentialControlExecutor);
1251 
1252                     return PropagatedFutures.transformAsync(
1253                         fileGroupVersionsFuture,
1254                         fileGroupVersionsPair -> {
1255                           // if pending version is not null, return pending version
1256                           if (fileGroupVersionsPair.pendingGroup() != null) {
1257                             return immediateFuture(
1258                                 DownloadGroupState.ofPendingGroup(
1259                                     checkNotNull(fileGroupVersionsPair.pendingGroup())));
1260                           }
1261                           // If both groups are null, return group not found failure
1262                           if (fileGroupVersionsPair.downloadedGroup() == null) {
1263                             // TODO(b/174808410): Add Logging
1264                             // file group is not pending nor downloaded -- return failure.
1265                             DownloadException failure =
1266                                 DownloadException.builder()
1267                                     .setDownloadResultCode(DownloadResultCode.GROUP_NOT_FOUND_ERROR)
1268                                     .setMessage(
1269                                         "Nothing to download for file group: "
1270                                             + groupKey.getGroupName())
1271                                     .build();
1272                             if (isDownloadListenerPresent) {
1273                               downloadFileGroupRequest.listenerOptional().get().onFailure(failure);
1274                             }
1275                             return immediateFailedFuture(failure);
1276                           }
1277 
1278                           DataFileGroupInternal downloadedDataFileGroup =
1279                               checkNotNull(fileGroupVersionsPair.downloadedGroup());
1280 
1281                           // Notify download listener (if present) that file group has been
1282                           // downloaded.
1283                           if (isDownloadListenerPresent) {
1284                             downloadMonitorOptional
1285                                 .get()
1286                                 .addDownloadListener(
1287                                     downloadFileGroupRequest.groupName(),
1288                                     downloadFileGroupRequest.listenerOptional().get());
1289                           }
1290                           PropagatedFluentFuture<ClientFileGroup> transformFuture =
1291                               PropagatedFluentFuture.from(
1292                                       createClientFileGroup(
1293                                           downloadedDataFileGroup,
1294                                           downloadFileGroupRequest.accountOptional().isPresent()
1295                                               ? AccountUtil.serialize(
1296                                                   downloadFileGroupRequest.accountOptional().get())
1297                                               : null,
1298                                           ClientFileGroup.Status.DOWNLOADED,
1299                                           downloadFileGroupRequest.preserveZipDirectories(),
1300                                           downloadFileGroupRequest.verifyIsolatedStructure(),
1301                                           mobileDataDownloadManager,
1302                                           sequentialControlExecutor,
1303                                           fileStorage))
1304                                   .transform(Preconditions::checkNotNull, sequentialControlExecutor)
1305                                   .transform(
1306                                       clientFileGroup -> {
1307                                         if (isDownloadListenerPresent) {
1308                                           try {
1309                                             downloadFileGroupRequest
1310                                                 .listenerOptional()
1311                                                 .get()
1312                                                 .onComplete(clientFileGroup);
1313                                           } catch (Exception e) {
1314                                             LogUtil.w(
1315                                                 e,
1316                                                 "%s: Listener onComplete failed for group %s",
1317                                                 TAG,
1318                                                 clientFileGroup.getGroupName());
1319                                           }
1320                                           downloadMonitorOptional
1321                                               .get()
1322                                               .removeDownloadListener(groupName);
1323                                         }
1324                                         return clientFileGroup;
1325                                       },
1326                                       sequentialControlExecutor);
1327                           transformFuture.addCallback(
1328                               new FutureCallback<ClientFileGroup>() {
1329                                 @Override
1330                                 public void onSuccess(ClientFileGroup result) {}
1331 
1332                                 @Override
1333                                 public void onFailure(Throwable t) {
1334                                   if (isDownloadListenerPresent) {
1335                                     downloadMonitorOptional.get().removeDownloadListener(groupName);
1336                                   }
1337                                 }
1338                               },
1339                               sequentialControlExecutor);
1340 
1341                           // Use directExecutor here since we are performing a trivial operation.
1342                           return transformFuture.transform(
1343                               DownloadGroupState::ofDownloadedGroup, directExecutor());
1344                         },
1345                         sequentialControlExecutor);
1346                   },
1347                   sequentialControlExecutor);
1348         },
1349         sequentialControlExecutor);
1350   }
1351 
1352   private DownloadListener createDownloadListenerWithNotification(
1353       DownloadFileGroupRequest downloadRequest, DataFileGroupInternal fileGroup) {
1354 
1355     String networkPausedMessage = getNetworkPausedMessage(downloadRequest, fileGroup);
1356 
1357     NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
1358     ForegroundDownloadKey foregroundDownloadKey =
1359         ForegroundDownloadKey.ofFileGroup(
1360             downloadRequest.groupName(),
1361             downloadRequest.accountOptional(),
1362             downloadRequest.variantIdOptional());
1363 
1364     NotificationCompat.Builder notification =
1365         NotificationUtil.createNotificationBuilder(
1366             context,
1367             downloadRequest.groupSizeBytes(),
1368             downloadRequest.contentTitleOptional().or(downloadRequest.groupName()),
1369             downloadRequest.contentTextOptional().or(downloadRequest.groupName()));
1370     int notificationKey = NotificationUtil.notificationKeyForKey(downloadRequest.groupName());
1371 
1372     if (downloadRequest.showNotifications() == DownloadFileGroupRequest.ShowNotifications.ALL) {
1373       NotificationUtil.createCancelAction(
1374           context,
1375           foregroundDownloadServiceClassOptional.get(),
1376           foregroundDownloadKey.toString(),
1377           notification,
1378           notificationKey);
1379 
1380       notificationManager.notify(notificationKey, notification.build());
1381     }
1382 
1383     return new DownloadListener() {
1384       @Override
1385       public void onProgress(long currentSize) {
1386         // TODO(b/229123693): return this future once DownloadListener has an async api.
1387         // There can be a race condition, where onProgress can be called
1388         // after onComplete or onFailure which removes the future and the notification.
1389         // Check foregroundDownloadFutureMap first before updating notification.
1390         ListenableFuture<?> unused =
1391             PropagatedFutures.transformAsync(
1392                 foregroundDownloadFutureMap.containsKey(foregroundDownloadKey.toString()),
1393                 futureInProgress -> {
1394                   if (futureInProgress
1395                       && downloadRequest.showNotifications()
1396                           == DownloadFileGroupRequest.ShowNotifications.ALL) {
1397                     notification
1398                         .setCategory(NotificationCompat.CATEGORY_PROGRESS)
1399                         .setSmallIcon(android.R.drawable.stat_sys_download)
1400                         .setProgress(
1401                             downloadRequest.groupSizeBytes(),
1402                             (int) currentSize,
1403                             /* indeterminate= */ downloadRequest.groupSizeBytes() <= 0);
1404                     notificationManager.notify(notificationKey, notification.build());
1405                   }
1406                   if (downloadRequest.listenerOptional().isPresent()) {
1407                     downloadRequest.listenerOptional().get().onProgress(currentSize);
1408                   }
1409                   return immediateVoidFuture();
1410                 },
1411                 sequentialControlExecutor);
1412       }
1413 
1414       @Override
1415       public void pausedForConnectivity() {
1416         // TODO(b/229123693): return this future once DownloadListener has an async api.
1417         // There can be a race condition, where pausedForConnectivity can be called
1418         // after onComplete or onFailure which removes the future and the notification.
1419         // Check foregroundDownloadFutureMap first before updating notification.
1420         ListenableFuture<?> unused =
1421             PropagatedFutures.transformAsync(
1422                 foregroundDownloadFutureMap.containsKey(foregroundDownloadKey.toString()),
1423                 futureInProgress -> {
1424                   if (futureInProgress
1425                       && downloadRequest.showNotifications()
1426                           == DownloadFileGroupRequest.ShowNotifications.ALL) {
1427                     notification
1428                         .setCategory(NotificationCompat.CATEGORY_STATUS)
1429                         .setContentText(networkPausedMessage)
1430                         .setSmallIcon(android.R.drawable.stat_sys_download)
1431                         .setOngoing(true)
1432                         // hide progress bar.
1433                         .setProgress(0, 0, false);
1434                     notificationManager.notify(notificationKey, notification.build());
1435                   }
1436                   if (downloadRequest.listenerOptional().isPresent()) {
1437                     downloadRequest.listenerOptional().get().pausedForConnectivity();
1438                   }
1439                   return immediateVoidFuture();
1440                 },
1441                 sequentialControlExecutor);
1442       }
1443 
1444       @Override
1445       public void onComplete(ClientFileGroup clientFileGroup) {
1446         // TODO(b/229123693): return this future once DownloadListener has an async api.
1447         ListenableFuture<?> unused =
1448             PropagatedFutures.submitAsync(
1449                 () -> {
1450                   boolean onCompleteFailed = false;
1451                   if (downloadRequest.listenerOptional().isPresent()) {
1452                     try {
1453                       downloadRequest.listenerOptional().get().onComplete(clientFileGroup);
1454                     } catch (Exception e) {
1455                       LogUtil.w(
1456                           e,
1457                           "%s: Delegate onComplete failed for group %s, showing failure"
1458                               + " notification.",
1459                           TAG,
1460                           clientFileGroup.getGroupName());
1461                       onCompleteFailed = true;
1462                     }
1463                   }
1464 
1465                   // Clear the notification action.
1466                   if (downloadRequest.showNotifications()
1467                       == DownloadFileGroupRequest.ShowNotifications.ALL) {
1468                     notification.mActions.clear();
1469 
1470                     if (onCompleteFailed) {
1471                       // Show download failed in notification.
1472                       notification
1473                           .setCategory(NotificationCompat.CATEGORY_STATUS)
1474                           .setContentText(NotificationUtil.getDownloadFailedMessage(context))
1475                           .setOngoing(false)
1476                           .setSmallIcon(android.R.drawable.stat_sys_warning)
1477                           // hide progress bar.
1478                           .setProgress(0, 0, false);
1479 
1480                       notificationManager.notify(notificationKey, notification.build());
1481                     } else {
1482                       NotificationUtil.cancelNotificationForKey(
1483                           context, downloadRequest.groupName());
1484                     }
1485                   }
1486 
1487                   downloadMonitorOptional.get().removeDownloadListener(downloadRequest.groupName());
1488 
1489                   return foregroundDownloadFutureMap.remove(foregroundDownloadKey.toString());
1490                 },
1491                 sequentialControlExecutor);
1492       }
1493 
1494       @Override
1495       public void onFailure(Throwable t) {
1496         // TODO(b/229123693): return this future once DownloadListener has an async api.
1497         ListenableFuture<?> unused =
1498             PropagatedFutures.submitAsync(
1499                 () -> {
1500                   if (downloadRequest.showNotifications()
1501                       == DownloadFileGroupRequest.ShowNotifications.ALL) {
1502                     // Clear the notification action.
1503                     notification.mActions.clear();
1504 
1505                     // Show download failed in notification.
1506                     notification
1507                         .setCategory(NotificationCompat.CATEGORY_STATUS)
1508                         .setContentText(NotificationUtil.getDownloadFailedMessage(context))
1509                         .setOngoing(false)
1510                         .setSmallIcon(android.R.drawable.stat_sys_warning)
1511                         // hide progress bar.
1512                         .setProgress(0, 0, false);
1513 
1514                     notificationManager.notify(notificationKey, notification.build());
1515                   }
1516 
1517                   if (downloadRequest.listenerOptional().isPresent()) {
1518                     downloadRequest.listenerOptional().get().onFailure(t);
1519                   }
1520                   downloadMonitorOptional.get().removeDownloadListener(downloadRequest.groupName());
1521 
1522                   return foregroundDownloadFutureMap.remove(foregroundDownloadKey.toString());
1523                 },
1524                 sequentialControlExecutor);
1525       }
1526     };
1527   }
1528 
1529   // Helper method to get the correct network paused message
1530   private String getNetworkPausedMessage(
1531       DownloadFileGroupRequest downloadRequest, DataFileGroupInternal fileGroup) {
1532     DeviceNetworkPolicy networkPolicyForDownload =
1533         fileGroup.getDownloadConditions().getDeviceNetworkPolicy();
1534     if (downloadRequest.downloadConditionsOptional().isPresent()) {
1535       try {
1536         networkPolicyForDownload =
1537             ProtoConversionUtil.convert(downloadRequest.downloadConditionsOptional().get())
1538                 .getDeviceNetworkPolicy();
1539       } catch (InvalidProtocolBufferException unused) {
1540         // Do nothing -- we will rely on the file group's network policy.
1541       }
1542     }
1543 
1544     switch (networkPolicyForDownload) {
1545       case DOWNLOAD_FIRST_ON_WIFI_THEN_ON_ANY_NETWORK: // fallthrough
1546       case DOWNLOAD_ONLY_ON_WIFI:
1547         return NotificationUtil.getDownloadPausedWifiMessage(context);
1548       default:
1549         return NotificationUtil.getDownloadPausedMessage(context);
1550     }
1551   }
1552 
1553   @Override
1554   public void cancelForegroundDownload(String downloadKey) {
1555     LogUtil.d("%s: CancelForegroundDownload for key = %s", TAG, downloadKey);
1556     ListenableFuture<?> unused =
1557         PropagatedFutures.transformAsync(
1558             foregroundDownloadFutureMap.get(downloadKey),
1559             downloadFuture -> {
1560               if (downloadFuture.isPresent()) {
1561                 LogUtil.v(
1562                     "%s: CancelForegroundDownload future found for key = %s, cancelling...",
1563                     TAG, downloadKey);
1564                 downloadFuture.get().cancel(false);
1565               }
1566               return immediateVoidFuture();
1567             },
1568             sequentialControlExecutor);
1569     // Attempt cancel with internal MDD Lite instance in case it's a single file uri (cancel call is
1570     // a noop if internal MDD Lite doesn't know about it).
1571     singleFileDownloader.cancelForegroundDownload(downloadKey);
1572   }
1573 
1574   @Override
1575   public void schedulePeriodicTasks() {
1576     schedulePeriodicTasksInternal(Optional.absent());
1577   }
1578 
1579   @Override
1580   public ListenableFuture<Void> schedulePeriodicBackgroundTasks() {
1581     return futureSerializer.submit(
1582         () -> {
1583           schedulePeriodicTasksInternal(/* constraintOverridesMap= */ Optional.absent());
1584           return null;
1585         },
1586         sequentialControlExecutor);
1587   }
1588 
1589   @Override
1590   public ListenableFuture<Void> schedulePeriodicBackgroundTasks(
1591       Optional<Map<String, ConstraintOverrides>> constraintOverridesMap) {
1592     return futureSerializer.submit(
1593         () -> {
1594           schedulePeriodicTasksInternal(constraintOverridesMap);
1595           return null;
1596         },
1597         sequentialControlExecutor);
1598   }
1599 
1600   private void schedulePeriodicTasksInternal(
1601       Optional<Map<String, ConstraintOverrides>> constraintOverridesMap) {
1602     if (!taskSchedulerOptional.isPresent()) {
1603       LogUtil.e(
1604           "%s: Called schedulePeriodicTasksInternal when taskScheduler is not provided.", TAG);
1605       return;
1606     }
1607 
1608     TaskScheduler taskScheduler = taskSchedulerOptional.get();
1609 
1610     // Schedule task that runs on charging without any network, every 6 hours.
1611     taskScheduler.schedulePeriodicTask(
1612         TaskScheduler.CHARGING_PERIODIC_TASK,
1613         flags.chargingGcmTaskPeriod(),
1614         NetworkState.NETWORK_STATE_ANY,
1615         getConstraintOverrides(constraintOverridesMap, TaskScheduler.CHARGING_PERIODIC_TASK));
1616 
1617     // Schedule maintenance task that runs on charging, once every day.
1618     // This task should run even if mdd is disabled, to handle cleanup.
1619     taskScheduler.schedulePeriodicTask(
1620         TaskScheduler.MAINTENANCE_PERIODIC_TASK,
1621         flags.maintenanceGcmTaskPeriod(),
1622         NetworkState.NETWORK_STATE_ANY,
1623         getConstraintOverrides(constraintOverridesMap, TaskScheduler.MAINTENANCE_PERIODIC_TASK));
1624 
1625     // Schedule task that runs on cellular+charging, every 6 hours.
1626     taskScheduler.schedulePeriodicTask(
1627         TaskScheduler.CELLULAR_CHARGING_PERIODIC_TASK,
1628         flags.cellularChargingGcmTaskPeriod(),
1629         NetworkState.NETWORK_STATE_CONNECTED,
1630         getConstraintOverrides(
1631             constraintOverridesMap, TaskScheduler.CELLULAR_CHARGING_PERIODIC_TASK));
1632 
1633     // Schedule task that runs on wifi+charging, every 6 hours.
1634     taskScheduler.schedulePeriodicTask(
1635         TaskScheduler.WIFI_CHARGING_PERIODIC_TASK,
1636         flags.wifiChargingGcmTaskPeriod(),
1637         NetworkState.NETWORK_STATE_UNMETERED,
1638         getConstraintOverrides(constraintOverridesMap, TaskScheduler.WIFI_CHARGING_PERIODIC_TASK));
1639   }
1640 
1641   private static Optional<ConstraintOverrides> getConstraintOverrides(
1642       Optional<Map<String, ConstraintOverrides>> constraintOverridesMap,
1643       String maintenancePeriodicTask) {
1644     return constraintOverridesMap.isPresent()
1645         ? Optional.fromNullable(constraintOverridesMap.get().get(maintenancePeriodicTask))
1646         : Optional.absent();
1647   }
1648 
1649   @Override
1650   public ListenableFuture<Void> cancelPeriodicBackgroundTasks() {
1651     return futureSerializer.submit(
1652         () -> {
1653           cancelPeriodicTasksInternal();
1654           return null;
1655         },
1656         sequentialControlExecutor);
1657   }
1658 
1659   private void cancelPeriodicTasksInternal() {
1660     if (!taskSchedulerOptional.isPresent()) {
1661       LogUtil.w("%s: Called cancelPeriodicTasksInternal when taskScheduler is not provided.", TAG);
1662       return;
1663     }
1664 
1665     TaskScheduler taskScheduler = taskSchedulerOptional.get();
1666 
1667     taskScheduler.cancelPeriodicTask(TaskScheduler.CHARGING_PERIODIC_TASK);
1668     taskScheduler.cancelPeriodicTask(TaskScheduler.MAINTENANCE_PERIODIC_TASK);
1669     taskScheduler.cancelPeriodicTask(TaskScheduler.CELLULAR_CHARGING_PERIODIC_TASK);
1670     taskScheduler.cancelPeriodicTask(TaskScheduler.WIFI_CHARGING_PERIODIC_TASK);
1671   }
1672 
1673   @Override
1674   public ListenableFuture<Void> handleTask(String tag) {
1675     // All work done here that touches metadata (MobileDataDownloadManager) should be serialized
1676     // through sequentialControlExecutor.
1677     switch (tag) {
1678       case TaskScheduler.MAINTENANCE_PERIODIC_TASK:
1679         return futureSerializer.submitAsync(
1680             mobileDataDownloadManager::maintenance, sequentialControlExecutor);
1681 
1682       case TaskScheduler.CHARGING_PERIODIC_TASK:
1683         ListenableFuture<Void> refreshFileGroupsFuture = refreshFileGroups();
1684         return PropagatedFutures.transformAsync(
1685             refreshFileGroupsFuture,
1686             propagateAsyncFunction(
1687                 v -> mobileDataDownloadManager.verifyAllPendingGroups(customFileGroupValidator)),
1688             sequentialControlExecutor);
1689 
1690       case TaskScheduler.CELLULAR_CHARGING_PERIODIC_TASK:
1691         return refreshAndDownload(false /*onWifi*/);
1692 
1693       case TaskScheduler.WIFI_CHARGING_PERIODIC_TASK:
1694         return refreshAndDownload(true /*onWifi*/);
1695 
1696       default:
1697         LogUtil.d("%s: gcm task doesn't belong to MDD", TAG);
1698         return immediateFailedFuture(
1699             new IllegalArgumentException("Unknown task tag sent to MDD.handleTask() " + tag));
1700     }
1701   }
1702 
1703   private ListenableFuture<Void> refreshAndDownload(boolean onWifi) {
1704     // We will do 2 passes to support 2-step downloads. In each step, we will refresh and then
1705     // download.
1706     return PropagatedFluentFuture.from(refreshFileGroups())
1707         .transformAsync(
1708             v ->
1709                 mobileDataDownloadManager.downloadAllPendingGroups(
1710                     onWifi, customFileGroupValidator),
1711             sequentialControlExecutor)
1712         .transformAsync(v -> refreshFileGroups(), sequentialControlExecutor)
1713         .transformAsync(
1714             v ->
1715                 mobileDataDownloadManager.downloadAllPendingGroups(
1716                     onWifi, customFileGroupValidator),
1717             sequentialControlExecutor);
1718   }
1719 
1720   private ListenableFuture<Void> refreshFileGroups() {
1721     List<ListenableFuture<Void>> refreshFutures = new ArrayList<>();
1722     for (FileGroupPopulator fileGroupPopulator : fileGroupPopulatorList) {
1723       refreshFutures.add(fileGroupPopulator.refreshFileGroups(this));
1724     }
1725 
1726     return PropagatedFutures.whenAllComplete(refreshFutures)
1727         .call(() -> null, sequentialControlExecutor);
1728   }
1729 
1730   @Override
1731   public ListenableFuture<Void> maintenance() {
1732     return handleTask(TaskScheduler.MAINTENANCE_PERIODIC_TASK);
1733   }
1734 
1735   @Override
1736   public ListenableFuture<Void> collectGarbage() {
1737     return futureSerializer.submitAsync(
1738         mobileDataDownloadManager::removeExpiredGroupsAndFiles, sequentialControlExecutor);
1739   }
1740 
1741   @Override
1742   public ListenableFuture<Void> clear() {
1743     return futureSerializer.submitAsync(
1744         mobileDataDownloadManager::clear, sequentialControlExecutor);
1745   }
1746 
1747   // incompatible argument for parameter msg of e.
1748   // incompatible types in return.
1749   @Override
1750   public String getDebugInfoAsString() {
1751     ByteArrayOutputStream out = new ByteArrayOutputStream();
1752     PrintWriter writer = new PrintWriter(out);
1753     try {
1754       // Okay to block here because this method is for debugging only.
1755       mobileDataDownloadManager.dump(writer).get(DUMP_DEBUG_INFO_TIMEOUT, TimeUnit.SECONDS);
1756       writer.println("==== MOBSTORE_DEBUG_INFO ====");
1757       writer.print(fileStorage.getDebugInfo());
1758     } catch (ExecutionException | TimeoutException e) {
1759       String errString = String.format("%s: Couldn't get debug info: %s", TAG, e);
1760       LogUtil.e(errString);
1761       return errString;
1762     } catch (InterruptedException e) {
1763       // see <internal>
1764       Thread.currentThread().interrupt();
1765       String errString = String.format("%s: Couldn't get debug info: %s", TAG, e);
1766       LogUtil.e(errString);
1767       return errString;
1768     }
1769     writer.flush();
1770     return out.toString();
1771   }
1772 
1773   @Override
1774   public ListenableFuture<Void> reportUsage(UsageEvent usageEvent) {
1775     eventLogger.logMddUsageEvent(createFileGroupDetails(usageEvent.clientFileGroup()), null);
1776 
1777     return immediateVoidFuture();
1778   }
1779 
1780   private static DownloadFutureMap.StateChangeCallbacks createCallbacksForForegroundService(
1781       Context context, Optional<Class<?>> foregroundDownloadServiceClassOptional) {
1782     return new DownloadFutureMap.StateChangeCallbacks() {
1783       @Override
1784       public void onAdd(String key, int newSize) {
1785         // Only start foreground service if this is the first future we are adding.
1786         if (newSize == 1 && foregroundDownloadServiceClassOptional.isPresent()) {
1787           NotificationUtil.startForegroundDownloadService(
1788               context, foregroundDownloadServiceClassOptional.get(), key);
1789         }
1790       }
1791 
1792       @Override
1793       public void onRemove(String key, int newSize) {
1794         // Only stop foreground service if there are no more futures remaining.
1795         if (newSize == 0 && foregroundDownloadServiceClassOptional.isPresent()) {
1796           NotificationUtil.stopForegroundDownloadService(
1797               context, foregroundDownloadServiceClassOptional.get(), key);
1798         }
1799       }
1800     };
1801   }
1802 }
1803