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