• 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.internal;
17 
18 import static com.google.android.libraries.mobiledatadownload.tracing.TracePropagation.propagateAsyncFunction;
19 import static com.google.common.base.Preconditions.checkNotNull;
20 import static com.google.common.util.concurrent.Futures.getDone;
21 import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
22 import static com.google.common.util.concurrent.Futures.immediateFuture;
23 import static com.google.common.util.concurrent.Futures.immediateVoidFuture;
24 import static java.lang.Math.max;
25 
26 import android.accounts.Account;
27 import android.annotation.TargetApi;
28 import android.content.Context;
29 import android.content.pm.PackageManager.NameNotFoundException;
30 import android.net.Uri;
31 import android.os.Build.VERSION;
32 import android.os.Build.VERSION_CODES;
33 import android.text.TextUtils;
34 import androidx.annotation.RequiresApi;
35 import com.google.android.libraries.mobiledatadownload.AccountSource;
36 import com.google.android.libraries.mobiledatadownload.AggregateException;
37 import com.google.android.libraries.mobiledatadownload.DownloadException;
38 import com.google.android.libraries.mobiledatadownload.DownloadException.DownloadResultCode;
39 import com.google.android.libraries.mobiledatadownload.FileSource;
40 import com.google.android.libraries.mobiledatadownload.Flags;
41 import com.google.android.libraries.mobiledatadownload.SilentFeedback;
42 import com.google.android.libraries.mobiledatadownload.TimeSource;
43 import com.google.android.libraries.mobiledatadownload.account.AccountUtil;
44 import com.google.android.libraries.mobiledatadownload.annotations.InstanceId;
45 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage;
46 import com.google.android.libraries.mobiledatadownload.internal.annotations.SequentialControlExecutor;
47 import com.google.android.libraries.mobiledatadownload.internal.collect.GroupKeyAndGroup;
48 import com.google.android.libraries.mobiledatadownload.internal.collect.GroupPair;
49 import com.google.android.libraries.mobiledatadownload.internal.experimentation.DownloadStageManager;
50 import com.google.android.libraries.mobiledatadownload.internal.logging.DownloadStateLogger;
51 import com.google.android.libraries.mobiledatadownload.internal.logging.DownloadStateLogger.Operation;
52 import com.google.android.libraries.mobiledatadownload.internal.logging.EventLogger;
53 import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil;
54 import com.google.android.libraries.mobiledatadownload.internal.util.AndroidSharingUtil;
55 import com.google.android.libraries.mobiledatadownload.internal.util.AndroidSharingUtil.AndroidSharingException;
56 import com.google.android.libraries.mobiledatadownload.internal.util.DirectoryUtil;
57 import com.google.android.libraries.mobiledatadownload.internal.util.FileGroupUtil;
58 import com.google.android.libraries.mobiledatadownload.internal.util.SymlinkUtil;
59 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedExecutionSequencer;
60 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFluentFuture;
61 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures;
62 import com.google.common.base.Function;
63 import com.google.common.base.Optional;
64 import com.google.common.base.Preconditions;
65 import com.google.common.collect.ComparisonChain;
66 import com.google.common.collect.ImmutableList;
67 import com.google.common.collect.ImmutableMap;
68 import com.google.common.collect.ImmutableSet;
69 import com.google.common.collect.Iterables;
70 import com.google.common.collect.Maps;
71 import com.google.common.util.concurrent.AsyncFunction;
72 import com.google.common.util.concurrent.FutureCallback;
73 import com.google.common.util.concurrent.Futures;
74 import com.google.common.util.concurrent.ListenableFuture;
75 import com.google.errorprone.annotations.CheckReturnValue;
76 import com.google.mobiledatadownload.internal.MetadataProto;
77 import com.google.mobiledatadownload.internal.MetadataProto.DataFile;
78 import com.google.mobiledatadownload.internal.MetadataProto.DataFile.AndroidSharingType;
79 import com.google.mobiledatadownload.internal.MetadataProto.DataFileGroupBookkeeping;
80 import com.google.mobiledatadownload.internal.MetadataProto.DataFileGroupInternal;
81 import com.google.mobiledatadownload.internal.MetadataProto.DownloadConditions;
82 import com.google.mobiledatadownload.internal.MetadataProto.DownloadConditions.ActivatingCondition;
83 import com.google.mobiledatadownload.internal.MetadataProto.DownloadConditions.DeviceNetworkPolicy;
84 import com.google.mobiledatadownload.internal.MetadataProto.FileStatus;
85 import com.google.mobiledatadownload.internal.MetadataProto.GroupKey;
86 import com.google.mobiledatadownload.internal.MetadataProto.GroupKeyProperties;
87 import com.google.mobiledatadownload.internal.MetadataProto.NewFileKey;
88 import com.google.mobiledatadownload.internal.MetadataProto.SharedFile;
89 import com.google.mobiledatadownload.LogEnumsProto.MddClientEvent;
90 import com.google.mobiledatadownload.LogEnumsProto.MddDownloadResult;
91 import com.google.mobiledatadownload.LogProto.DataDownloadFileGroupStats;
92 import com.google.protobuf.Any;
93 import java.io.IOException;
94 import java.io.PrintWriter;
95 import java.util.ArrayList;
96 import java.util.Collections;
97 import java.util.HashMap;
98 import java.util.HashSet;
99 import java.util.List;
100 import java.util.Map;
101 import java.util.Map.Entry;
102 import java.util.Set;
103 import java.util.concurrent.Executor;
104 import java.util.concurrent.atomic.AtomicReference;
105 import javax.annotation.Nullable;
106 import javax.inject.Inject;
107 import org.checkerframework.checker.nullness.compatqual.NullableType;
108 
109 /**
110  * Keeps track of pending groups to download and stores the downloaded groups for retrieval. It's
111  * not thread safe. Currently it works by being called from a single thread executor.
112  *
113  * <p>Also provides methods to register and verify download complete for all pending downloads.
114  */
115 @CheckReturnValue
116 public class FileGroupManager {
117 
118   /** The current state of the group. */
119   public enum GroupDownloadStatus {
120     /** At least one file has not downloaded fully, but no file download has failed. */
121     PENDING,
122 
123     /** All files have successfully downloaded and should now be fully available. */
124     DOWNLOADED,
125 
126     /** The download of at least one file failed. */
127     FAILED,
128 
129     /** The status of the group is unknown. */
130     UNKNOWN,
131   }
132 
133   private static final String TAG = "FileGroupManager";
134 
135   private final Context context;
136   private final EventLogger eventLogger;
137   private final SilentFeedback silentFeedback;
138   private final FileGroupsMetadata fileGroupsMetadata;
139   private final SharedFileManager sharedFileManager;
140   private final TimeSource timeSource;
141   private final SynchronousFileStorage fileStorage;
142   private final Optional<AccountSource> accountSourceOptional;
143   private final Executor sequentialControlExecutor;
144   private final Optional<String> instanceId;
145   private final DownloadStageManager downloadStageManager;
146   private final Flags flags;
147 
148   // Create an internal ExecutionSequencer to ensure that certain operations remain synced.
149   private final PropagatedExecutionSequencer futureSerializer =
150       PropagatedExecutionSequencer.create();
151 
152   @Inject
FileGroupManager( @pplicationContext Context context, EventLogger eventLogger, SilentFeedback silentFeedback, FileGroupsMetadata fileGroupsMetadata, SharedFileManager sharedFileManager, TimeSource timeSource, Optional<AccountSource> accountSourceOptional, @SequentialControlExecutor Executor sequentialControlExecutor, @InstanceId Optional<String> instanceId, SynchronousFileStorage fileStorage, DownloadStageManager downloadStageManager, Flags flags)153   public FileGroupManager(
154       @ApplicationContext Context context,
155       EventLogger eventLogger,
156       SilentFeedback silentFeedback,
157       FileGroupsMetadata fileGroupsMetadata,
158       SharedFileManager sharedFileManager,
159       TimeSource timeSource,
160       Optional<AccountSource> accountSourceOptional,
161       @SequentialControlExecutor Executor sequentialControlExecutor,
162       @InstanceId Optional<String> instanceId,
163       SynchronousFileStorage fileStorage,
164       DownloadStageManager downloadStageManager,
165       Flags flags) {
166     this.context = context;
167     this.eventLogger = eventLogger;
168     this.silentFeedback = silentFeedback;
169     this.fileGroupsMetadata = fileGroupsMetadata;
170     this.sharedFileManager = sharedFileManager;
171     this.timeSource = timeSource;
172     this.accountSourceOptional = accountSourceOptional;
173     this.sequentialControlExecutor = sequentialControlExecutor;
174     this.instanceId = instanceId;
175     this.fileStorage = fileStorage;
176     this.downloadStageManager = downloadStageManager;
177     this.flags = flags;
178   }
179 
180   /**
181    * Adds the given data file group for download.
182    *
183    * <p>Calling this method with the exact same file group multiple times is a no op.
184    *
185    * @param groupKey The key for the group.
186    * @param receivedGroup The File group that needs to be downloaded.
187    * @return A future that resolves to true if the received group was new/upgrade and was
188    *     successfully added, false otherwise.
189    */
190   // TODO(b/124072754): Change to package private once all code is refactored.
191   @SuppressWarnings("nullness")
addGroupForDownload( GroupKey groupKey, DataFileGroupInternal receivedGroup)192   public ListenableFuture<Boolean> addGroupForDownload(
193       GroupKey groupKey, DataFileGroupInternal receivedGroup)
194       throws ExpiredFileGroupException,
195           IOException,
196           UninstalledAppException,
197           ActivationRequiredForGroupException {
198     if (FileGroupUtil.isActiveGroupExpired(receivedGroup, timeSource)) {
199       LogUtil.e("%s: Trying to add expired group %s.", TAG, groupKey.getGroupName());
200       logEventWithDataFileGroup(
201           MddClientEvent.Code.EVENT_CODE_UNSPECIFIED, eventLogger, receivedGroup);
202       throw new ExpiredFileGroupException();
203     }
204     if (!isAppInstalled(groupKey.getOwnerPackage())) {
205       LogUtil.e(
206           "%s: Trying to add group %s for uninstalled app %s.",
207           TAG, groupKey.getGroupName(), groupKey.getOwnerPackage());
208       logEventWithDataFileGroup(
209           MddClientEvent.Code.EVENT_CODE_UNSPECIFIED, eventLogger, receivedGroup);
210       throw new UninstalledAppException();
211     }
212 
213     ListenableFuture<Boolean> resultFuture = immediateFuture(null);
214     if (flags.enableDelayedDownload()
215         && receivedGroup.getDownloadConditions().getActivatingCondition()
216             == ActivatingCondition.DEVICE_ACTIVATED) {
217 
218       resultFuture =
219           transformSequentialAsync(
220               fileGroupsMetadata.readGroupKeyProperties(groupKey),
221               groupKeyProperties -> {
222                 // It shouldn't make a difference if we found an existing value or not.
223                 if (groupKeyProperties == null) {
224                   groupKeyProperties = GroupKeyProperties.getDefaultInstance();
225                 }
226 
227                 if (!groupKeyProperties.getActivatedOnDevice()) {
228                   LogUtil.d(
229                       "%s: Trying to add group %s that requires activation %s.",
230                       TAG, groupKey.getGroupName(), groupKey.getOwnerPackage());
231 
232                   logEventWithDataFileGroup(
233                       MddClientEvent.Code.EVENT_CODE_UNSPECIFIED, eventLogger, receivedGroup);
234 
235                   throw new ActivationRequiredForGroupException();
236                 }
237                 return immediateFuture(null);
238               });
239     }
240 
241     return PropagatedFluentFuture.from(resultFuture)
242         .transformAsync(
243             voidArg -> isAddedGroupDuplicate(groupKey, receivedGroup), sequentialControlExecutor)
244         .transformAsync(
245             newConfigReason -> {
246               if (!newConfigReason.isPresent()) {
247                 // Absent reason means the config is not new
248                 LogUtil.d(
249                     "%s: Received duplicate config for group: %s", TAG, groupKey.getGroupName());
250                 return immediateFuture(false);
251               }
252 
253               // If supported, set the isolated root before writing to metadata
254               DataFileGroupInternal receivedGroupWithIsolatedRoot =
255                   FileGroupUtil.maybeSetIsolatedRoot(receivedGroup, groupKey);
256 
257               return transformSequentialAsync(
258                   maybeSetGroupNewFilesReceivedTimestamp(groupKey, receivedGroupWithIsolatedRoot),
259                   receivedGroupCopy -> {
260                     LogUtil.d(
261                         "%s: Received new config for group: %s", TAG, groupKey.getGroupName());
262 
263                     eventLogger.logNewConfigReceived(
264                         DataDownloadFileGroupStats.newBuilder()
265                             .setFileGroupName(receivedGroupCopy.getGroupName())
266                             .setOwnerPackage(receivedGroupCopy.getOwnerPackage())
267                             .setFileGroupVersionNumber(
268                                 receivedGroupCopy.getFileGroupVersionNumber())
269                             .setBuildId(receivedGroupCopy.getBuildId())
270                             .setVariantId(receivedGroupCopy.getVariantId())
271                             .build(),
272                         null);
273 
274                     return transformSequentialAsync(
275                         subscribeGroup(receivedGroupCopy),
276                         subscribed -> {
277                           if (!subscribed) {
278                             throw new IOException("Subscribing to group failed");
279                           }
280 
281                           // TODO(b/160164032): if the File Group has new SyncId, clear the old
282                           // sync.
283                           // TODO(b/160164032): triggerSync in daily maintenance for not
284                           // completed groups.
285                           // Write to Metadata then schedule task via SPE.
286                           return transformSequentialAsync(
287                               writeUpdatedGroupToMetadata(groupKey, receivedGroupCopy),
288                               (voidArg) -> {
289                                 return immediateFuture(true);
290                               });
291                         });
292                   });
293             },
294             sequentialControlExecutor);
295   }
296 
297   private ListenableFuture<Void> writeUpdatedGroupToMetadata(
298       GroupKey groupKey, MetadataProto.DataFileGroupInternal receivedGroupCopy) {
299     // Write the received group as a pending group. If there was a
300     // pending group already present, it will be overwritten and any
301     // files will be garbage collected later.
302     GroupKey pendingGroupKey = groupKey.toBuilder().setDownloaded(false).build();
303 
304     ListenableFuture<@NullableType DataFileGroupInternal> toBeOverwrittenPendingGroupFuture =
305         fileGroupsMetadata.read(pendingGroupKey);
306 
307     return PropagatedFluentFuture.from(toBeOverwrittenPendingGroupFuture)
308         .transformAsync(
309             nullVoid -> fileGroupsMetadata.write(pendingGroupKey, receivedGroupCopy),
310             sequentialControlExecutor)
311         .transformAsync(
312             writeSuccess -> {
313               if (!writeSuccess) {
314                 eventLogger.logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
315                 return immediateFailedFuture(
316                     new IOException("Failed to commit new group metadata to disk."));
317               }
318               return immediateVoidFuture();
319             },
320             sequentialControlExecutor)
321         .transformAsync(
322             nullVoid -> downloadStageManager.updateExperimentIds(receivedGroupCopy.getGroupName()),
323             sequentialControlExecutor)
324         .transformAsync(
325             nullVoid -> {
326               // We need to make sure to clear the experiment ids for this group here, since it will
327               // be overwritten afterwards.
328               DataFileGroupInternal toBeOverwrittenPendingGroup =
329                   Futures.getDone(toBeOverwrittenPendingGroupFuture);
330               if (toBeOverwrittenPendingGroup != null) {
331                 return downloadStageManager.clearExperimentIdsForBuildsIfNoneActive(
332                     ImmutableList.of(toBeOverwrittenPendingGroup));
333               }
334 
335               return immediateVoidFuture();
336             },
337             sequentialControlExecutor);
338   }
339 
340   /**
341    * Removes data file group with the given group key, and cancels any ongoing download of the file
342    * group.
343    *
344    * @param groupKey The key of the data file group to be removed.
345    * @param pendingOnly If true, only remove the pending version of this filegroup.
346    * @return ListenableFuture that may throw an IOException if some error is encountered when
347    *     removing from metadata or a SharedFileMissingException if some of the shared file metadata
348    *     is missing.
349    */
350   ListenableFuture<Void> removeFileGroup(GroupKey groupKey, boolean pendingOnly)
351       throws SharedFileMissingException, IOException {
352     // Remove the pending version from metadata.
353     GroupKey pendingGroupKey = groupKey.toBuilder().setDownloaded(false).build();
354     return transformSequentialAsync(
355         fileGroupsMetadata.read(pendingGroupKey),
356         pendingFileGroup -> {
357           ListenableFuture<Void> removePendingGroupFuture = immediateVoidFuture();
358           if (pendingFileGroup != null) {
359             // Clear Sync Reason before removing the file group.
360             ListenableFuture<Void> clearSyncReasonFuture = immediateVoidFuture();
361             removePendingGroupFuture =
362                 transformSequentialAsync(
363                     clearSyncReasonFuture,
364                     voidArg ->
365                         transformSequentialAsync(
366                             fileGroupsMetadata.remove(pendingGroupKey),
367                             removeSuccess -> {
368                               if (!removeSuccess) {
369                                 LogUtil.e(
370                                     "%s: Failed to remove pending version for group: '%s';"
371                                         + " account: '%s'",
372                                     TAG, groupKey.getGroupName(), groupKey.getAccount());
373                                 eventLogger.logEventSampled(
374                                     MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
375                                 return immediateFailedFuture(
376                                     new IOException(
377                                         "Failed to remove pending group: "
378                                             + groupKey.getGroupName()));
379                               }
380                               return downloadStageManager.clearExperimentIdsForBuildsIfNoneActive(
381                                   ImmutableList.of(pendingFileGroup));
382                             }));
383           }
384           return transformSequentialAsync(
385               removePendingGroupFuture,
386               voidArg0 -> {
387                 GroupKey downloadedGroupKey = groupKey.toBuilder().setDownloaded(true).build();
388                 return transformSequentialAsync(
389                     fileGroupsMetadata.read(downloadedGroupKey),
390                     downloadedFileGroup -> {
391                       ListenableFuture<Void> removeDownloadedGroupFuture = immediateVoidFuture();
392                       if (downloadedFileGroup != null && !pendingOnly) {
393                         // Remove the downloaded version from metadata.
394                         removeDownloadedGroupFuture =
395                             transformSequentialAsync(
396                                 fileGroupsMetadata.remove(downloadedGroupKey),
397                                 removeSuccess -> {
398                                   if (!removeSuccess) {
399                                     LogUtil.e(
400                                         "%s: Failed to remove the downloaded version for group:"
401                                             + " '%s'; account: '%s'",
402                                         TAG, groupKey.getGroupName(), groupKey.getAccount());
403                                     eventLogger.logEventSampled(
404                                         MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
405                                     return immediateFailedFuture(
406                                         new IOException(
407                                             "Failed to remove downloaded group: "
408                                                 + groupKey.getGroupName()));
409                                   }
410                                   // Add the downloaded version to stale.
411                                   return transformSequentialAsync(
412                                       fileGroupsMetadata.addStaleGroup(downloadedFileGroup),
413                                       addSuccess -> {
414                                         if (!addSuccess) {
415                                           LogUtil.e(
416                                               "%s: Failed to add to stale for group: '%s';"
417                                                   + " account: '%s'",
418                                               TAG, groupKey.getGroupName(), groupKey.getAccount());
419                                           eventLogger.logEventSampled(
420                                               MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
421                                           return immediateFailedFuture(
422                                               new IOException(
423                                                   "Failed to add downloaded group to stale: "
424                                                       + groupKey.getGroupName()));
425                                         }
426                                         return downloadStageManager.updateExperimentIds(
427                                             downloadedFileGroup.getGroupName());
428                                       });
429                                 });
430                       }
431 
432                       return transformSequentialAsync(
433                           removeDownloadedGroupFuture,
434                           voidArg1 -> {
435                             // Cancel any ongoing download of the data files in the file group, if
436                             // the data file
437                             // is not referenced by any fresh group.
438                             if (pendingFileGroup != null) {
439                               return transformSequentialAsync(
440                                   getFileKeysReferencedByFreshGroups(),
441                                   referencedFileKeys -> {
442                                     List<ListenableFuture<Void>> cancelDownloadsFutures =
443                                         new ArrayList<>();
444                                     for (DataFile dataFile : pendingFileGroup.getFileList()) {
445                                       // Skip sideloaded files -- they will not have a pending
446                                       // download by definition
447                                       if (FileGroupUtil.isSideloadedFile(dataFile)) {
448                                         continue;
449                                       }
450 
451                                       NewFileKey newFileKey =
452                                           SharedFilesMetadata.createKeyFromDataFile(
453                                               dataFile, pendingFileGroup.getAllowedReadersEnum());
454                                       // Cancel the ongoing download, if the file is not referenced
455                                       // by any fresh file group.
456                                       if (!referencedFileKeys.contains(newFileKey)) {
457                                         cancelDownloadsFutures.add(
458                                             sharedFileManager.cancelDownload(newFileKey));
459                                       }
460                                     }
461                                     return PropagatedFutures.whenAllComplete(cancelDownloadsFutures)
462                                         .call(() -> null, sequentialControlExecutor);
463                                   });
464                             }
465                             return immediateVoidFuture();
466                           });
467                     });
468               });
469         });
470   }
471 
472   /**
473    * Removes data file groups with given group keys and cancels any ongoing downloads of the file
474    * groups.
475    *
476    * <p>The following steps are performed for each file group to remove. If any step fails, the
477    * operation stops and failures are returned.
478    *
479    * <ol>
480    *   <li>Clear SPE Sync Reasons (if applicable) and remove pending file group metadata
481    *   <li>Remove downloaded file group metadata
482    *   <li>Move any removed file groups from downloaded to stale
483    *   <li>Remove any pending downloads for files no longer referenced
484    * </ol>
485    *
486    * @param groupKeys Keys of the File Groups to remove
487    * @return ListenableFuture that resolves when file groups have been removed, or fails if unable
488    *     to remove file groups from metadata.
489    */
490   ListenableFuture<Void> removeFileGroups(List<GroupKey> groupKeys) {
491     // Track Pending and Downloaded Group Keys to remove
492     Map<GroupKey, DataFileGroupInternal> pendingGroupsToRemove =
493         Maps.newHashMapWithExpectedSize(groupKeys.size());
494     Map<GroupKey, DataFileGroupInternal> downloadedGroupsToRemove =
495         Maps.newHashMapWithExpectedSize(groupKeys.size());
496 
497     // Track Pending File Keys that should be canceled
498     Set<NewFileKey> pendingFileKeysToCancel = new HashSet<>();
499 
500     // Track Downloaded File Groups that should be moved to Stale
501     List<DataFileGroupInternal> fileGroupsToAddAsStale = new ArrayList<>(groupKeys.size());
502 
503     return PropagatedFluentFuture.from(
504             PropagatedFutures.submitAsync(
505                 () -> {
506                   // First, Clear SPE Sync Reasons (if applicable) and remove pending file group
507                   // metadata.
508                   List<ListenableFuture<Void>> clearSpeSyncReasonFutures =
509                       new ArrayList<>(groupKeys.size());
510                   for (GroupKey groupKey : groupKeys) {
511                     GroupKey pendingGroupKey = groupKey.toBuilder().setDownloaded(false).build();
512 
513                     clearSpeSyncReasonFutures.add(
514                         PropagatedFluentFuture.from(fileGroupsMetadata.read(pendingGroupKey))
515                             .transformAsync(
516                                 pendingFileGroup -> {
517                                   if (pendingFileGroup == null) {
518                                     // no pending group found, return early
519                                     return immediateVoidFuture();
520                                   }
521 
522                                   // Pending group exists, add it to remove list
523                                   pendingGroupsToRemove.put(pendingGroupKey, pendingFileGroup);
524 
525                                   // Add all pending file keys to cancel
526                                   for (DataFile dataFile : pendingFileGroup.getFileList()) {
527                                     NewFileKey newFileKey =
528                                         SharedFilesMetadata.createKeyFromDataFile(
529                                             dataFile, pendingFileGroup.getAllowedReadersEnum());
530                                     pendingFileKeysToCancel.add(newFileKey);
531                                   }
532 
533                                   return Futures.immediateVoidFuture();
534                                 },
535                                 sequentialControlExecutor));
536                   }
537 
538                   return PropagatedFutures.whenAllComplete(clearSpeSyncReasonFutures)
539                       .callAsync(
540                           () -> {
541                             // Throw aggregate exception if any reasons failed.
542                             AggregateException.throwIfFailed(
543                                 clearSpeSyncReasonFutures, "Unable to clear SPE Sync Reasons");
544                             return transformSequentialAsync(
545                                 fileGroupsMetadata.removeAllGroupsWithKeys(
546                                     ImmutableList.copyOf(pendingGroupsToRemove.keySet())),
547                                 removePendingGroupsResult -> {
548                                   if (!removePendingGroupsResult.booleanValue()) {
549                                     LogUtil.e(
550                                         "%s: Failed to remove %d pending versions of %d requested"
551                                             + " groups",
552                                         TAG, pendingGroupsToRemove.size(), groupKeys.size());
553                                     eventLogger.logEventSampled(
554                                         MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
555                                     return immediateFailedFuture(
556                                         new IOException(
557                                             "Failed to remove pending group keys, count = "
558                                                 + groupKeys.size()));
559                                   }
560                                   return downloadStageManager
561                                       .clearExperimentIdsForBuildsIfNoneActive(
562                                           pendingGroupsToRemove.values());
563                                 });
564                           },
565                           sequentialControlExecutor);
566                 },
567                 sequentialControlExecutor))
568         .transformAsync(
569             unused -> {
570               // Second, remove downloaded file group metadata.
571               List<ListenableFuture<Void>> readDownloadedFileGroupFutures =
572                   new ArrayList<>(groupKeys.size());
573               for (GroupKey groupKey : groupKeys) {
574                 GroupKey downloadedGroupKey = groupKey.toBuilder().setDownloaded(true).build();
575 
576                 readDownloadedFileGroupFutures.add(
577                     transformSequentialAsync(
578                         fileGroupsMetadata.read(downloadedGroupKey),
579                         downloadedFileGroup -> {
580                           if (downloadedFileGroup != null) {
581                             // Downloaded group exists, add to remove list
582                             downloadedGroupsToRemove.put(downloadedGroupKey, downloadedFileGroup);
583 
584                             // Store downloaded group so it can be moved to stale when all metadata
585                             // is updated.
586                             fileGroupsToAddAsStale.add(downloadedFileGroup);
587                           }
588                           return immediateVoidFuture();
589                         }));
590               }
591 
592               return PropagatedFutures.whenAllComplete(readDownloadedFileGroupFutures)
593                   .callAsync(
594                       () -> {
595                         AggregateException.throwIfFailed(
596                             readDownloadedFileGroupFutures,
597                             "Unable to read downloaded file groups to remove");
598                         return transformSequentialAsync(
599                             fileGroupsMetadata.removeAllGroupsWithKeys(
600                                 ImmutableList.copyOf(downloadedGroupsToRemove.keySet())),
601                             removeDownloadedGroupsResult -> {
602                               if (!removeDownloadedGroupsResult.booleanValue()) {
603                                 LogUtil.e(
604                                     "%s: Failed to remove %d downloaded versions of %d requested"
605                                         + " groups",
606                                     TAG, downloadedGroupsToRemove.size(), groupKeys.size());
607                                 eventLogger.logEventSampled(
608                                     MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
609                                 return immediateFailedFuture(
610                                     new IOException(
611                                         "Failed to remove downloaded groups, count = "
612                                             + downloadedGroupsToRemove.size()));
613                               }
614                               return downloadStageManager.clearExperimentIdsForBuildsIfNoneActive(
615                                   downloadedGroupsToRemove.values());
616                             });
617                       },
618                       sequentialControlExecutor);
619             },
620             sequentialControlExecutor)
621         .transformAsync(
622             unused -> {
623               // Third, move any removed file groups from downloaded to stale.
624               // This prevents a files in the group from being removed before its
625               // stale_lifetime_secs has expired.
626               if (downloadedGroupsToRemove.isEmpty()) {
627                 // No downloaded groups were removed, return early
628                 return immediateVoidFuture();
629               }
630 
631               List<ListenableFuture<Void>> addStaleGroupFutures = new ArrayList<>();
632               for (DataFileGroupInternal staleGroup : fileGroupsToAddAsStale) {
633                 addStaleGroupFutures.add(
634                     transformSequentialAsync(
635                         fileGroupsMetadata.addStaleGroup(staleGroup),
636                         addStaleGroupResult -> {
637                           if (!addStaleGroupResult.booleanValue()) {
638                             LogUtil.e(
639                                 "%s: Failed to add to stale for group: '%s';",
640                                 TAG, staleGroup.getGroupName());
641                             eventLogger.logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
642                             return immediateFailedFuture(
643                                 new IOException(
644                                     "Failed to add downloaded group to stale: "
645                                         + staleGroup.getGroupName()));
646                           }
647                           return immediateVoidFuture();
648                         }));
649               }
650               return PropagatedFutures.whenAllComplete(addStaleGroupFutures)
651                   .call(
652                       () -> {
653                         AggregateException.throwIfFailed(
654                             addStaleGroupFutures,
655                             "Unable to add removed downloaded groups as stale");
656                         return null;
657                       },
658                       sequentialControlExecutor);
659             },
660             sequentialControlExecutor)
661         .transformAsync(
662             unused -> {
663               // Fourth, remove any pending downloads for files no longer referenced.
664               // A file that was referenced by a removed file group may still be referenced by an
665               // existing pending group and should not be cancelled. Only cancel pending downloads
666               // that are no longer referenced by any active/pending file groups.
667               if (pendingGroupsToRemove.isEmpty()) {
668                 // No pending groups were removed, return early
669                 return immediateVoidFuture();
670               }
671 
672               return transformSequentialAsync(
673                   getFileKeysReferencedByFreshGroups(),
674                   referencedFileKeys -> {
675                     List<ListenableFuture<Void>> cancelDownloadFutures = new ArrayList<>();
676                     for (NewFileKey newFileKey : pendingFileKeysToCancel) {
677                       // Only cancel file download if it's not referenced by a fresh group
678                       if (!referencedFileKeys.contains(newFileKey)) {
679                         cancelDownloadFutures.add(sharedFileManager.cancelDownload(newFileKey));
680                       }
681                     }
682                     return PropagatedFutures.whenAllComplete(cancelDownloadFutures)
683                         .call(
684                             () -> {
685                               AggregateException.throwIfFailed(
686                                   cancelDownloadFutures,
687                                   "Unable to cancel downloads for removed groups");
688                               return null;
689                             },
690                             sequentialControlExecutor);
691                   });
692             },
693             sequentialControlExecutor);
694   }
695 
696   /**
697    * Returns the required version of the group that we have for the given client key.
698    *
699    * <p>If the group is downloaded and requires an isolated structure, this structure is verified
700    * before returning. If we are unable to verify the isolated structure, null will be returned.
701    *
702    * @param groupKey The key for the data to be returned. This is a combination of many parameters
703    *     like group name, user account.
704    * @return A ListenableFuture that resolves to the requested data file group for the given group
705    *     name, if it exists, null otherwise.
706    */
707   // TODO(b/124072754): Change to package private once all code is refactored.
708   public ListenableFuture<@NullableType DataFileGroupInternal> getFileGroup(
709       GroupKey groupKey, boolean downloaded) {
710     GroupKey downloadedKey = groupKey.toBuilder().setDownloaded(downloaded).build();
711     return fileGroupsMetadata.read(downloadedKey);
712   }
713 
714   /**
715    * Returns a file group/state pair based on the given key and additional identifying information.
716    *
717    * <p>This method allows callers to specify identifying information (buildId, variantId and
718    * customPropertyOptional). It is assumed that different identifying information will be used for
719    * pending/downloded states of a file group, so the downloaded status in the given groupKey is not
720    * considered by this method.
721    *
722    * <p>If a group is found, a {@link GroupKeyAndGroup} will be returned. If a group is not found,
723    * null will be returned. The boolean returned will be true if the group is downloaded and false
724    * if the group is pending.
725    *
726    * @param groupKey The key for the data to be returned. This is should include group name, owner
727    *     package and user account
728    * @param buildId The expected buildId of the file group
729    * @param variantId The expected variantId of the file group
730    * @param customPropertyOptional The expected customProperty, if necessary
731    * @return A ListenableFuture that resolves, if the requested group is found, to a {@link
732    *     GroupKeyAndGroup}, or null if no group is found.
733    */
734   private ListenableFuture<@NullableType GroupKeyAndGroup> getGroupPairById(
735       GroupKey groupKey, long buildId, String variantId, Optional<Any> customPropertyOptional) {
736     return transformSequential(
737         fileGroupsMetadata.getAllFreshGroups(),
738         freshGroupPairList -> {
739           for (GroupKeyAndGroup freshGroupPair : freshGroupPairList) {
740             if (!verifyGroupPairMatchesIdentifiers(
741                 freshGroupPair,
742                 groupKey.getAccount(),
743                 buildId,
744                 variantId,
745                 customPropertyOptional)) {
746               // Identifiers don't match, continue
747               continue;
748             }
749 
750             // Group matches ID, but ensure that it also matches requested group name
751             if (!groupKey.getGroupName().equals(freshGroupPair.groupKey().getGroupName())) {
752               LogUtil.e(
753                   "%s: getGroupPairById: Group %s matches the given buildId = %d and variantId ="
754                       + " %s, but does not match the given group name %s",
755                   TAG,
756                   freshGroupPair.groupKey().getGroupName(),
757                   buildId,
758                   variantId,
759                   groupKey.getGroupName());
760               continue;
761             }
762 
763             return freshGroupPair;
764           }
765 
766           // No compatible group found, return null;
767           return null;
768         });
769   }
770 
771   /**
772    * Set the activation status for the group.
773    *
774    * @param groupKey The key for which the activation is to be set.
775    * @param activation Whether the group should be activated or deactivated.
776    * @return future resolving to whether the activation was successful.
777    */
778   public ListenableFuture<Boolean> setGroupActivation(GroupKey groupKey, boolean activation) {
779     return transformSequentialAsync(
780         fileGroupsMetadata.readGroupKeyProperties(groupKey),
781         groupKeyProperties -> {
782           // It shouldn't make a difference if we found an existing value or not.
783           if (groupKeyProperties == null) {
784             groupKeyProperties = GroupKeyProperties.getDefaultInstance();
785           }
786 
787           GroupKeyProperties.Builder groupKeyPropertiesBuilder = groupKeyProperties.toBuilder();
788           List<ListenableFuture<Void>> removeGroupFutures = new ArrayList<>();
789           if (activation) {
790             // The group will be added to MDD with the next run of AddFileGroupOperation.
791             groupKeyPropertiesBuilder.setActivatedOnDevice(true);
792           } else {
793             groupKeyPropertiesBuilder.setActivatedOnDevice(false);
794 
795             // Remove the existing pending and downloaded groups from MDD in case of deactivation,
796             // if they required activation to be done on the device.
797             GroupKey pendingGroupKey = groupKey.toBuilder().setDownloaded(false).build();
798             removeGroupFutures.add(removeActivatedGroup(pendingGroupKey));
799 
800             GroupKey downloadedGroupKey = groupKey.toBuilder().setDownloaded(true).build();
801             removeGroupFutures.add(removeActivatedGroup(downloadedGroupKey));
802           }
803 
804           return PropagatedFutures.whenAllComplete(removeGroupFutures)
805               .callAsync(
806                   () ->
807                       fileGroupsMetadata.writeGroupKeyProperties(
808                           groupKey, groupKeyPropertiesBuilder.build()),
809                   sequentialControlExecutor);
810         });
811   }
812 
813   private ListenableFuture<Void> removeActivatedGroup(GroupKey groupKey) {
814     return transformSequentialAsync(
815         fileGroupsMetadata.read(groupKey),
816         group -> {
817           if (group != null
818               && group.getDownloadConditions().getActivatingCondition()
819                   == ActivatingCondition.DEVICE_ACTIVATED) {
820             return transformSequentialAsync(
821                 fileGroupsMetadata.remove(groupKey),
822                 removeSuccess -> {
823                   if (!removeSuccess) {
824                     eventLogger.logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
825                   }
826                   return immediateVoidFuture();
827                 });
828           }
829           return immediateVoidFuture();
830         });
831   }
832 
833   /**
834    * Import inline files into an existing DataFileGroup and update its metadata accordingly.
835    *
836    * <p>The given GroupKey will be used to check for an existing DataFileGroup to update and the
837    * given identifying information (buildId, variantId, customProperty) will be used to ensure an
838    * existing file group matches the caller expected version. An import will only take place if an
839    * existing file group of the same version is found.
840    *
841    * <p>Once a valid file group is found, the given updatedDataFileList will be merged into it. If a
842    * DataFile exists in both updatedDataFileList and the existing DataFileGroup (the fileId is the
843    * same), updatedDataFileList's version will be preferred. The resulting merged File Group will be
844    * used to determine which files need to be imported.
845    *
846    * <p>Only files in the updated File Group will be imported (the inlineFileMap may contain extra
847    * files, but they will not be imported).
848    *
849    * <p>This method is an atomic operation: all files must be successfully imported before the
850    * merged file group is written back to MDD metadata. A failure to import any file will result in
851    * no change to the existing metadata and a this failure will be returned.
852    *
853    * @param groupKey The key of the existing group to update
854    * @param buildId build id to identify the file group to update
855    * @param variantId variant id to identify the file group to update
856    * @param updatedDataFileList list of DataFiles to import into the file group
857    * @param inlineFileMap Map of inline file sources that will be imported, where the key is file id
858    *     and the values are {@link FileSource}s containing file content
859    * @param customPropertyOptional Optional custom property used to identify the file group to
860    *     update
861    * @param customFileGroupValidator Validation that runs after the file group is downloaded but
862    *     before the file group leaves the pending state.
863    * @return A ListenableFuture that resolves when inline files have successfully imported
864    */
865   ListenableFuture<Void> importFilesIntoFileGroup(
866       GroupKey groupKey,
867       long buildId,
868       String variantId,
869       ImmutableList<DataFile> updatedDataFileList,
870       ImmutableMap<String, FileSource> inlineFileMap,
871       Optional<Any> customPropertyOptional,
872       AsyncFunction<DataFileGroupInternal, Boolean> customFileGroupValidator) {
873     DownloadStateLogger downloadStateLogger = DownloadStateLogger.forImport(eventLogger);
874 
875     // Get group that should be updated for import, or return group not found failure
876     ListenableFuture<GroupKeyAndGroup> groupKeyAndGroupToUpdateFuture =
877         transformSequentialAsync(
878             getGroupPairById(groupKey, buildId, variantId, customPropertyOptional),
879             foundGroupKeyAndGroup -> {
880               if (foundGroupKeyAndGroup == null) {
881                 // Group with identifiers could not be found, return failure.
882                 LogUtil.e(
883                     "%s: importFiles for group name: %s, buildId: %d, variantId: %s, but no group"
884                         + " was found",
885                     TAG, groupKey.getGroupName(), buildId, variantId);
886                 return immediateFailedFuture(
887                     DownloadException.builder()
888                         .setDownloadResultCode(DownloadResultCode.GROUP_NOT_FOUND_ERROR)
889                         .setMessage(
890                             "file group: "
891                                 + groupKey.getGroupName()
892                                 + " not found! Make sure addFileGroup has been called.")
893                         .build());
894               }
895 
896               // wrap in checkNotNull to ensure type safety.
897               return immediateFuture(checkNotNull(foundGroupKeyAndGroup));
898             });
899 
900     return PropagatedFluentFuture.from(groupKeyAndGroupToUpdateFuture)
901         .transformAsync(
902             groupKeyAndGroupToUpdate -> {
903               // Perform an in-memory merge of updatedDataFileList into the group, so we get the
904               // correct list of files to import.
905               DataFileGroupInternal mergedFileGroup =
906                   mergeFilesIntoFileGroup(
907                       updatedDataFileList, groupKeyAndGroupToUpdate.dataFileGroup());
908 
909               // Log the start of the import now that we have the group.
910               downloadStateLogger.logStarted(mergedFileGroup);
911 
912               // Reserve file entries in case any new DataFiles were included in the merge. This
913               // will be a no-op for existing DataFiles.
914               return transformSequentialAsync(
915                   subscribeGroup(mergedFileGroup),
916                   subscribed -> {
917                     if (!subscribed) {
918                       return immediateFailedFuture(
919                           DownloadException.builder()
920                               .setDownloadResultCode(
921                                   DownloadResultCode.UNABLE_TO_RESERVE_FILE_ENTRY)
922                               .setMessage(
923                                   "Failed to reserve new file entries for group: "
924                                       + mergedFileGroup.getGroupName())
925                               .build());
926                     }
927                     return immediateFuture(mergedFileGroup);
928                   });
929             },
930             sequentialControlExecutor)
931         .transformAsync(
932             mergedFileGroup -> {
933               boolean groupIsDownloaded =
934                   Futures.getDone(groupKeyAndGroupToUpdateFuture).groupKey().getDownloaded();
935 
936               // If we are updating a pending group and the import is successful, the pending
937               // version should be removed from metadata.
938               boolean removePendingVersion = !groupIsDownloaded;
939 
940               List<ListenableFuture<Void>> allImportFutures =
941                   startImportFutures(groupKey, mergedFileGroup, inlineFileMap);
942 
943               // Combine Futures using whenAllComplete so all imports are attempted, even if some
944               // fail.
945               ListenableFuture<GroupDownloadStatus> combinedImportFuture =
946                   PropagatedFutures.whenAllComplete(allImportFutures)
947                       .callAsync(
948                           () ->
949                               futureSerializer.submitAsync(
950                                   () ->
951                                       verifyGroupDownloaded(
952                                           groupKey,
953                                           mergedFileGroup,
954                                           removePendingVersion,
955                                           customFileGroupValidator,
956                                           downloadStateLogger),
957                                   sequentialControlExecutor),
958                           sequentialControlExecutor);
959               return transformSequentialAsync(
960                   combinedImportFuture,
961                   groupDownloadStatus -> {
962                     // If the imports failed, we should return this immediately.
963                     AggregateException.throwIfFailed(
964                         allImportFutures,
965                         "Failed to import files, %d attempted",
966                         allImportFutures.size());
967 
968                     // We log other results in verifyGroupDownloaded, so only check for
969                     // downloaded here.
970                     if (groupDownloadStatus == GroupDownloadStatus.DOWNLOADED) {
971                       eventLogger.logMddDownloadResult(
972                           MddDownloadResult.Code.SUCCESS,
973                           DataDownloadFileGroupStats.newBuilder()
974                               .setFileGroupName(groupKey.getGroupName())
975                               .setOwnerPackage(groupKey.getOwnerPackage())
976                               .setFileGroupVersionNumber(
977                                   mergedFileGroup.getFileGroupVersionNumber())
978                               .setBuildId(mergedFileGroup.getBuildId())
979                               .setVariantId(mergedFileGroup.getVariantId())
980                               .build());
981                       // group downloaded, so it will be written in verifyGroupDownloaded, return
982                       // early.
983                       return immediateVoidFuture();
984                     }
985 
986                     // Group to update is pending or failed. However, this state is not due to the
987                     // import futures (which all succeeded). Therefore, we are safe to write
988                     // merged file group to metadata using the original state (downloaded/pending)
989                     // as before.
990                     return transformSequentialAsync(
991                         fileGroupsMetadata.write(
992                             groupKey.toBuilder().setDownloaded(groupIsDownloaded).build(),
993                             mergedFileGroup),
994                         writeSuccess -> {
995                           if (!writeSuccess) {
996                             eventLogger.logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
997                             return immediateFailedFuture(
998                                 DownloadException.builder()
999                                     .setMessage(
1000                                         "File Import(s) succeeded, but failed to save MDD state.")
1001                                     .setDownloadResultCode(
1002                                         DownloadResultCode.UNABLE_TO_UPDATE_GROUP_METADATA_ERROR)
1003                                     .build());
1004                           }
1005                           return immediateVoidFuture();
1006                         });
1007                   });
1008             },
1009             sequentialControlExecutor)
1010         .catchingAsync(
1011             Exception.class,
1012             exception -> {
1013               // Log DownloadException (or multiple DownloadExceptions if wrapped in
1014               // AggregateException) for debugging.
1015               ListenableFuture<Void> resultFuture = immediateVoidFuture();
1016               if (exception instanceof DownloadException) {
1017                 LogUtil.d("%s: Logging DownloadException", TAG);
1018 
1019                 DownloadException downloadException = (DownloadException) exception;
1020                 resultFuture =
1021                     transformSequentialAsync(
1022                         resultFuture,
1023                         voidArg ->
1024                             logDownloadFailure(groupKey, downloadException, buildId, variantId));
1025               } else if (exception instanceof AggregateException) {
1026                 LogUtil.d("%s: Logging AggregateException", TAG);
1027 
1028                 AggregateException aggregateException = (AggregateException) exception;
1029                 for (Throwable throwable : aggregateException.getFailures()) {
1030                   if (!(throwable instanceof DownloadException)) {
1031                     LogUtil.e("%s: Expecting DownloadExceptions in AggregateException", TAG);
1032                     continue;
1033                   }
1034 
1035                   DownloadException downloadException = (DownloadException) throwable;
1036                   resultFuture =
1037                       transformSequentialAsync(
1038                           resultFuture,
1039                           voidArg ->
1040                               logDownloadFailure(groupKey, downloadException, buildId, variantId));
1041                 }
1042               }
1043 
1044               // Always return failure to upstream callers for further error handling.
1045               return transformSequentialAsync(
1046                   resultFuture, voidArg -> immediateFailedFuture(exception));
1047             },
1048             sequentialControlExecutor);
1049   }
1050 
1051   /**
1052    * Verifies file group pair matches given identifiers.
1053    *
1054    * <p>The following properties are checked to ensure the same id of a file group:
1055    *
1056    * <ul>
1057    *   <li>account
1058    *   <li>build id
1059    *   <li>variant id
1060    *   <li>custom property
1061    * </ul>
1062    */
1063   private static boolean verifyGroupPairMatchesIdentifiers(
1064       GroupKeyAndGroup groupPair,
1065       String serializedAccount,
1066       long buildId,
1067       String variantId,
1068       Optional<Any> customPropertyOptional) {
1069     DataFileGroupInternal fileGroup = groupPair.dataFileGroup();
1070     if (!groupPair.groupKey().getAccount().equals(serializedAccount)) {
1071       LogUtil.v(
1072           "%s: verifyGroupPairMatchesIdentifiers failed for group %s due to mismatched account",
1073           TAG, fileGroup.getGroupName());
1074       return false;
1075     }
1076     if (fileGroup.getBuildId() != buildId) {
1077       LogUtil.v(
1078           "%s: verifyGroupPairMatchesIdentifiers failed for group %s due to mismatched buildId:"
1079               + " existing = %d, expected = %d",
1080           TAG, fileGroup.getGroupName(), fileGroup.getBuildId(), buildId);
1081       return false;
1082     }
1083     if (!variantId.equals(fileGroup.getVariantId())) {
1084       LogUtil.v(
1085           "%s: verifyGroupPairMatchesIdentifiers failed for group %s due to mismatched"
1086               + " variantId: existing = %s, expected = %s",
1087           TAG, fileGroup.getGroupName(), fileGroup.getVariantId(), variantId);
1088       return false;
1089     }
1090 
1091     Optional<Any> existingCustomPropertyOptional =
1092         fileGroup.hasCustomProperty()
1093             ? Optional.of(fileGroup.getCustomProperty())
1094             : Optional.absent();
1095     if (!existingCustomPropertyOptional.equals(customPropertyOptional)) {
1096       LogUtil.v(
1097           "%s: verifyGroupPairMatchesIdentifiers failed for group %s due to mismatched custom"
1098               + " property optional: existing = %s, expected = %s",
1099           TAG, fileGroup.getGroupName(), existingCustomPropertyOptional, customPropertyOptional);
1100       return false;
1101     }
1102     return true;
1103   }
1104 
1105   /**
1106    * Merge files from a List of DataFiles into a File Group.
1107    *
1108    * <p>The merge operation will "override" DataFiles of {@code existingFileGroup} with DataFiles
1109    * from {@code dataFileList} if they share the same fileIds. DataFiles that are in {@code
1110    * existingFileGroup} but not in {@code dataFileList} will remain unchanged. DataFiles which are
1111    * in {@code dataFileList} but not {@code existingFileGroup} will be appended to the file list.
1112    *
1113    * @param dataFileList file list to merge into existing file group
1114    * @param existingFileGroup existing file group to contain file list
1115    * @return LF of a "merged" file group with files from {@code dataFileList} and any non-updated
1116    *     files from {@code existingFileGroup}
1117    */
1118   private static DataFileGroupInternal mergeFilesIntoFileGroup(
1119       ImmutableList<DataFile> dataFileList, DataFileGroupInternal existingFileGroup) {
1120     // Start with existingFileGroup's properties, but clear the file list
1121     DataFileGroupInternal.Builder mergedGroupBuilder = existingFileGroup.toBuilder().clearFile();
1122 
1123     // Use a map to track files by fileId
1124     Map<String, DataFile> fileMap = new HashMap<>();
1125 
1126     // Add all files from existing file group to map first
1127     for (DataFile file : existingFileGroup.getFileList()) {
1128       fileMap.put(file.getFileId(), file);
1129     }
1130 
1131     // Add all files from data file list to map second, ensuring new files update the existing
1132     // entries
1133     for (DataFile file : dataFileList) {
1134       fileMap.put(file.getFileId(), file);
1135     }
1136 
1137     // Add all files from map to the group and build
1138     return mergedGroupBuilder.addAllFile(fileMap.values()).build();
1139   }
1140 
1141   /** Starts imports of inline files in given group. */
1142   private List<ListenableFuture<Void>> startImportFutures(
1143       GroupKey groupKey,
1144       DataFileGroupInternal pendingGroup,
1145       Map<String, FileSource> inlineFileMap) {
1146     List<ListenableFuture<Void>> allImportFutures = new ArrayList<>();
1147     for (DataFile dataFile : pendingGroup.getFileList()) {
1148       if (!dataFile.getUrlToDownload().startsWith(MddConstants.INLINE_FILE_URL_SCHEME)) {
1149         // Skip non-inline files
1150         continue;
1151       }
1152       NewFileKey newFileKey =
1153           SharedFilesMetadata.createKeyFromDataFile(dataFile, pendingGroup.getAllowedReadersEnum());
1154 
1155       allImportFutures.add(
1156           transformSequentialAsync(
1157               sharedFileManager.getFileStatus(newFileKey),
1158               fileStatus -> {
1159                 if (fileStatus.equals(FileStatus.DOWNLOAD_COMPLETE)) {
1160                   // file already downloaded, return immediately
1161                   return immediateVoidFuture();
1162                 }
1163 
1164                 // File needs to be downloaded, check that inline file source is available
1165                 if (!inlineFileMap.containsKey(dataFile.getFileId())) {
1166                   LogUtil.e(
1167                       "%s:Attempt to import file without inline file source. Id = %s",
1168                       TAG, dataFile.getFileId());
1169                   return immediateFailedFuture(
1170                       DownloadException.builder()
1171                           .setDownloadResultCode(DownloadResultCode.MISSING_INLINE_FILE_SOURCE)
1172                           .build());
1173                 }
1174 
1175                 // File source is provided, proceed with import.
1176                 // NOTE: the use of checkNotNull here is fine since we explicitly check that map
1177                 // contains the source above.
1178                 return sharedFileManager.startImport(
1179                     groupKey,
1180                     dataFile,
1181                     newFileKey,
1182                     pendingGroup.getDownloadConditions(),
1183                     checkNotNull(inlineFileMap.get(dataFile.getFileId())));
1184               }));
1185     }
1186 
1187     return allImportFutures;
1188   }
1189 
1190   /**
1191    * Initiates download of the file group and returns a listenable future to track it. The
1192    * ListenableFuture resolves to the non-null DataFileGroup if the group is successfully
1193    * downloaded. Otherwise it returns a null.
1194    *
1195    * @param groupKey The key of the group to schedule for download.
1196    * @param downloadConditions The download conditions that we should download the group under.
1197    * @return the ListenableFuture of the download of all files in the file group.
1198    */
1199   // TODO(b/124072754): Change to package private once all code is refactored.
1200   public ListenableFuture<DataFileGroupInternal> downloadFileGroup(
1201       GroupKey groupKey,
1202       @Nullable DownloadConditions downloadConditionsParam,
1203       AsyncFunction<DataFileGroupInternal, Boolean> customFileGroupValidator) {
1204 
1205     // Capture a reference to the DataFileGroup so we can include build id and variant id in our
1206     // logs.
1207     AtomicReference<@NullableType DataFileGroupInternal> fileGroupForLogging =
1208         new AtomicReference<>();
1209 
1210     ListenableFuture<DataFileGroupInternal> downloadFuture =
1211         transformSequentialAsync(
1212             getFileGroup(groupKey, false /* downloaded */),
1213             pendingGroup -> {
1214               if (pendingGroup == null) {
1215                 // There is no pending group. See if there is a downloaded version and return if it
1216                 // exists.
1217                 return transformSequentialAsync(
1218                     getFileGroup(groupKey, true /* downloaded */),
1219                     downloadedGroup -> {
1220                       if (downloadedGroup == null) {
1221                         return immediateFailedFuture(
1222                             DownloadException.builder()
1223                                 .setDownloadResultCode(DownloadResultCode.GROUP_NOT_FOUND_ERROR)
1224                                 .setMessage(
1225                                     "Nothing to download for file group: "
1226                                         + groupKey.getGroupName())
1227                                 .build());
1228                       }
1229                       fileGroupForLogging.set(downloadedGroup);
1230                       return immediateFuture(downloadedGroup);
1231                     });
1232               }
1233               fileGroupForLogging.set(pendingGroup);
1234 
1235               // Set the download started timestamp and log download started event.
1236               return PropagatedFluentFuture.from(
1237                       updateBookkeepingOnStartDownload(groupKey, pendingGroup))
1238                   .catchingAsync(
1239                       IOException.class,
1240                       ex ->
1241                           immediateFailedFuture(
1242                               DownloadException.builder()
1243                                   .setDownloadResultCode(
1244                                       DownloadResultCode.UNABLE_TO_UPDATE_GROUP_METADATA_ERROR)
1245                                   .setCause(ex)
1246                                   .build()),
1247                       sequentialControlExecutor)
1248                   .transformAsync(
1249                       updatedPendingGroup -> {
1250                         List<ListenableFuture<Void>> allFileFutures =
1251                             startDownloadFutures(
1252                                 downloadConditionsParam, updatedPendingGroup, groupKey);
1253                         // Note: We use whenAllComplete instead of whenAllSucceed since we want to
1254                         // continue to download all other files even if one or more fail. Verify the
1255                         // file group.
1256                         return PropagatedFutures.whenAllComplete(allFileFutures)
1257                             .callAsync(
1258                                 () ->
1259                                     futureSerializer.submitAsync(
1260                                         () ->
1261                                             transformSequentialAsync(
1262                                                 getGroupPair(groupKey),
1263                                                 groupPair -> {
1264                                                   @NullableType
1265                                                   DataFileGroupInternal groupToVerify =
1266                                                       groupPair.pendingGroup() != null
1267                                                           ? groupPair.pendingGroup()
1268                                                           : groupPair.downloadedGroup();
1269                                                   if (groupToVerify != null) {
1270                                                     return transformSequentialAsync(
1271                                                         verifyGroupDownloaded(
1272                                                             groupKey,
1273                                                             groupToVerify,
1274                                                             /* removePendingVersion= */ true,
1275                                                             customFileGroupValidator,
1276                                                             DownloadStateLogger.forDownload(
1277                                                                 eventLogger)),
1278                                                         groupDownloadStatus ->
1279                                                             finalizeDownloadFileFutures(
1280                                                                 allFileFutures,
1281                                                                 groupDownloadStatus,
1282                                                                 groupToVerify,
1283                                                                 groupKey));
1284                                                   } else {
1285                                                     // No group to verify, which should be
1286                                                     // impossible -- force a failure state so we can
1287                                                     // track any download file failures.
1288                                                     handleDownloadFileFutureFailures(
1289                                                         allFileFutures, groupKey);
1290                                                     return immediateFailedFuture(
1291                                                         new AssertionError("impossible error"));
1292                                                   }
1293                                                 }),
1294                                         sequentialControlExecutor),
1295                                 sequentialControlExecutor);
1296                       },
1297                       sequentialControlExecutor);
1298             });
1299 
1300     return PropagatedFutures.catchingAsync(
1301         downloadFuture,
1302         Exception.class,
1303         exception -> {
1304           DataFileGroupInternal dfgInternal = fileGroupForLogging.get();
1305 
1306           final DataFileGroupInternal finalDfgInternal =
1307               (dfgInternal == null) ? DataFileGroupInternal.getDefaultInstance() : dfgInternal;
1308 
1309           ListenableFuture<Void> resultFuture = immediateVoidFuture();
1310           if (exception instanceof DownloadException) {
1311             LogUtil.d("%s: Logging DownloadException", TAG);
1312 
1313             DownloadException downloadException = (DownloadException) exception;
1314             resultFuture =
1315                 transformSequentialAsync(
1316                     resultFuture,
1317                     voidArg ->
1318                         logDownloadFailure(
1319                             groupKey,
1320                             downloadException,
1321                             finalDfgInternal.getBuildId(),
1322                             finalDfgInternal.getVariantId()));
1323           } else if (exception instanceof AggregateException) {
1324             LogUtil.d("%s: Logging AggregateException", TAG);
1325 
1326             AggregateException aggregateException = (AggregateException) exception;
1327             for (Throwable throwable : aggregateException.getFailures()) {
1328               if (!(throwable instanceof DownloadException)) {
1329                 LogUtil.e("%s: Expecting DownloadException's in AggregateException", TAG);
1330                 continue;
1331               }
1332 
1333               DownloadException downloadException = (DownloadException) throwable;
1334               resultFuture =
1335                   transformSequentialAsync(
1336                       resultFuture,
1337                       voidArg ->
1338                           logDownloadFailure(
1339                               groupKey,
1340                               downloadException,
1341                               finalDfgInternal.getBuildId(),
1342                               finalDfgInternal.getVariantId()));
1343             }
1344           }
1345           return transformSequentialAsync(
1346               resultFuture,
1347               voidArg -> {
1348                 throw exception;
1349               });
1350         },
1351         sequentialControlExecutor);
1352   }
1353 
1354   private ListenableFuture<GroupPair> getGroupPair(GroupKey groupKey) {
1355     return PropagatedFutures.submitAsync(
1356         () -> {
1357           ListenableFuture<@NullableType DataFileGroupInternal> pendingGroupFuture =
1358               getFileGroup(groupKey, /* downloaded= */ false);
1359           ListenableFuture<@NullableType DataFileGroupInternal> downloadedGroupFuture =
1360               getFileGroup(groupKey, /* downloaded= */ true);
1361           return PropagatedFutures.whenAllSucceed(pendingGroupFuture, downloadedGroupFuture)
1362               .callAsync(
1363                   () ->
1364                       immediateFuture(
1365                           GroupPair.create(
1366                               getDone(pendingGroupFuture), getDone(downloadedGroupFuture))),
1367                   sequentialControlExecutor);
1368         },
1369         sequentialControlExecutor);
1370   }
1371 
1372   private List<ListenableFuture<Void>> startDownloadFutures(
1373       @Nullable DownloadConditions downloadConditions,
1374       DataFileGroupInternal pendingGroup,
1375       GroupKey groupKey) {
1376     // If absent, use the config from server.
1377     DownloadConditions downloadConditionsFinal =
1378         downloadConditions != null ? downloadConditions : pendingGroup.getDownloadConditions();
1379 
1380     List<ListenableFuture<Void>> allFileFutures = new ArrayList<>();
1381     for (DataFile dataFile : pendingGroup.getFileList()) {
1382       // Skip sideloaded files -- they, by definition, can't be downloaded.
1383       if (FileGroupUtil.isSideloadedFile(dataFile)) {
1384         continue;
1385       }
1386       NewFileKey newFileKey =
1387           SharedFilesMetadata.createKeyFromDataFile(dataFile, pendingGroup.getAllowedReadersEnum());
1388       ListenableFuture<Void> fileFuture;
1389       if (VERSION.SDK_INT >= VERSION_CODES.R) {
1390         ListenableFuture<Void> tryToShareBeforeDownload =
1391             tryToShareBeforeDownload(pendingGroup, dataFile, newFileKey);
1392         fileFuture =
1393             transformSequentialAsync(
1394                 tryToShareBeforeDownload,
1395                 (voidArg) -> {
1396                   ListenableFuture<Void> startDownloadFuture;
1397                   try {
1398                     startDownloadFuture =
1399                         sharedFileManager.startDownload(
1400                             groupKey,
1401                             dataFile,
1402                             newFileKey,
1403                             downloadConditionsFinal,
1404                             pendingGroup.getTrafficTag(),
1405                             pendingGroup.getGroupExtraHttpHeadersList());
1406                   } catch (RuntimeException e) {
1407                     // Catch any unchecked exceptions that prevented the download from starting.
1408                     return immediateFailedFuture(
1409                         DownloadException.builder()
1410                             .setDownloadResultCode(DownloadResultCode.UNKNOWN_ERROR)
1411                             .setCause(e)
1412                             .build());
1413                   }
1414                   // After file as being downloaded locally
1415                   return transformSequentialAsync(
1416                       startDownloadFuture,
1417                       (downloadResult) ->
1418                           tryToShareAfterDownload(pendingGroup, dataFile, newFileKey));
1419                 });
1420       } else {
1421         try {
1422           fileFuture =
1423               sharedFileManager.startDownload(
1424                   groupKey,
1425                   dataFile,
1426                   newFileKey,
1427                   downloadConditionsFinal,
1428                   pendingGroup.getTrafficTag(),
1429                   pendingGroup.getGroupExtraHttpHeadersList());
1430         } catch (RuntimeException e) {
1431           // Catch any unchecked exceptions that prevented the download from starting.
1432           fileFuture =
1433               immediateFailedFuture(
1434                   DownloadException.builder()
1435                       .setDownloadResultCode(DownloadResultCode.UNKNOWN_ERROR)
1436                       .setCause(e)
1437                       .build());
1438         }
1439       }
1440       allFileFutures.add(fileFuture);
1441     }
1442     return allFileFutures;
1443   }
1444 
1445   // Requires that all futures in allFileFutures are completed.
1446   private ListenableFuture<DataFileGroupInternal> finalizeDownloadFileFutures(
1447       List<ListenableFuture<Void>> allFileFutures,
1448       GroupDownloadStatus groupDownloadStatus,
1449       DataFileGroupInternal pendingGroup,
1450       GroupKey groupKey)
1451       throws AggregateException, DownloadException {
1452     // TODO(b/136112848): When all fileFutures succeed, we don't need to verify them again. However
1453     // we still need logic to remove pending and update stale group.
1454     if (groupDownloadStatus != GroupDownloadStatus.DOWNLOADED) {
1455       handleDownloadFileFutureFailures(allFileFutures, groupKey);
1456     }
1457 
1458     eventLogger.logMddDownloadResult(
1459         MddDownloadResult.Code.SUCCESS,
1460         DataDownloadFileGroupStats.newBuilder()
1461             .setFileGroupName(groupKey.getGroupName())
1462             .setOwnerPackage(groupKey.getOwnerPackage())
1463             .setFileGroupVersionNumber(pendingGroup.getFileGroupVersionNumber())
1464             .setBuildId(pendingGroup.getBuildId())
1465             .setVariantId(pendingGroup.getVariantId())
1466             .build());
1467     return immediateFuture(pendingGroup);
1468   }
1469 
1470   // Requires that all futures in allFileFutures are completed.
1471   private void handleDownloadFileFutureFailures(
1472       List<ListenableFuture<Void>> allFileFutures, GroupKey groupKey)
1473       throws DownloadException, AggregateException {
1474     LogUtil.e(
1475         "%s downloadFileGroup %s %s can't finish!",
1476         TAG, groupKey.getGroupName(), groupKey.getOwnerPackage());
1477 
1478     AggregateException.throwIfFailed(
1479         allFileFutures, "Failed to download file group %s", groupKey.getGroupName());
1480 
1481     // TODO(b/118137672): Investigate on the unknown error that we've missed. There is a download
1482     // failure that we don't recognize.
1483     LogUtil.e("%s: An unknown error has occurred during" + " download", TAG);
1484     throw DownloadException.builder()
1485         .setDownloadResultCode(DownloadResultCode.UNKNOWN_ERROR)
1486         .build();
1487   }
1488 
1489   /**
1490    * If the file is available in the shared blob storage, it acquires the lease and updates the
1491    * shared file metadata. The {@code FileStatus} will be set to DOWNLOAD_COMPLETE so that the file
1492    * won't be downloaded again.
1493    *
1494    * <p>The file is available in the shared blob storage if:
1495    *
1496    * <ul>
1497    *   <li>the file is already available in the shared storage, or
1498    *   <li>the file can be copied from the local MDD storage to the shared storage
1499    * </ul>
1500    *
1501    * NOTE: we copy the file only if the file is configured to be shared through the {@code
1502    * android_sharing_type} field.
1503    *
1504    * <p>NOTE: android-sharing is a best effort feature, hence if an error occurs while trying to
1505    * share a file, the download operation won't be stopped.
1506    *
1507    * @return ListenableFuture that may throw a SharedFileMissingException if the shared file
1508    *     metadata is missing.
1509    */
1510   ListenableFuture<Void> tryToShareBeforeDownload(
1511       DataFileGroupInternal fileGroup, DataFile dataFile, NewFileKey newFileKey) {
1512     ListenableFuture<SharedFile> sharedFileFuture =
1513         PropagatedFutures.catchingAsync(
1514             sharedFileManager.getSharedFile(newFileKey),
1515             SharedFileMissingException.class,
1516             e -> {
1517               // TODO(b/131166925): MDD dump should not use lite proto toString.
1518               LogUtil.e("%s: Shared file not found, newFileKey = %s", TAG, newFileKey);
1519               silentFeedback.send(e, "Shared file not found in downloadFileGroup");
1520               logMddAndroidSharingLog(eventLogger, fileGroup, dataFile, 0);
1521               return immediateFailedFuture(e);
1522             },
1523             sequentialControlExecutor);
1524     return transformSequentialAsync(
1525         sharedFileFuture,
1526         sharedFile -> {
1527           long fileExpirationDateSecs = fileGroup.getExpirationDateSecs();
1528           try {
1529             // case 1: the file is already shared in the blob storage.
1530             if (sharedFile.getAndroidShared()) {
1531               LogUtil.d(
1532                   "%s: Android sharing CASE 1 for file %s, filegroup %s",
1533                   TAG, dataFile.getFileId(), fileGroup.getGroupName());
1534               return transformSequentialAsync(
1535                   maybeUpdateLeaseAndSharedMetadata(
1536                       fileGroup,
1537                       dataFile,
1538                       sharedFile,
1539                       newFileKey,
1540                       sharedFile.getAndroidSharingChecksum(),
1541                       fileExpirationDateSecs,
1542                       0),
1543                   res -> immediateVoidFuture());
1544             }
1545 
1546             String androidSharingChecksum = dataFile.getAndroidSharingChecksum();
1547             if (!TextUtils.isEmpty(androidSharingChecksum)) {
1548               // case 2: the file is available in the blob storage.
1549               if (AndroidSharingUtil.blobExists(
1550                   context, androidSharingChecksum, fileGroup, dataFile, fileStorage)) {
1551                 LogUtil.d(
1552                     "%s: Android sharing CASE 2 for file %s, filegroup %s",
1553                     TAG, dataFile.getFileId(), fileGroup.getGroupName());
1554                 return transformSequentialAsync(
1555                     maybeUpdateLeaseAndSharedMetadata(
1556                         fileGroup,
1557                         dataFile,
1558                         sharedFile,
1559                         newFileKey,
1560                         androidSharingChecksum,
1561                         fileExpirationDateSecs,
1562                         0),
1563                     res -> immediateVoidFuture());
1564               }
1565 
1566               // case 3: the to-be-shared file is available in the local storage.
1567               if (dataFile.getAndroidSharingType()
1568                       == DataFile.AndroidSharingType.ANDROID_BLOB_WHEN_AVAILABLE
1569                   && sharedFile.getFileStatus() == FileStatus.DOWNLOAD_COMPLETE) {
1570                 LogUtil.d(
1571                     "%s: Android sharing CASE 3 for file %s, filegroup %s",
1572                     TAG, dataFile.getFileId(), fileGroup.getGroupName());
1573                 Uri downloadFileOnDeviceUri = getLocalUri(dataFile, newFileKey, sharedFile);
1574                 AndroidSharingUtil.copyFileToBlobStore(
1575                     context,
1576                     androidSharingChecksum,
1577                     downloadFileOnDeviceUri,
1578                     fileGroup,
1579                     dataFile,
1580                     fileStorage,
1581                     /* afterDownload= */ false);
1582                 return transformSequentialAsync(
1583                     maybeUpdateLeaseAndSharedMetadata(
1584                         fileGroup,
1585                         dataFile,
1586                         sharedFile,
1587                         newFileKey,
1588                         androidSharingChecksum,
1589                         fileExpirationDateSecs,
1590                         0),
1591                     res -> immediateVoidFuture());
1592               }
1593             }
1594           } catch (AndroidSharingException e) {
1595             logMddAndroidSharingLog(eventLogger, fileGroup, dataFile, e.getErrorCode());
1596           }
1597           LogUtil.d(
1598               "%s: File couldn't be shared before download %s, filegroup %s",
1599               TAG, dataFile.getFileId(), fileGroup.getGroupName());
1600           return immediateVoidFuture();
1601         });
1602   }
1603 
1604   /**
1605    * If sharing the file succeeds, it acquires the lease, updates the file status and deletes the
1606    * local copy.
1607    *
1608    * <p>Sharing the file succeeds if:
1609    *
1610    * <ul>
1611    *   <li>the file is already available in the shared storage, or
1612    *   <li>the file can be copied from the local MDD storage to the shared storage
1613    * </ul>
1614    *
1615    * NOTE: we copy the file only if the file is configured to be shared through the {@code
1616    * android_sharing_type} field.
1617    *
1618    * <p>NOTE: android-sharing is a best effort feature, hence if the file was downlaoded
1619    * successfully and an error occurs while trying to share it, the file will be stored locally.
1620    *
1621    * @return ListenableFuture that may throw a SharedFileMissingException if the shared file
1622    *     metadata is missing.
1623    */
1624   ListenableFuture<Void> tryToShareAfterDownload(
1625       DataFileGroupInternal fileGroup, DataFile dataFile, NewFileKey newFileKey) {
1626     ListenableFuture<SharedFile> sharedFileFuture =
1627         PropagatedFutures.catchingAsync(
1628             sharedFileManager.getSharedFile(newFileKey),
1629             SharedFileMissingException.class,
1630             e -> {
1631               // TODO(b/131166925): MDD dump should not use lite proto toString.
1632               LogUtil.e("%s: Shared file not found, newFileKey = %s", TAG, newFileKey);
1633               silentFeedback.send(e, "Shared file not found in downloadFileGroup");
1634               logMddAndroidSharingLog(eventLogger, fileGroup, dataFile, 0);
1635               return immediateFailedFuture(e);
1636             },
1637             sequentialControlExecutor);
1638     return transformSequentialAsync(
1639         sharedFileFuture,
1640         sharedFile -> {
1641           String androidSharingChecksum = dataFile.getAndroidSharingChecksum();
1642           long fileExpirationDateSecs = fileGroup.getExpirationDateSecs();
1643           // NOTE: if the file wasn't downloaded this method should be no-op.
1644           if (sharedFile.getFileStatus() != FileStatus.DOWNLOAD_COMPLETE) {
1645             return immediateVoidFuture();
1646           }
1647 
1648           if (sharedFile.getAndroidShared()) {
1649             // If the file had been android-shared in another file group while this file instance
1650             // was being downloaded, update the lease if necessary.
1651             if (shouldUpdateMaxExpiryDate(sharedFile, fileExpirationDateSecs)) {
1652               LogUtil.d(
1653                   "%s: File already shared after downloaded but lease has to be updated"
1654                       + " for file %s, filegroup %s",
1655                   TAG, dataFile.getFileId(), fileGroup.getGroupName());
1656               return transformSequentialAsync(
1657                   maybeUpdateLeaseAndSharedMetadata(
1658                       fileGroup,
1659                       dataFile,
1660                       sharedFile,
1661                       newFileKey,
1662                       sharedFile.getAndroidSharingChecksum(),
1663                       fileExpirationDateSecs,
1664                       0),
1665                   res -> {
1666                     if (!res) {
1667                       return updateMaxExpirationDateSecs(
1668                           fileGroup, dataFile, newFileKey, fileExpirationDateSecs);
1669                     }
1670                     return immediateVoidFuture();
1671                   });
1672             }
1673             return immediateVoidFuture();
1674           }
1675           try {
1676             if (!TextUtils.isEmpty(androidSharingChecksum)) {
1677               Uri downloadFileOnDeviceUri = getLocalUri(dataFile, newFileKey, sharedFile);
1678               // case 1: the file is available in the blob storage.
1679               if (AndroidSharingUtil.blobExists(
1680                   context, androidSharingChecksum, fileGroup, dataFile, fileStorage)) {
1681                 LogUtil.d(
1682                     "%s: Android sharing after downloaded, CASE 1 for file %s, filegroup %s",
1683                     TAG, dataFile.getFileId(), fileGroup.getGroupName());
1684                 return transformSequentialAsync(
1685                     maybeUpdateLeaseAndSharedMetadata(
1686                         fileGroup,
1687                         dataFile,
1688                         sharedFile,
1689                         newFileKey,
1690                         androidSharingChecksum,
1691                         fileExpirationDateSecs,
1692                         0),
1693                     res -> {
1694                       if (res) {
1695                         return immediateVoidFuture();
1696                       }
1697                       return updateMaxExpirationDateSecs(
1698                           fileGroup, dataFile, newFileKey, fileExpirationDateSecs);
1699                     });
1700               }
1701 
1702               // case 2: the file is configured to be shared.
1703               if (dataFile.getAndroidSharingType()
1704                   == DataFile.AndroidSharingType.ANDROID_BLOB_WHEN_AVAILABLE) {
1705                 LogUtil.d(
1706                     "%s: Android sharing after downloaded, CASE 2 for file %s, filegroup %s",
1707                     TAG, dataFile.getFileId(), fileGroup.getGroupName());
1708                 AndroidSharingUtil.copyFileToBlobStore(
1709                     context,
1710                     androidSharingChecksum,
1711                     downloadFileOnDeviceUri,
1712                     fileGroup,
1713                     dataFile,
1714                     fileStorage,
1715                     /* afterDownload= */ true);
1716                 return transformSequentialAsync(
1717                     maybeUpdateLeaseAndSharedMetadata(
1718                         fileGroup,
1719                         dataFile,
1720                         sharedFile,
1721                         newFileKey,
1722                         androidSharingChecksum,
1723                         fileExpirationDateSecs,
1724                         0),
1725                     res -> {
1726                       if (res) {
1727                         return immediateVoidFuture();
1728                       }
1729                       return updateMaxExpirationDateSecs(
1730                           fileGroup, dataFile, newFileKey, fileExpirationDateSecs);
1731                     });
1732               }
1733             }
1734             // The file was supposed to be shared but it wasn't.
1735             // NOTE: this scenario should never happened but we want to make sure of it with some
1736             // logs.
1737             if (dataFile.getAndroidSharingType()
1738                 == DataFile.AndroidSharingType.ANDROID_BLOB_WHEN_AVAILABLE) {
1739               logMddAndroidSharingLog(eventLogger, fileGroup, dataFile, 0);
1740             }
1741           } catch (AndroidSharingException e) {
1742             logMddAndroidSharingLog(eventLogger, fileGroup, dataFile, e.getErrorCode());
1743           }
1744           LogUtil.d(
1745               "%s: File couldn't be shared after download %s, filegroup %s",
1746               TAG, dataFile.getFileId(), fileGroup.getGroupName());
1747           return updateMaxExpirationDateSecs(
1748               fileGroup, dataFile, newFileKey, fileExpirationDateSecs);
1749         });
1750   }
1751 
1752   /**
1753    * Returns immediateVoidFuture even in case of error. This is because it is the last method to be
1754    * called by {@code tryToShareAfterDownload}, which implements a best effort feature and is no-op
1755    * in case of error.
1756    */
1757   private ListenableFuture<Void> updateMaxExpirationDateSecs(
1758       DataFileGroupInternal fileGroup,
1759       DataFile dataFile,
1760       NewFileKey newFileKey,
1761       long fileExpirationDateSecs) {
1762     ListenableFuture<Boolean> updateFuture =
1763         sharedFileManager.updateMaxExpirationDateSecs(newFileKey, fileExpirationDateSecs);
1764     return transformSequentialAsync(
1765         updateFuture,
1766         res -> {
1767           if (!res) {
1768             LogUtil.e(
1769                 "%s: Failed to set new state for file %s, filegroup %s",
1770                 TAG, dataFile.getFileId(), fileGroup.getGroupName());
1771             logMddAndroidSharingLog(eventLogger, fileGroup, dataFile, 0);
1772           }
1773           return immediateVoidFuture();
1774         });
1775   }
1776 
1777   /**
1778    * Acquires or updates the lease to the DataFile {@code dataFile} and updates the shared file
1779    * metadata. The sharedFile's {@code FileStatus} will be set to DOWNLOAD_COMPLETE so that the file
1780    * won't be downloaded again.
1781    *
1782    * <p>No-op operation if the lease had already been acquired and it shouldn't been updated.
1783    *
1784    * <p>This lease indicates to the system that the calling package wants the dataFile to be kept
1785    * around.
1786    */
1787   ListenableFuture<Boolean> maybeUpdateLeaseAndSharedMetadata(
1788       DataFileGroupInternal fileGroup,
1789       DataFile dataFile,
1790       SharedFile sharedFile,
1791       NewFileKey newFileKey,
1792       String androidSharingChecksum,
1793       long fileExpirationDateSecs,
1794       int evetTypeToLog)
1795       throws AndroidSharingException {
1796     if (sharedFile.getAndroidShared()
1797         && !shouldUpdateMaxExpiryDate(sharedFile, fileExpirationDateSecs)) {
1798       // The callingPackage has already a lease on the file which expires after the current
1799       // expiration date.
1800       logMddAndroidSharingLog(eventLogger, fileGroup, dataFile, evetTypeToLog);
1801       return immediateFuture(true);
1802     }
1803 
1804     long maxExpiryDate = max(fileExpirationDateSecs, sharedFile.getMaxExpirationDateSecs());
1805     AndroidSharingUtil.acquireLease(
1806         context, androidSharingChecksum, maxExpiryDate, fileGroup, dataFile, fileStorage);
1807     return transformSequentialAsync(
1808         sharedFileManager.setAndroidSharedDownloadedFileEntry(
1809             newFileKey, androidSharingChecksum, maxExpiryDate),
1810         res -> {
1811           if (!res) {
1812             LogUtil.e(
1813                 "%s: Failed to set new state for file %s, filegroup %s",
1814                 TAG, dataFile.getFileId(), fileGroup.getGroupName());
1815             logMddAndroidSharingLog(eventLogger, fileGroup, dataFile, 0);
1816             return immediateFuture(false);
1817           }
1818           logMddAndroidSharingLog(
1819               eventLogger, fileGroup, dataFile, evetTypeToLog, true, maxExpiryDate);
1820           return immediateFuture(true);
1821         });
1822   }
1823 
1824   /**
1825    * Returns true if the file {@code expirationDateSecs} is greater than the current sharedFile
1826    * {@code max_expiration_date}.
1827    */
1828   private static boolean shouldUpdateMaxExpiryDate(SharedFile sharedFile, long expirationDateSecs) {
1829     return expirationDateSecs > sharedFile.getMaxExpirationDateSecs();
1830   }
1831 
1832   // TODO(b/118137672): remove this helper method once DirectoryUtil.getOnDeviceUri throws an
1833   // exception instead of returning null.
1834   private Uri getLocalUri(DataFile dataFile, NewFileKey newFileKey, SharedFile sharedFile)
1835       throws AndroidSharingException {
1836     Uri downloadFileOnDeviceUri =
1837         DirectoryUtil.getOnDeviceUri(
1838             context,
1839             newFileKey.getAllowedReaders(),
1840             sharedFile.getFileName(),
1841             dataFile.getChecksum(),
1842             silentFeedback,
1843             instanceId,
1844             /* androidShared= */ false);
1845     if (downloadFileOnDeviceUri == null) {
1846       LogUtil.e("%s: Failed to get file uri!", TAG);
1847       throw new AndroidSharingException(0, "Failed to get local file uri");
1848     }
1849     return downloadFileOnDeviceUri;
1850   }
1851 
1852   /**
1853    * Download and Verify all files present in any pending groups.
1854    *
1855    * @param onWifi whether the device is on wifi at the moment.
1856    * @return A Combined Future of all file group downloads.
1857    */
1858   // TODO(b/124072754): Change to package private once all code is refactored.
1859   // TODO: Change name to downloadAndVerifyAllPendingGroups.
1860   public ListenableFuture<Void> scheduleAllPendingGroupsForDownload(
1861       boolean onWifi, AsyncFunction<DataFileGroupInternal, Boolean> customFileGroupValidator) {
1862     return transformSequentialAsync(
1863         fileGroupsMetadata.getAllGroupKeys(),
1864         propagateAsyncFunction(
1865             groupKeyList ->
1866                 schedulePendingDownloads(groupKeyList, onWifi, customFileGroupValidator)));
1867   }
1868 
1869   @SuppressWarnings("nullness")
1870   // Suppress nullness warnings because otherwise static analysis would require us to falsely label
1871   // downloadFileGroup with @NullableType
1872   private ListenableFuture<Void> schedulePendingDownloads(
1873       List<GroupKey> groupKeyList,
1874       boolean onWifi,
1875       AsyncFunction<DataFileGroupInternal, Boolean> customFileGroupValidator) {
1876     List<ListenableFuture<DataFileGroupInternal>> allGroupFutures = new ArrayList<>();
1877     for (GroupKey key : groupKeyList) {
1878       // We are only checking the non-downloaded groups
1879       if (key.getDownloaded()) {
1880         continue;
1881       }
1882 
1883       allGroupFutures.add(
1884           transformSequentialAsync(
1885               fileGroupsMetadata.read(key),
1886               pendingGroup -> {
1887                 if (pendingGroup == null) {
1888                   return Futures.immediateFuture(null);
1889                 }
1890 
1891                 boolean allowDownloadWithoutWifi = false;
1892                 if (pendingGroup.getDownloadConditions().getDeviceNetworkPolicy()
1893                     == DeviceNetworkPolicy.DOWNLOAD_ON_ANY_NETWORK) {
1894                   allowDownloadWithoutWifi = true;
1895                 } else if (pendingGroup.getDownloadConditions().getDeviceNetworkPolicy()
1896                     == DeviceNetworkPolicy.DOWNLOAD_FIRST_ON_WIFI_THEN_ON_ANY_NETWORK) {
1897                   long timeDownloadingWithWifiSecs =
1898                       (timeSource.currentTimeMillis()
1899                               - pendingGroup.getBookkeeping().getGroupNewFilesReceivedTimestamp())
1900                           / 1000;
1901                   if (timeDownloadingWithWifiSecs
1902                       > pendingGroup.getDownloadConditions().getDownloadFirstOnWifiPeriodSecs()) {
1903                     allowDownloadWithoutWifi = true;
1904 
1905                     pendingGroup =
1906                         pendingGroup.toBuilder()
1907                             .setDownloadConditions(
1908                                 pendingGroup.getDownloadConditions().toBuilder()
1909                                     .setDeviceNetworkPolicy(
1910                                         DeviceNetworkPolicy.DOWNLOAD_ON_ANY_NETWORK))
1911                             .build();
1912                   }
1913                 }
1914 
1915                 LogUtil.d(
1916                     "%s: Try to download pending file group: %s, download over cellular = %s",
1917                     TAG, pendingGroup.getGroupName(), allowDownloadWithoutWifi);
1918 
1919                 if (onWifi || allowDownloadWithoutWifi) {
1920                   return downloadFileGroup(
1921                       key, pendingGroup.getDownloadConditions(), customFileGroupValidator);
1922                 }
1923                 return immediateFuture(null);
1924               }));
1925     }
1926     // Note: We use whenAllComplete instead of whenAllSucceed since we want to continue to download
1927     // all other file groups even if one or more fail.
1928     return PropagatedFutures.whenAllComplete(allGroupFutures)
1929         .call(() -> null, sequentialControlExecutor);
1930   }
1931 
1932   /**
1933    * Verifies that the given group was downloaded, and updates the metadata if the download has
1934    * completed.
1935    *
1936    * @param groupKey The key of the group to verify for download.
1937    * @param fileGroup The group to verify for download.
1938    * @param removePendingVersion boolean to tell whether or not the pending version should be
1939    *     removed.
1940    * @return A future that resolves to true if the given group was verify for download, false
1941    *     otherwise.
1942    */
1943   ListenableFuture<GroupDownloadStatus> verifyGroupDownloaded(
1944       GroupKey groupKey,
1945       DataFileGroupInternal fileGroup,
1946       boolean removePendingVersion,
1947       AsyncFunction<DataFileGroupInternal, Boolean> customFileGroupValidator,
1948       DownloadStateLogger downloadStateLogger) {
1949     LogUtil.d(
1950         "%s: Verify group: %s, remove pending version: %s",
1951         TAG, fileGroup.getGroupName(), removePendingVersion);
1952 
1953     GroupKey downloadedGroupKey = groupKey.toBuilder().setDownloaded(true).build();
1954     GroupKey pendingGroupKey = groupKey.toBuilder().setDownloaded(false).build();
1955 
1956     // It's possible that we are calling verifyGroupDownloaded concurrently, which would lead to
1957     // multiple DOWNLOAD_COMPLETE logs. To prevent this, we check to see if we've already logged the
1958     // timestamp so we can skip logging later.
1959     boolean completeAlreadyLogged =
1960         fileGroup.getBookkeeping().hasGroupDownloadedTimestampInMillis();
1961     DataFileGroupInternal downloadedFileGroupWithTimestamp =
1962         FileGroupUtil.setDownloadedTimestampInMillis(fileGroup, timeSource.currentTimeMillis());
1963 
1964     return PropagatedFluentFuture.from(getFileGroupDownloadStatus(fileGroup))
1965         .transformAsync(
1966             groupDownloadStatus -> {
1967               // TODO(b/159828199) Use exceptions instead of nesting to exit early from transform
1968               // chain.
1969               if (groupDownloadStatus == GroupDownloadStatus.FAILED) {
1970                 downloadStateLogger.logFailed(fileGroup);
1971                 return Futures.immediateFuture(GroupDownloadStatus.FAILED);
1972               }
1973               if (groupDownloadStatus == GroupDownloadStatus.PENDING) {
1974                 downloadStateLogger.logPending(fileGroup);
1975                 return Futures.immediateFuture(GroupDownloadStatus.PENDING);
1976               }
1977 
1978               Preconditions.checkArgument(groupDownloadStatus == GroupDownloadStatus.DOWNLOADED);
1979               return validateFileGroupAndMaybeRemoveIfFailed(
1980                       pendingGroupKey,
1981                       fileGroup,
1982                       downloadStateLogger,
1983                       removePendingVersion,
1984                       customFileGroupValidator)
1985                   .transformAsync(
1986                       unused -> {
1987                         // Create isolated file structure (using symlinks) if necessary and
1988                         // supported
1989                         if (FileGroupUtil.isIsolatedStructureAllowed(fileGroup)
1990                             && VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
1991                           // TODO(b/225409326): Prevent race condition where recreation of isolated
1992                           // paths happens at the same time as group access.
1993                           return createIsolatedFilePaths(fileGroup);
1994                         }
1995                         return immediateVoidFuture();
1996                       },
1997                       sequentialControlExecutor)
1998                   .transformAsync(
1999                       unused ->
2000                           writeNewGroupAndReturnOldGroup(
2001                               downloadedGroupKey, downloadedFileGroupWithTimestamp),
2002                       sequentialControlExecutor)
2003                   .transformAsync(
2004                       downloadedGroupOptional -> {
2005                         if (removePendingVersion) {
2006                           return removePendingGroup(pendingGroupKey, downloadedGroupOptional);
2007                         }
2008 
2009                         return immediateFuture(downloadedGroupOptional);
2010                       },
2011                       sequentialControlExecutor)
2012                   .transformAsync(this::addGroupAsStaleIfPresent, sequentialControlExecutor)
2013                   .transform(
2014                       voidArg -> {
2015                         // Only log complete if we are performing an import operation OR we haven't
2016                         // already logged a download complete event.
2017                         if (!completeAlreadyLogged
2018                             || downloadStateLogger.getOperation() == Operation.IMPORT) {
2019                           downloadStateLogger.logComplete(downloadedFileGroupWithTimestamp);
2020                         }
2021                         return GroupDownloadStatus.DOWNLOADED;
2022                       },
2023                       sequentialControlExecutor);
2024             },
2025             sequentialControlExecutor)
2026         .transformAsync(
2027             downloadStatus ->
2028                 transformSequential(
2029                     downloadStageManager.updateExperimentIds(fileGroup.getGroupName()),
2030                     success -> downloadStatus),
2031             sequentialControlExecutor);
2032   }
2033 
2034   private ListenableFuture<Optional<DataFileGroupInternal>> writeNewGroupAndReturnOldGroup(
2035       GroupKey downloadedGroupKey, DataFileGroupInternal newGroup) {
2036     PropagatedFluentFuture<Optional<DataFileGroupInternal>> existingFileGroup =
2037         PropagatedFluentFuture.from(fileGroupsMetadata.read(downloadedGroupKey))
2038             .transform(Optional::fromNullable, sequentialControlExecutor);
2039 
2040     return existingFileGroup
2041         .transformAsync(
2042             unused -> fileGroupsMetadata.write(downloadedGroupKey, newGroup),
2043             sequentialControlExecutor)
2044         .transformAsync(
2045             writeSuccess -> {
2046               if (!writeSuccess) {
2047                 eventLogger.logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
2048                 return immediateFailedFuture(
2049                     new IOException(
2050                         "Failed to write updated group: " + downloadedGroupKey.getGroupName()));
2051               }
2052 
2053               return existingFileGroup;
2054             },
2055             sequentialControlExecutor);
2056   }
2057 
2058   private ListenableFuture<Optional<DataFileGroupInternal>> removePendingGroup(
2059       GroupKey pendingGroupKey, Optional<DataFileGroupInternal> toReturn) {
2060     // Remove the newly downloaded version from the pending groups list,
2061     // if removing fails, we will verify it again the next time.
2062     return transformSequential(
2063         fileGroupsMetadata.remove(pendingGroupKey),
2064         removeSuccess -> {
2065           if (!removeSuccess) {
2066             eventLogger.logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
2067           }
2068           return toReturn;
2069         });
2070   }
2071 
2072   private PropagatedFluentFuture<Void> validateFileGroupAndMaybeRemoveIfFailed(
2073       GroupKey pendingGroupKey,
2074       DataFileGroupInternal fileGroup,
2075       DownloadStateLogger downloadStateLogger,
2076       boolean removePendingVersion,
2077       AsyncFunction<DataFileGroupInternal, Boolean> customFileGroupValidator)
2078       throws Exception {
2079     return PropagatedFluentFuture.from(customFileGroupValidator.apply(fileGroup))
2080         .transformAsync(
2081             validatedOk -> {
2082               if (validatedOk) {
2083                 return immediateVoidFuture();
2084               }
2085 
2086               downloadStateLogger.logFailed(fileGroup);
2087 
2088               ListenableFuture<Boolean> removePendingGroupFuture = immediateFuture(true);
2089               if (removePendingVersion) {
2090                 removePendingGroupFuture = fileGroupsMetadata.remove(pendingGroupKey);
2091               }
2092               return transformSequentialAsync(
2093                   removePendingGroupFuture,
2094                   removeSuccess -> {
2095                     if (!removeSuccess) {
2096                       LogUtil.e(
2097                           "%s: Failed to remove pending version for group: '%s';"
2098                               + " account: '%s'",
2099                           TAG, pendingGroupKey.getGroupName(), pendingGroupKey.getAccount());
2100                       eventLogger.logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
2101                       return immediateFailedFuture(
2102                           new IOException(
2103                               "Failed to remove pending group: " + pendingGroupKey.getGroupName()));
2104                     }
2105                     return immediateFailedFuture(
2106                         DownloadException.builder()
2107                             .setDownloadResultCode(
2108                                 DownloadResultCode.CUSTOM_FILEGROUP_VALIDATION_FAILED)
2109                             .setMessage(
2110                                 DownloadResultCode.CUSTOM_FILEGROUP_VALIDATION_FAILED.name())
2111                             .build());
2112                   });
2113             },
2114             sequentialControlExecutor);
2115   }
2116 
2117   private ListenableFuture<Void> addGroupAsStaleIfPresent(
2118       Optional<DataFileGroupInternal> oldGroup) {
2119     if (!oldGroup.isPresent()) {
2120       return immediateVoidFuture();
2121     }
2122 
2123     return transformSequentialAsync(
2124         fileGroupsMetadata.addStaleGroup(oldGroup.get()),
2125         addSuccess -> {
2126           if (!addSuccess) {
2127             // If this fails, the stale file group will be
2128             // unaccounted for, and the files will get deleted
2129             // in the next daily maintenance, hence not
2130             // enforcing its stale lifetime.
2131             eventLogger.logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
2132           }
2133           return immediateVoidFuture();
2134         });
2135   }
2136 
2137   /**
2138    * When a DataFileGroup has preserve_filenames_and_isolate_files set, this method will create an
2139    * isolated file structure (using symlinks to the shared files).
2140    *
2141    * <p>This method will also respect a DataFiles relative_file_path field (if set), otherwise it
2142    * will use the last segment of the download url.
2143    *
2144    * <p>If preserve_filenames_and_isolate_files is not set, this method is a noop and will
2145    * immediately return
2146    *
2147    * @return Future that resolves once isolated paths are created, or failure with DownloadException
2148    *     if unable to create isolated structure.
2149    */
2150   @RequiresApi(VERSION_CODES.LOLLIPOP)
2151   private ListenableFuture<Void> createIsolatedFilePaths(DataFileGroupInternal dataFileGroup) {
2152     // If no isolated structure is required, return early.
2153     if (!dataFileGroup.getPreserveFilenamesAndIsolateFiles()) {
2154       return immediateVoidFuture();
2155     }
2156 
2157     // Remove existing symlinks if they exist
2158     try {
2159       FileGroupUtil.removeIsolatedFileStructure(context, instanceId, dataFileGroup, fileStorage);
2160     } catch (IOException e) {
2161       return immediateFailedFuture(
2162           DownloadException.builder()
2163               .setDownloadResultCode(DownloadResultCode.UNABLE_TO_REMOVE_SYMLINK_STRUCTURE)
2164               .setMessage("Unable to cleanup symlink structure")
2165               .setCause(e)
2166               .build());
2167     }
2168 
2169     List<DataFile> dataFiles = dataFileGroup.getFileList();
2170 
2171     if (Iterables.tryFind(
2172             dataFiles,
2173             dataFile ->
2174                 dataFile.getAndroidSharingType() == AndroidSharingType.ANDROID_BLOB_WHEN_AVAILABLE)
2175         .isPresent()) {
2176       // Creating isolated structure is not supported when android sharing is enabled in the group;
2177       // return immediately.
2178       return immediateFailedFuture(
2179           new UnsupportedOperationException(
2180               "Preserve File Paths is invalid with Android Blob Sharing"));
2181     }
2182 
2183     ImmutableMap<DataFile, Uri> isolatedFileUriMap = getIsolatedFileUris(dataFileGroup);
2184     ListenableFuture<Void> createIsolatedStructureFuture =
2185         PropagatedFutures.transformAsync(
2186             getOnDeviceUris(dataFileGroup),
2187             onDeviceUriMap -> {
2188               for (DataFile dataFile : dataFiles) {
2189                 try {
2190                   Uri symlinkUri = checkNotNull(isolatedFileUriMap.get(dataFile));
2191                   Uri originalUri = checkNotNull(onDeviceUriMap.get(dataFile));
2192 
2193                   // Check/create parent dir of symlink.
2194                   Uri symlinkParentDir =
2195                       Uri.parse(
2196                           symlinkUri
2197                               .toString()
2198                               .substring(0, symlinkUri.toString().lastIndexOf("/")));
2199                   if (!fileStorage.exists(symlinkParentDir)) {
2200                     fileStorage.createDirectory(symlinkParentDir);
2201                   }
2202                   SymlinkUtil.createSymlink(context, symlinkUri, originalUri);
2203                 } catch (NullPointerException | IOException e) {
2204                   return immediateFailedFuture(
2205                       DownloadException.builder()
2206                           .setDownloadResultCode(
2207                               DownloadResultCode.UNABLE_TO_CREATE_SYMLINK_STRUCTURE)
2208                           .setMessage("Unable to create symlink")
2209                           .setCause(e)
2210                           .build());
2211                 }
2212               }
2213               return immediateVoidFuture();
2214             },
2215             sequentialControlExecutor);
2216 
2217     PropagatedFutures.addCallback(
2218         createIsolatedStructureFuture,
2219         new FutureCallback<Void>() {
2220           @Override
2221           public void onSuccess(Void unused) {}
2222 
2223           @Override
2224           public void onFailure(Throwable t) {
2225             // cleanup symlink structure on failure
2226             LogUtil.d(t, "%s: Unable to create symlink structure, cleaning up symlinks...", TAG);
2227             try {
2228               FileGroupUtil.removeIsolatedFileStructure(
2229                   context, instanceId, dataFileGroup, fileStorage);
2230             } catch (IOException e) {
2231               LogUtil.d(e, "%s: Unable to clean up symlink structure after failure", TAG);
2232             }
2233           }
2234         },
2235         sequentialControlExecutor);
2236 
2237     return createIsolatedStructureFuture;
2238   }
2239 
2240   /**
2241    * Verifies a file group's isolated structure is correct.
2242    *
2243    * <p>This verification is only performed under the following conditions:
2244    *
2245    * <ul>
2246    *   <li>MDD Flags enable this verification
2247    *   <li>The group is not null
2248    *   <li>The group is downloaded
2249    *   <li>The group uses an isolated structure
2250    * </ul>
2251    *
2252    * <p>If any of these conditions are not met, this method is a noop and returns true immediately.
2253    *
2254    * <p>If structure is correct, this method returns true.
2255    *
2256    * <p>If the isolated structure is corrupted (missing symlink or invalid symlink), this method
2257    * will return false.
2258    *
2259    * <p>This method is annotated with @TargetApi(21) since symlink structure methods require API
2260    * level 21 or later. The FileGroupUtil.isIsolatedStructureAllowed check will ensure this
2261    * condition is met before calling verifyIsolatedFileUris and createIsolatedFilePaths.
2262    *
2263    * @return Future that resolves to true if the isolated structure is verified, or false if the
2264    *     structure couldn't be verified
2265    */
2266   @TargetApi(21)
2267   private ListenableFuture<Boolean> maybeVerifyIsolatedStructure(
2268       @NullableType DataFileGroupInternal dataFileGroup, boolean isDownloaded) {
2269     // Return early if conditions are not met
2270     if (!flags.enableIsolatedStructureVerification()
2271         || dataFileGroup == null
2272         || !isDownloaded
2273         || !FileGroupUtil.isIsolatedStructureAllowed(dataFileGroup)) {
2274       return immediateFuture(true);
2275     }
2276 
2277     return PropagatedFluentFuture.from(getOnDeviceUris(dataFileGroup))
2278         .transform(
2279             onDeviceUriMap -> {
2280               ImmutableMap<DataFile, Uri> verifiedUriMap =
2281                   verifyIsolatedFileUris(getIsolatedFileUris(dataFileGroup), onDeviceUriMap);
2282               for (DataFile dataFile : dataFileGroup.getFileList()) {
2283                 if (!verifiedUriMap.containsKey(dataFile)) {
2284                   // File is missing from map, so verification failed, log this error and return
2285                   // false.
2286                   LogUtil.w(
2287                       "%s: Detected corruption of isolated structure for group %s %s",
2288                       TAG, dataFileGroup.getGroupName(), dataFile.getFileId());
2289                   return false;
2290                 }
2291               }
2292               return true;
2293             },
2294             sequentialControlExecutor);
2295   }
2296 
2297   /**
2298    * Gets the on device uri of the given {@link DataFile}.
2299    *
2300    * <p>Checks for sideloading support. If file is sideloaded and sideloading is enabled, the
2301    * sideload uri will be returned immediately. If sideloading is not enabled, returns failure.
2302    *
2303    * <p>If file is not sideloaded, delegates to {@link
2304    * SharedFileManager#getOnDeviceUri(NewFileKey)}.
2305    */
2306   public ListenableFuture<@NullableType Uri> getOnDeviceUri(
2307       DataFile dataFile, DataFileGroupInternal dataFileGroup) {
2308     // If sideloaded file -- return url immediately
2309     if (FileGroupUtil.isSideloadedFile(dataFile)) {
2310       return immediateFuture(Uri.parse(dataFile.getUrlToDownload()));
2311     }
2312 
2313     NewFileKey newFileKey =
2314         SharedFilesMetadata.createKeyFromDataFile(dataFile, dataFileGroup.getAllowedReadersEnum());
2315 
2316     return sharedFileManager.getOnDeviceUri(newFileKey);
2317   }
2318 
2319   /**
2320    * Gets the on-device uri of the given list of {@link DataFile}s.
2321    *
2322    * <p>Checks for sideloading support. If the file is sideloaded and sideloading is enabled, the
2323    * sideloaded uri will be returned immediately. If sideloading is not enabled, returns a faliure.
2324    *
2325    * <p>If file is not sideloaded, delegates to {@link SharedFileManager#getOnDeviceUris()}.
2326    *
2327    * <p>NOTE: The returned map will contain entries for all data files with a known uri. If the uri
2328    * is unable to be calculated, it will not be included in the returned list.
2329    */
2330   ListenableFuture<ImmutableMap<DataFile, Uri>> getOnDeviceUris(
2331       DataFileGroupInternal dataFileGroup) {
2332     ImmutableMap.Builder<DataFile, Uri> onDeviceUriMap = ImmutableMap.builder();
2333     ImmutableMap.Builder<DataFile, NewFileKey> nonSideloadedKeyMapBuilder = ImmutableMap.builder();
2334     for (DataFile dataFile : dataFileGroup.getFileList()) {
2335       if (FileGroupUtil.isSideloadedFile(dataFile)) {
2336         // Sideloaded file -- put in map immediately
2337         onDeviceUriMap.put(dataFile, Uri.parse(dataFile.getUrlToDownload()));
2338       } else {
2339         // Non sideloaded file -- mark for further lookup
2340         nonSideloadedKeyMapBuilder.put(
2341             dataFile,
2342             SharedFilesMetadata.createKeyFromDataFile(
2343                 dataFile, dataFileGroup.getAllowedReadersEnum()));
2344       }
2345     }
2346     ImmutableMap<DataFile, NewFileKey> nonSideloadedKeyMap =
2347         nonSideloadedKeyMapBuilder.build();
2348 
2349     return PropagatedFluentFuture.from(
2350             sharedFileManager.getOnDeviceUris(ImmutableSet.copyOf(nonSideloadedKeyMap.values())))
2351         .transform(
2352             nonSideloadedUriMap -> {
2353               // Extract the <DataFile, Uri> entries from the two non-sideloaded maps.
2354               // DataFile -> NewFileKey -> Uri now becomes DataFile -> Uri
2355               for (Entry<DataFile, NewFileKey> keyMapEntry : nonSideloadedKeyMap.entrySet()) {
2356                 NewFileKey newFileKey = keyMapEntry.getValue();
2357                 if (newFileKey != null && nonSideloadedUriMap.containsKey(newFileKey)) {
2358                   onDeviceUriMap.put(keyMapEntry.getKey(), nonSideloadedUriMap.get(newFileKey));
2359                 }
2360               }
2361               return onDeviceUriMap.build();
2362             },
2363             sequentialControlExecutor);
2364   }
2365 
2366   /**
2367    * Helper method to get a map of isolated file uris.
2368    *
2369    * <p>This method does not check whether or not isolated uris are allowed to be created/used, but
2370    * simply returns all calculated isolated file uris. The caller is responsible for checking if the
2371    * returned uris can/should be used!
2372    */
2373   ImmutableMap<DataFile, Uri> getIsolatedFileUris(DataFileGroupInternal dataFileGroup) {
2374     ImmutableMap.Builder<DataFile, Uri> isolatedFileUrisBuilder = ImmutableMap.builder();
2375     Uri isolatedRootUri =
2376         FileGroupUtil.getIsolatedRootDirectory(context, instanceId, dataFileGroup);
2377     for (DataFile dataFile : dataFileGroup.getFileList()) {
2378       isolatedFileUrisBuilder.put(
2379           dataFile, FileGroupUtil.appendIsolatedFileUri(isolatedRootUri, dataFile));
2380     }
2381     return isolatedFileUrisBuilder.build();
2382   }
2383 
2384   /**
2385    * Verify the given isolated uris point to the given on-device uris.
2386    *
2387    * <p>The verification steps include 1) ensuring each isolated uri exists; 2) each isolated uri
2388    * points to the corresponding on-device uri. Isolated uris and on-device uris will be matched by
2389    * their {@link DataFile} keys from the input maps.
2390    *
2391    * <p>Each verified isolated uri is included in the return map. If an isolated uri cannot be
2392    * verified, no entry for the corresponding data file will be included in the return map.
2393    *
2394    * <p>If an entry for a DataFile key is missing from either input map, it is also omitted from the
2395    * return map (i.e. this method returns an INNER JOIN of the two input maps)
2396    *
2397    * @return map of isolated uris which have been verified
2398    */
2399   @RequiresApi(VERSION_CODES.LOLLIPOP)
2400   ImmutableMap<DataFile, Uri> verifyIsolatedFileUris(
2401       ImmutableMap<DataFile, Uri> isolatedFileUris, ImmutableMap<DataFile, Uri> onDeviceUris) {
2402     ImmutableMap.Builder<DataFile, Uri> verifiedUriMapBuilder = ImmutableMap.builder();
2403     for (Entry<DataFile, Uri> onDeviceEntry : onDeviceUris.entrySet()) {
2404       // Skip null/missing uris
2405       if (onDeviceEntry.getValue() == null
2406           || !isolatedFileUris.containsKey(onDeviceEntry.getKey())) {
2407         continue;
2408       }
2409 
2410       Uri isolatedUri = isolatedFileUris.get(onDeviceEntry.getKey());
2411       Uri onDeviceUri = onDeviceEntry.getValue();
2412 
2413       try {
2414         Uri targetFileUri = SymlinkUtil.readSymlink(context, isolatedUri);
2415         if (fileStorage.exists(isolatedUri)
2416             && targetFileUri.toString().equals(onDeviceUri.toString())) {
2417           verifiedUriMapBuilder.put(onDeviceEntry.getKey(), isolatedUri);
2418         } else {
2419           LogUtil.e(
2420               "%s verifyIsolatedFileUris unable to get isolated file uri! %s %s",
2421               TAG, isolatedUri, onDeviceUri);
2422         }
2423       } catch (IOException e) {
2424         LogUtil.e(
2425             "%s verifyIsolatedFileUris unable to get isolated file uri! %s %s",
2426             TAG, isolatedUri, onDeviceUri);
2427       }
2428     }
2429     return verifiedUriMapBuilder.build();
2430   }
2431 
2432   /**
2433    * Get the current status of the file group. Since the status of the group is not stored in the
2434    * file group, this method iterates over all files and re-calculates the current status.
2435    *
2436    * <p>Note that this method doesn't modify the status of the file group on disk.
2437    */
2438   public ListenableFuture<GroupDownloadStatus> getFileGroupDownloadStatus(
2439       DataFileGroupInternal dataFileGroup) {
2440     return getFileGroupDownloadStatusIter(
2441         dataFileGroup,
2442         /* downloadFailed= */ false,
2443         /* downloadPending= */ false,
2444         /* index= */ 0,
2445         dataFileGroup.getFileCount());
2446   }
2447 
2448   // Because the decision to continue iterating depends on the result of the asynchronous
2449   // getFileStatus operation, we have to use recursion here instead of a loop construct.
2450   private ListenableFuture<GroupDownloadStatus> getFileGroupDownloadStatusIter(
2451       DataFileGroupInternal dataFileGroup,
2452       boolean downloadFailed,
2453       boolean downloadPending,
2454       int index,
2455       int fileCount) {
2456     if (index < fileCount) {
2457       DataFile dataFile = dataFileGroup.getFile(index);
2458 
2459       // Skip sideloaded files -- they are always considered downloaded.
2460       if (FileGroupUtil.isSideloadedFile(dataFile)) {
2461         return getFileGroupDownloadStatusIter(
2462             dataFileGroup, downloadFailed, downloadPending, index + 1, fileCount);
2463       }
2464 
2465       NewFileKey newFileKey =
2466           SharedFilesMetadata.createKeyFromDataFile(
2467               dataFile, dataFileGroup.getAllowedReadersEnum());
2468       return PropagatedFluentFuture.from(sharedFileManager.getFileStatus(newFileKey))
2469           .catchingAsync(
2470               SharedFileMissingException.class,
2471               e -> {
2472                 // TODO(b/118137672): reconsider on the swallowed exception.
2473                 LogUtil.e(
2474                     "%s: Encountered SharedFileMissingException for group: %s",
2475                     TAG, dataFileGroup.getGroupName());
2476                 silentFeedback.send(e, "Shared file not found in getFileGroupDownloadStatus");
2477                 return immediateFuture(FileStatus.NONE);
2478               },
2479               sequentialControlExecutor)
2480           .transformAsync(
2481               fileStatus -> {
2482                 if (fileStatus == FileStatus.DOWNLOAD_COMPLETE) {
2483                   LogUtil.d(
2484                       "%s: File %s downloaded for group: %s",
2485                       TAG, dataFile.getFileId(), dataFileGroup.getGroupName());
2486                   return getFileGroupDownloadStatusIter(
2487                       dataFileGroup, downloadFailed, downloadPending, index + 1, fileCount);
2488                 } else if (fileStatus == FileStatus.SUBSCRIBED
2489                     || fileStatus == FileStatus.DOWNLOAD_IN_PROGRESS) {
2490                   LogUtil.d(
2491                       "%s: File %s not downloaded for group: %s",
2492                       TAG, dataFile.getFileId(), dataFileGroup.getGroupName());
2493                   return getFileGroupDownloadStatusIter(
2494                       dataFileGroup,
2495                       downloadFailed,
2496                       /* downloadPending= */ true,
2497                       index + 1,
2498                       fileCount);
2499                 } else {
2500                   LogUtil.d(
2501                       "%s: File %s not downloaded for group: %s",
2502                       TAG, dataFile.getFileId(), dataFileGroup.getGroupName());
2503                   return getFileGroupDownloadStatusIter(
2504                       dataFileGroup,
2505                       /* downloadFailed= */ true,
2506                       downloadPending,
2507                       index + 1,
2508                       fileCount);
2509                 }
2510               },
2511               sequentialControlExecutor);
2512     } else if (downloadFailed) { // index == fileCount
2513       return immediateFuture(GroupDownloadStatus.FAILED);
2514     } else if (downloadPending) {
2515       return immediateFuture(GroupDownloadStatus.PENDING);
2516     } else {
2517       return immediateFuture(GroupDownloadStatus.DOWNLOADED);
2518     }
2519   }
2520 
2521   /**
2522    * Verify if any of the pending groups was downloaded.
2523    *
2524    * <p>If a group has been completely downloaded, it will be made available the next time a {@link
2525    * #getFileGroup} is called.
2526    */
2527   // TODO(b/124072754): Change to package private once all code is refactored.
2528   public ListenableFuture<Void> verifyAllPendingGroupsDownloaded(
2529       AsyncFunction<DataFileGroupInternal, Boolean> customFileGroupValidator) {
2530     return transformSequentialAsync(
2531         fileGroupsMetadata.getAllGroupKeys(),
2532         propagateAsyncFunction(
2533             groupKeyList ->
2534                 verifyAllPendingGroupsDownloaded(groupKeyList, customFileGroupValidator)));
2535   }
2536 
2537   private ListenableFuture<Void> verifyAllPendingGroupsDownloaded(
2538       List<GroupKey> groupKeyList,
2539       AsyncFunction<DataFileGroupInternal, Boolean> customFileGroupValidator) {
2540     List<ListenableFuture<GroupDownloadStatus>> allFileFutures = new ArrayList<>();
2541     for (GroupKey groupKey : groupKeyList) {
2542       if (groupKey.getDownloaded()) {
2543         continue;
2544       }
2545       allFileFutures.add(
2546           transformSequentialAsync(
2547               getFileGroup(groupKey, /* downloaded= */ false),
2548               pendingGroup -> {
2549                 // If no pending group exists for this group key, skip the verification.
2550                 if (pendingGroup == null) {
2551                   return immediateFuture(GroupDownloadStatus.PENDING);
2552                 }
2553                 return verifyGroupDownloaded(
2554                     groupKey,
2555                     pendingGroup,
2556                     /* removePendingVersion= */ true,
2557                     customFileGroupValidator,
2558                     DownloadStateLogger.forDownload(eventLogger));
2559               }));
2560     }
2561     return PropagatedFutures.whenAllComplete(allFileFutures)
2562         .call(() -> null, sequentialControlExecutor);
2563   }
2564 
2565   // TODO(b/124072754): Change to package private once all code is refactored.
2566   public ListenableFuture<Void> deleteUninstalledAppGroups() {
2567     return transformSequentialAsync(
2568         fileGroupsMetadata.getAllGroupKeys(),
2569         groupKeyList -> {
2570           List<ListenableFuture<Void>> removeGroupFutures = new ArrayList<>();
2571           for (GroupKey key : groupKeyList) {
2572             if (!isAppInstalled(key.getOwnerPackage())) {
2573               removeGroupFutures.add(
2574                   transformSequentialAsync(
2575                       fileGroupsMetadata.read(key),
2576                       group -> {
2577                         if (group == null) {
2578                           return immediateVoidFuture();
2579                         }
2580                         LogUtil.d(
2581                             "%s: Deleting file group %s for uninstalled app %s",
2582                             TAG, key.getGroupName(), key.getOwnerPackage());
2583                         eventLogger.logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
2584                         return transformSequentialAsync(
2585                             fileGroupsMetadata.remove(key),
2586                             removeSuccess -> {
2587                               if (!removeSuccess) {
2588                                 eventLogger.logEventSampled(
2589                                     MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
2590                               }
2591                               return immediateVoidFuture();
2592                             });
2593                       }));
2594             }
2595           }
2596           return PropagatedFutures.whenAllComplete(removeGroupFutures)
2597               .call(() -> null, sequentialControlExecutor);
2598         });
2599   }
2600 
2601   ListenableFuture<Void> deleteRemovedAccountGroups() {
2602     // In the library case, the account manager should be present. But in the GmsCore service case,
2603     // the account manager is absent, and the removed-account check is skipped.
2604     if (!accountSourceOptional.isPresent()) {
2605       return immediateVoidFuture();
2606     }
2607 
2608     ImmutableSet<String> serializedAccounts;
2609     try {
2610       serializedAccounts = getSerializedGoogleAccounts(accountSourceOptional.get());
2611     } catch (RuntimeException e) {
2612       // getSerializedGoogleAccounts could throw a SecurityException, which will bubble up and
2613       // prevent any other maintenance tasks from being performed. Instead, catch it and wrap it in
2614       // an LF so other tasks are performed even if this fails.
2615       return immediateFailedFuture(e);
2616     }
2617 
2618     return transformSequentialAsync(
2619         fileGroupsMetadata.getAllGroupKeys(),
2620         groupKeyList -> {
2621           List<ListenableFuture<Void>> removeGroupFutures = new ArrayList<>();
2622           for (GroupKey key : groupKeyList) {
2623             if (key.getAccount().isEmpty() || serializedAccounts.contains(key.getAccount())) {
2624               continue;
2625             }
2626 
2627             removeGroupFutures.add(
2628                 transformSequentialAsync(
2629                     fileGroupsMetadata.read(key),
2630                     group -> {
2631                       if (group == null) {
2632                         return immediateVoidFuture();
2633                       }
2634 
2635                       LogUtil.d(
2636                           "%s: Deleting file group %s for removed account %s",
2637                           TAG, key.getGroupName(), key.getOwnerPackage());
2638                       logEventWithDataFileGroup(
2639                           MddClientEvent.Code.EVENT_CODE_UNSPECIFIED, eventLogger, group);
2640 
2641                       // Remove the group from fresh file groups if the account is removed.
2642                       return transformSequentialAsync(
2643                           fileGroupsMetadata.remove(key),
2644                           removeSuccess -> {
2645                             if (!removeSuccess) {
2646                               logEventWithDataFileGroup(
2647                                   MddClientEvent.Code.EVENT_CODE_UNSPECIFIED, eventLogger, group);
2648                             }
2649                             return immediateVoidFuture();
2650                           });
2651                     }));
2652           }
2653 
2654           return PropagatedFutures.whenAllComplete(removeGroupFutures)
2655               .call(() -> null, sequentialControlExecutor);
2656         });
2657   }
2658 
2659   /**
2660    * Accumulates download started count. Sets download started timestamp if it has not been set
2661    * before. Writes the pending group back to metadata after the timestamp is set. Logs download
2662    * started event.
2663    */
2664   private ListenableFuture<DataFileGroupInternal> updateBookkeepingOnStartDownload(
2665       GroupKey groupKey, DataFileGroupInternal pendingGroup) {
2666     // Accumulate download started count, since we're scheduling download for the file group.
2667     DataFileGroupBookkeeping bookkeeping = pendingGroup.getBookkeeping();
2668     int downloadStartedCount = bookkeeping.getDownloadStartedCount() + 1;
2669     pendingGroup =
2670         pendingGroup.toBuilder()
2671             .setBookkeeping(bookkeeping.toBuilder().setDownloadStartedCount(downloadStartedCount))
2672             .build();
2673 
2674     // Only set the download started timestamp once.
2675     boolean firstDownloadAttempt = !bookkeeping.hasGroupDownloadStartedTimestampInMillis();
2676     if (firstDownloadAttempt) {
2677       pendingGroup =
2678           FileGroupUtil.setDownloadStartedTimestampInMillis(
2679               pendingGroup, timeSource.currentTimeMillis());
2680     }
2681 
2682     // Variables captured in lambdas must be effectively final.
2683     DataFileGroupInternal pendingGroupCapture = pendingGroup;
2684     GroupKey pendingGroupKey = groupKey.toBuilder().setDownloaded(false).build();
2685     return transformSequentialAsync(
2686         fileGroupsMetadata.write(pendingGroupKey, pendingGroup),
2687         writeSuccess -> {
2688           if (!writeSuccess) {
2689             eventLogger.logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
2690             return immediateFailedFuture(new IOException("Unable to update file group metadata"));
2691           }
2692 
2693           // Only log download stated event when bookkeping is successfully updated upon the first
2694           // download attempt (for dedup purposes).
2695           if (firstDownloadAttempt) {
2696             DownloadStateLogger.forDownload(eventLogger).logStarted(pendingGroupCapture);
2697           }
2698 
2699           return immediateFuture(pendingGroupCapture);
2700         });
2701   }
2702 
2703   /** Gets a set of {@link NewFileKey}'s which are referenced by some fresh group. */
2704   private ListenableFuture<ImmutableSet<NewFileKey>> getFileKeysReferencedByFreshGroups() {
2705     ImmutableSet.Builder<NewFileKey> referencedFileKeys = ImmutableSet.builder();
2706     return transformSequential(
2707         fileGroupsMetadata.getAllFreshGroups(),
2708         pairs -> {
2709           for (GroupKeyAndGroup pair : pairs) {
2710             DataFileGroupInternal fileGroup = pair.dataFileGroup();
2711             for (DataFile dataFile : fileGroup.getFileList()) {
2712               NewFileKey newFileKey =
2713                   SharedFilesMetadata.createKeyFromDataFile(
2714                       dataFile, fileGroup.getAllowedReadersEnum());
2715               referencedFileKeys.add(newFileKey);
2716             }
2717           }
2718           return referencedFileKeys.build();
2719         });
2720   }
2721 
2722   /** Logs download failure remotely via {@code eventLogger}. */
2723   // incompatible argument for parameter code of logMddDownloadResult.
2724   @SuppressWarnings("nullness:argument.type.incompatible")
2725   private ListenableFuture<Void> logDownloadFailure(
2726       GroupKey groupKey, DownloadException downloadException, long buildId, String variantId) {
2727     DataDownloadFileGroupStats.Builder groupDetails =
2728         DataDownloadFileGroupStats.newBuilder()
2729             .setFileGroupName(groupKey.getGroupName())
2730             .setOwnerPackage(groupKey.getOwnerPackage())
2731             .setBuildId(buildId)
2732             .setVariantId(variantId);
2733 
2734     return transformSequentialAsync(
2735         fileGroupsMetadata.read(groupKey.toBuilder().setDownloaded(false).build()),
2736         dataFileGroup -> {
2737           if (dataFileGroup != null) {
2738             groupDetails.setFileGroupVersionNumber(dataFileGroup.getFileGroupVersionNumber());
2739           }
2740 
2741           eventLogger.logMddDownloadResult(
2742               MddDownloadResult.Code.forNumber(downloadException.getDownloadResultCode().getCode()),
2743               groupDetails.build());
2744           return immediateVoidFuture();
2745         });
2746   }
2747 
2748   private ListenableFuture<Boolean> subscribeGroup(DataFileGroupInternal dataFileGroup) {
2749     return subscribeGroup(dataFileGroup, /* index= */ 0, dataFileGroup.getFileCount());
2750   }
2751 
2752   // Because the decision to continue iterating or not depends on the result of the asynchronous
2753   // reserveFileEntry operation, we have to use recursion instead of a loop construct.
2754   private ListenableFuture<Boolean> subscribeGroup(
2755       DataFileGroupInternal dataFileGroup, int index, int fileCount) {
2756     if (index < fileCount) {
2757       DataFile dataFile = dataFileGroup.getFile(index);
2758 
2759       // Skip sideloaded files since they will not interact with SharedFileManager
2760       if (FileGroupUtil.isSideloadedFile(dataFile)) {
2761         return subscribeGroup(dataFileGroup, index + 1, fileCount);
2762       }
2763 
2764       NewFileKey newFileKey =
2765           SharedFilesMetadata.createKeyFromDataFile(
2766               dataFile, dataFileGroup.getAllowedReadersEnum());
2767       return transformSequentialAsync(
2768           sharedFileManager.reserveFileEntry(newFileKey),
2769           success -> {
2770             if (!success) {
2771               // If we fail to reserve for one of the files, return immediately. Any files added
2772               // already will be cleared by garbage collection.
2773               LogUtil.e(
2774                   "%s: Subscribing to file failed for group: %s",
2775                   TAG, dataFileGroup.getGroupName());
2776               return immediateFuture(false);
2777             } else {
2778               return subscribeGroup(dataFileGroup, index + 1, fileCount);
2779             }
2780           });
2781     } else {
2782       return immediateFuture(true);
2783     }
2784   }
2785 
2786   private ListenableFuture<Optional<Integer>> isAddedGroupDuplicate(
2787       GroupKey groupKey, DataFileGroupInternal dataFileGroup) {
2788     // Search for a non-downloaded version of this group.
2789     GroupKey pendingGroupKey = groupKey.toBuilder().setDownloaded(false).build();
2790     return transformSequentialAsync(
2791         fileGroupsMetadata.read(pendingGroupKey),
2792         pendingGroup -> {
2793           if (pendingGroup != null) {
2794             return immediateFuture(areSameGroup(dataFileGroup, pendingGroup));
2795           }
2796 
2797           // Search for a downloaded version of this group.
2798           GroupKey downloadedGroupKey = groupKey.toBuilder().setDownloaded(true).build();
2799           return transformSequentialAsync(
2800               fileGroupsMetadata.read(downloadedGroupKey),
2801               downloadedGroup -> {
2802                 Optional<Integer> result =
2803                     (downloadedGroup == null)
2804                         ? Optional.of(0)
2805                         : areSameGroup(dataFileGroup, downloadedGroup);
2806                 return immediateFuture(result);
2807               });
2808         });
2809   }
2810 
2811   /**
2812    * Check if the new group is same as existing version. This just checks the fields that we expect
2813    * to be set when we receive a new group. Other fields are ignored.
2814    *
2815    * @param newGroup The new config that we received for the client.
2816    * @param prevGroup The old config that we already have for the client.
2817    * @return absent if the group is the same, otherwise a code for why the new config isn't the same
2818    */
2819   private static Optional<Integer> areSameGroup(
2820       DataFileGroupInternal newGroup, DataFileGroupInternal prevGroup) {
2821     // We do not compare the protos directly and check individual fields because proto.equals
2822     // also compares extensions (and unknown fields).
2823     // TODO: Consider clearing extensions and then comparing protos.
2824     if (prevGroup.getBuildId() != newGroup.getBuildId()) {
2825       return Optional.of(0);
2826     }
2827     if (!prevGroup.getVariantId().equals(newGroup.getVariantId())) {
2828       return Optional.of(0);
2829     }
2830     if (prevGroup.getFileGroupVersionNumber() != newGroup.getFileGroupVersionNumber()) {
2831       return Optional.of(0);
2832     }
2833     if (!hasSameFiles(newGroup, prevGroup)) {
2834       return Optional.of(0);
2835     }
2836     if (prevGroup.getStaleLifetimeSecs() != newGroup.getStaleLifetimeSecs()) {
2837       return Optional.of(0);
2838     }
2839     if (prevGroup.getExpirationDateSecs() != newGroup.getExpirationDateSecs()) {
2840       return Optional.of(0);
2841     }
2842     if (!prevGroup.getDownloadConditions().equals(newGroup.getDownloadConditions())) {
2843       return Optional.of(0);
2844     }
2845     if (!prevGroup.getAllowedReadersEnum().equals(newGroup.getAllowedReadersEnum())) {
2846       return Optional.of(0);
2847     }
2848 //    if (!prevGroup.getExperimentInfo().equals(newGroup.getExperimentInfo())) {
2849 //      return Optional.of(0);
2850 //    }
2851     return Optional.absent();
2852   }
2853 
2854   /**
2855    * Check if the new group has the same set of files as prev groups.
2856    *
2857    * @param newGroup The new config that we received for the client.
2858    * @param prevGroup The old config that we already have for the client.
2859    * @return true iff - All urlToDownloads are the same - Their checksums are the same - Their sizes
2860    *     are the same.
2861    */
2862   private static boolean hasSameFiles(
2863       DataFileGroupInternal newGroup, DataFileGroupInternal prevGroup) {
2864     return newGroup.getFileList().equals(prevGroup.getFileList());
2865   }
2866 
2867   private ListenableFuture<DataFileGroupInternal> maybeSetGroupNewFilesReceivedTimestamp(
2868       GroupKey groupKey, DataFileGroupInternal receivedFileGroup) {
2869     // Search for a non-downloaded version of this group.
2870     GroupKey pendingGroupKey = groupKey.toBuilder().setDownloaded(false).build();
2871     return transformSequentialAsync(
2872         fileGroupsMetadata.read(pendingGroupKey),
2873         pendingGroup -> {
2874           // We will only set the GroupNewFilesReceivedTimestamp when either this is the first time
2875           // we receive this File Group or the files are changed. In other cases, we will keep the
2876           // existing timestamp. This will avoid reset timestamp when metadata of the File Group
2877           // changes but the files stay the same.
2878           long groupNewFilesReceivedTimestamp;
2879           if (pendingGroup != null && hasSameFiles(receivedFileGroup, pendingGroup)) {
2880             // The files are not changed, we will copy over the timestamp from the pending group.
2881             groupNewFilesReceivedTimestamp =
2882                 pendingGroup.getBookkeeping().getGroupNewFilesReceivedTimestamp();
2883           } else {
2884             // First time we receive this FileGroup or the files are changed, set the timestamp to
2885             // the current time.
2886             groupNewFilesReceivedTimestamp = timeSource.currentTimeMillis();
2887           }
2888           DataFileGroupInternal receivedFileGroupWithTimestamp =
2889               FileGroupUtil.setGroupNewFilesReceivedTimestamp(
2890                   receivedFileGroup, groupNewFilesReceivedTimestamp);
2891           return immediateFuture(receivedFileGroupWithTimestamp);
2892         });
2893   }
2894 
2895   private boolean isAppInstalled(String packageName) {
2896     try {
2897       context.getPackageManager().getApplicationInfo(packageName, 0);
2898       return true;
2899     } catch (NameNotFoundException e) {
2900       return false;
2901     }
2902   }
2903 
2904   private ImmutableSet<String> getSerializedGoogleAccounts(AccountSource accountSource) {
2905     ImmutableList<Account> accounts = accountSource.getAllAccounts();
2906 
2907     ImmutableSet.Builder<String> serializedAccounts = new ImmutableSet.Builder<>();
2908     for (Account account : accounts) {
2909       if (account.name != null && account.type != null) {
2910         serializedAccounts.add(AccountUtil.serialize(account));
2911       }
2912     }
2913     return serializedAccounts.build();
2914   }
2915 
2916   // Logs and deletes file groups where a file is missing or corrupted, allowing the group and its
2917   // files to be added again via phenotype.
2918   //
2919   // For detail, see b/119555756.
2920   // TODO(b/124072754): Change to package private once all code is refactored.
2921   public ListenableFuture<Void> logAndDeleteForMissingSharedFiles() {
2922     return iterateOverAllFileGroups(
2923         groupKeyAndGroup -> {
2924           DataFileGroupInternal dataFileGroup = groupKeyAndGroup.dataFileGroup();
2925 
2926           for (DataFile dataFile : dataFileGroup.getFileList()) {
2927             NewFileKey newFileKey =
2928                 SharedFilesMetadata.createKeyFromDataFile(
2929                     dataFile, dataFileGroup.getAllowedReadersEnum());
2930             ListenableFuture<Void> unused =
2931                 PropagatedFutures.catchingAsync(
2932                     sharedFileManager.reVerifyFile(newFileKey, dataFile),
2933                     SharedFileMissingException.class,
2934                     e -> {
2935                       LogUtil.e("%s: Missing file. Logging and deleting file group.", TAG);
2936                       logEventWithDataFileGroup(
2937                           MddClientEvent.Code.EVENT_CODE_UNSPECIFIED, eventLogger, dataFileGroup);
2938 
2939                       if (flags.deleteFileGroupsWithFilesMissing()) {
2940                         return transformSequentialAsync(
2941                             fileGroupsMetadata.remove(groupKeyAndGroup.groupKey()),
2942                             ok -> immediateVoidFuture());
2943                       }
2944                       return immediateVoidFuture();
2945                     },
2946                     sequentialControlExecutor);
2947           }
2948           return immediateVoidFuture();
2949         });
2950   }
2951 
2952   /**
2953    * Verifies that any isolated files (symlinks) still exist for all file groups. If any are
2954    * missing, it attempts to recreate them.
2955    */
2956   @TargetApi(VERSION_CODES.LOLLIPOP)
2957   public ListenableFuture<Void> verifyAndAttemptToRepairIsolatedFiles() {
2958     // No symlinks available on pre-Lollipop devices
2959     if (VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) {
2960       return immediateVoidFuture();
2961     }
2962 
2963     return iterateOverAllFileGroups(
2964         groupKeyAndGroup -> {
2965           GroupKey groupKey = groupKeyAndGroup.groupKey();
2966           DataFileGroupInternal dataFileGroup = groupKeyAndGroup.dataFileGroup();
2967 
2968           if (dataFileGroup == null
2969               || !groupKey.getDownloaded()
2970               || !FileGroupUtil.isIsolatedStructureAllowed(dataFileGroup)) {
2971             return immediateVoidFuture();
2972           }
2973 
2974           return transformSequentialAsync(
2975               maybeVerifyIsolatedStructure(dataFileGroup, /* isDownloaded= */ true),
2976               verified -> {
2977                 if (!verified) {
2978                   return PropagatedFluentFuture.from(createIsolatedFilePaths(dataFileGroup))
2979                       .catchingAsync(
2980                           DownloadException.class,
2981                           exception -> {
2982                             LogUtil.w(
2983                                 exception,
2984                                 "%s: Unable to correct isolated structure, returning null"
2985                                     + " instead of group %s",
2986                                 TAG,
2987                                 dataFileGroup.getGroupName());
2988                             return immediateVoidFuture();
2989                           },
2990                           sequentialControlExecutor);
2991                 }
2992                 return immediateVoidFuture();
2993               });
2994         });
2995   }
2996 
2997   private ListenableFuture<Void> iterateOverAllFileGroups(
2998       AsyncFunction<GroupKeyAndGroup, Void> processGroup) {
2999 
3000     List<ListenableFuture<Void>> allGroupsProcessed = new ArrayList<>();
3001 
3002     return transformSequentialAsync(
3003         fileGroupsMetadata.getAllGroupKeys(),
3004         groupKeyList -> {
3005           for (GroupKey groupKey : groupKeyList) {
3006             allGroupsProcessed.add(
3007                 transformSequentialAsync(
3008                     fileGroupsMetadata.read(groupKey),
3009                     dataFileGroup ->
3010                         (dataFileGroup != null)
3011                             ? processGroup.apply(GroupKeyAndGroup.create(groupKey, dataFileGroup))
3012                             : immediateVoidFuture()));
3013           }
3014           return PropagatedFutures.whenAllComplete(allGroupsProcessed)
3015               .call(() -> null, sequentialControlExecutor);
3016         });
3017   }
3018 
3019   /** Dumps the current internal state of the FileGroupManager. */
3020   public ListenableFuture<Void> dump(final PrintWriter writer) {
3021     writer.println("==== MDD_FILE_GROUP_MANAGER ====");
3022     writer.println("MDD_FRESH_FILE_GROUPS:");
3023     ListenableFuture<Void> writeDataFileGroupsFuture =
3024         transformSequentialAsync(
3025             fileGroupsMetadata.getAllFreshGroups(),
3026             dataFileGroups -> {
3027               ArrayList<GroupKeyAndGroup> sortedFileGroups = new ArrayList<>(dataFileGroups);
3028               Collections.sort(
3029                   sortedFileGroups,
3030                   (pairA, pairB) ->
3031                       ComparisonChain.start()
3032                           .compare(pairA.groupKey().getGroupName(), pairB.groupKey().getGroupName())
3033                           .compare(pairA.groupKey().getAccount(), pairB.groupKey().getAccount())
3034                           .result());
3035               for (GroupKeyAndGroup dataFileGroupPair : sortedFileGroups) {
3036                 // TODO(b/131166925): MDD dump should not use lite proto toString.
3037                 writer.format(
3038                     "GroupName: %s\nAccount: %s\nDataFileGroup:\n %s\n\n",
3039                     dataFileGroupPair.groupKey().getGroupName(),
3040                     dataFileGroupPair.groupKey().getAccount(),
3041                     dataFileGroupPair.dataFileGroup().toString());
3042               }
3043               return immediateVoidFuture();
3044             });
3045     return transformSequentialAsync(
3046         writeDataFileGroupsFuture,
3047         voidParam -> {
3048           writer.println("MDD_STALE_FILE_GROUPS:");
3049           return transformSequentialAsync(
3050               fileGroupsMetadata.getAllStaleGroups(),
3051               staleGroups -> {
3052                 for (DataFileGroupInternal fileGroup : staleGroups) {
3053                   // TODO(b/131166925): MDD dump should not use lite proto toString.
3054                   writer.format(
3055                       "GroupName: %s\nDataFileGroup:\n%s\n",
3056                       fileGroup.getGroupName(), fileGroup.toString());
3057                 }
3058                 return immediateVoidFuture();
3059               });
3060         });
3061   }
3062 
3063   /**
3064    * TriggerSync for all pending groups. This is a catch-all effort in case triggerSync was not
3065    * triggered before.
3066    */
3067   // TODO(b/160770792): Change to package private once all code is refactored.
3068   public ListenableFuture<Void> triggerSyncAllPendingGroups() {
3069     return immediateVoidFuture();
3070   }
3071 
3072   private static void logMddAndroidSharingLog(
3073       EventLogger eventLogger, DataFileGroupInternal fileGroup, DataFile dataFile, int code) {
3074     Void androidSharingEvent = null;
3075     eventLogger.logMddAndroidSharingLog(androidSharingEvent);
3076   }
3077 
3078   private static void logMddAndroidSharingLog(
3079       EventLogger eventLogger,
3080       DataFileGroupInternal fileGroup,
3081       DataFile dataFile,
3082       int code,
3083       boolean leaseAcquired,
3084       long expiryDate) {
3085     Void androidSharingEvent = null;
3086     eventLogger.logMddAndroidSharingLog(androidSharingEvent);
3087   }
3088 
3089   private static void logEventWithDataFileGroup(
3090       MddClientEvent.Code code, EventLogger eventLogger, DataFileGroupInternal fileGroup) {
3091     eventLogger.logEventSampled(
3092         code,
3093         fileGroup.getGroupName(),
3094         fileGroup.getFileGroupVersionNumber(),
3095         fileGroup.getBuildId(),
3096         fileGroup.getVariantId());
3097   }
3098 
3099   private <I, O> ListenableFuture<O> transformSequential(
3100       ListenableFuture<I> input, Function<? super I, ? extends O> function) {
3101     return PropagatedFutures.transform(input, function, sequentialControlExecutor);
3102   }
3103 
3104   private <I, O> ListenableFuture<O> transformSequentialAsync(
3105       ListenableFuture<I> input, AsyncFunction<? super I, ? extends O> function) {
3106     return PropagatedFutures.transformAsync(input, function, sequentialControlExecutor);
3107   }
3108 }
3109