• 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.common.util.concurrent.Futures.getDone;
19 import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
20 import static com.google.common.util.concurrent.Futures.immediateFuture;
21 import static com.google.common.util.concurrent.Futures.immediateVoidFuture;
22 import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
23 
24 import android.content.Context;
25 import android.content.SharedPreferences;
26 import android.net.Uri;
27 import android.os.Build.VERSION;
28 import android.os.Build.VERSION_CODES;
29 import androidx.annotation.VisibleForTesting;
30 import com.google.android.libraries.mobiledatadownload.DownloadException;
31 import com.google.android.libraries.mobiledatadownload.DownloadException.DownloadResultCode;
32 import com.google.android.libraries.mobiledatadownload.FileSource;
33 import com.google.android.libraries.mobiledatadownload.Flags;
34 import com.google.android.libraries.mobiledatadownload.SilentFeedback;
35 import com.google.android.libraries.mobiledatadownload.annotations.InstanceId;
36 import com.google.android.libraries.mobiledatadownload.delta.DeltaDecoder;
37 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage;
38 import com.google.android.libraries.mobiledatadownload.file.common.UnsupportedFileStorageOperation;
39 import com.google.android.libraries.mobiledatadownload.internal.Migrations.FileKeyVersion;
40 import com.google.android.libraries.mobiledatadownload.internal.annotations.SequentialControlExecutor;
41 import com.google.android.libraries.mobiledatadownload.internal.downloader.DeltaFileDownloaderCallbackImpl;
42 import com.google.android.libraries.mobiledatadownload.internal.downloader.DownloaderCallbackImpl;
43 import com.google.android.libraries.mobiledatadownload.internal.downloader.FileNameUtil;
44 import com.google.android.libraries.mobiledatadownload.internal.downloader.FileValidator;
45 import com.google.android.libraries.mobiledatadownload.internal.downloader.MddFileDownloader;
46 import com.google.android.libraries.mobiledatadownload.internal.downloader.MddFileDownloader.DownloaderCallback;
47 import com.google.android.libraries.mobiledatadownload.internal.logging.EventLogger;
48 import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil;
49 import com.google.android.libraries.mobiledatadownload.internal.util.DirectoryUtil;
50 import com.google.android.libraries.mobiledatadownload.internal.util.SharedPreferencesUtil;
51 import com.google.android.libraries.mobiledatadownload.monitor.DownloadProgressMonitor;
52 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFluentFuture;
53 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures;
54 import com.google.common.base.Optional;
55 import com.google.common.collect.ImmutableMap;
56 import com.google.common.collect.ImmutableSet;
57 import com.google.common.util.concurrent.Futures;
58 import com.google.common.util.concurrent.ListenableFuture;
59 import com.google.errorprone.annotations.CheckReturnValue;
60 import com.google.mobiledatadownload.LogEnumsProto.MddClientEvent;
61 import com.google.mobiledatadownload.internal.MetadataProto.DataFile;
62 import com.google.mobiledatadownload.internal.MetadataProto.DataFileGroupInternal;
63 import com.google.mobiledatadownload.internal.MetadataProto.DataFileGroupInternal.AllowedReaders;
64 import com.google.mobiledatadownload.internal.MetadataProto.DeltaFile;
65 import com.google.mobiledatadownload.internal.MetadataProto.DeltaFile.DiffDecoder;
66 import com.google.mobiledatadownload.internal.MetadataProto.DownloadConditions;
67 import com.google.mobiledatadownload.internal.MetadataProto.ExtraHttpHeader;
68 import com.google.mobiledatadownload.internal.MetadataProto.FileStatus;
69 import com.google.mobiledatadownload.internal.MetadataProto.GroupKey;
70 import com.google.mobiledatadownload.internal.MetadataProto.NewFileKey;
71 import com.google.mobiledatadownload.internal.MetadataProto.SharedFile;
72 import java.io.IOException;
73 import java.io.PrintWriter;
74 import java.util.ArrayList;
75 import java.util.List;
76 import java.util.concurrent.Executor;
77 import javax.annotation.Nullable;
78 import javax.inject.Inject;
79 import org.checkerframework.checker.nullness.compatqual.NullableType;
80 
81 /**
82  * Manages the life cycle of files used by MDD. For each file group in MDD, the file group will
83  * subscribe for the files that it needs. The SharedFileManager will maintain a reference count to
84  * ensure that it only retains files that are being used by MDD and that multiple file groups will
85  * share a single common file.
86  *
87  * <p>Whenever MDD receives a new filegroup, it will call {@link SharedFileManager#reserveFileEntry}
88  * for each file within the group.
89  *
90  * <p>When MDD discards a file group (because a new one has been received, downloaded), it will call
91  * {@link SharedFileManager#removeFileEntry} for each file within the group.
92  *
93  * <p>Note: SharedFileManager is considered thread-compatible. Calls to methods that modify the
94  * state of SharedFileManager {@link SharedFileManager#reserveFileEntry}, {@link
95  * SharedFileManager#startDownload}, {@link SharedFileManager#getFileStatus}, and {@link
96  * SharedFileManager#removeFileEntry} require exclusive access.
97  */
98 @CheckReturnValue
99 public class SharedFileManager {
100 
101   private static final String TAG = "SharedFileManager";
102 
103   public static final String MDD_SHARED_FILE_MANAGER_METADATA =
104       "gms_icing_mdd_shared_file_manager_metadata";
105 
106   @VisibleForTesting static final String PREFS_KEY_NEXT_FILE_NAME = "next_file_name_v2";
107   @VisibleForTesting static final String FILE_NAME_PREFIX = "datadownloadfile_";
108 
109   @VisibleForTesting
110   static final String PREFS_KEY_MIGRATED_TO_NEW_FILE_KEY = "migrated_to_new_file_key";
111 
112   private final Context context;
113   private final SilentFeedback silentFeedback;
114   private final SharedFilesMetadata sharedFilesMetadata;
115   private final MddFileDownloader fileDownloader;
116   private final SynchronousFileStorage fileStorage;
117   private final Optional<DeltaDecoder> deltaDecoderOptional;
118   private final Optional<DownloadProgressMonitor> downloadMonitorOptional;
119   private final EventLogger eventLogger;
120   private final Flags flags;
121   private final FileGroupsMetadata fileGroupsMetadata;
122   private final Optional<String> instanceId;
123   private final Executor sequentialControlExecutor;
124 
125   @Inject
SharedFileManager( @pplicationContext Context context, SilentFeedback silentFeedback, SharedFilesMetadata sharedFilesMetadata, SynchronousFileStorage fileStorage, MddFileDownloader fileDownloader, Optional<DeltaDecoder> deltaDecoderOptional, Optional<DownloadProgressMonitor> downloadMonitorOptional, EventLogger eventLogger, Flags flags, FileGroupsMetadata fileGroupsMetadata, @InstanceId Optional<String> instanceId, @SequentialControlExecutor Executor sequentialControlExecutor)126   public SharedFileManager(
127       @ApplicationContext Context context,
128       SilentFeedback silentFeedback,
129       SharedFilesMetadata sharedFilesMetadata,
130       SynchronousFileStorage fileStorage,
131       MddFileDownloader fileDownloader,
132       Optional<DeltaDecoder> deltaDecoderOptional,
133       Optional<DownloadProgressMonitor> downloadMonitorOptional,
134       EventLogger eventLogger,
135       Flags flags,
136       FileGroupsMetadata fileGroupsMetadata,
137       @InstanceId Optional<String> instanceId,
138       @SequentialControlExecutor Executor sequentialControlExecutor) {
139     this.context = context;
140     this.silentFeedback = silentFeedback;
141     this.sharedFilesMetadata = sharedFilesMetadata;
142     this.fileStorage = fileStorage;
143     this.fileDownloader = fileDownloader;
144     this.deltaDecoderOptional = deltaDecoderOptional;
145     this.downloadMonitorOptional = downloadMonitorOptional;
146     this.eventLogger = eventLogger;
147     this.flags = flags;
148     this.fileGroupsMetadata = fileGroupsMetadata;
149     this.instanceId = instanceId;
150     this.sequentialControlExecutor = sequentialControlExecutor;
151   }
152 
153   /**
154    * Makes any changes that should be made before accessing the internal state of this class.
155    *
156    * <p>Other methods in this class do not call or check if this method was already called before
157    * trying to access internal state. It is expected from the caller to call this before anything
158    * else.
159    *
160    * @return false if init failed, signalling caller to clear internal storage.
161    */
162   // TODO(b/124072754): Change to package private once all code is refactored.
init()163   public ListenableFuture<Boolean> init() {
164     SharedPreferences sharedFileManagerMetadata =
165         SharedPreferencesUtil.getSharedPreferences(
166             context, MDD_SHARED_FILE_MANAGER_METADATA, instanceId);
167 
168     // Migrations class was added in v24, whereas new file key migration done in v23. If we already
169     // migrated, check and set it in Migrations.
170     if (sharedFileManagerMetadata.contains(PREFS_KEY_MIGRATED_TO_NEW_FILE_KEY)) {
171       if (sharedFileManagerMetadata.getBoolean(PREFS_KEY_MIGRATED_TO_NEW_FILE_KEY, false)) {
172         Migrations.setMigratedToNewFileKey(context, true);
173       }
174       sharedFileManagerMetadata.edit().remove(PREFS_KEY_MIGRATED_TO_NEW_FILE_KEY).commit();
175     }
176 
177     return immediateFuture(true);
178   }
179 
180   /**
181    * Adds a subscribed file entry if there is no existing entry for newFileKey. Does nothing if such
182    * an entry already exists.
183    *
184    * @param newFileKey - the file key for the enry that you wish to reserve.
185    * @return - Future resolving to false if unable to commit the reservation
186    */
187   // TODO - refactor to throw Exception when write to SharedPreferences fails
reserveFileEntry(NewFileKey newFileKey)188   public ListenableFuture<Boolean> reserveFileEntry(NewFileKey newFileKey) {
189     return PropagatedFutures.transformAsync(
190         sharedFilesMetadata.read(newFileKey),
191         sharedFile -> {
192           if (sharedFile != null) {
193             // There's already an entry for this file. Nothing to do here.
194             return immediateFuture(true);
195           }
196           // Set the file name and update the metadata file.
197           SharedPreferences sharedFileManagerMetadata =
198               SharedPreferencesUtil.getSharedPreferences(
199                   context, MDD_SHARED_FILE_MANAGER_METADATA, instanceId);
200           long nextFileName =
201               sharedFileManagerMetadata.getLong(
202                   PREFS_KEY_NEXT_FILE_NAME, System.currentTimeMillis());
203           if (!sharedFileManagerMetadata
204               .edit()
205               .putLong(PREFS_KEY_NEXT_FILE_NAME, nextFileName + 1)
206               .commit()) {
207             // TODO(b/131166925): MDD dump should not use lite proto toString.
208             LogUtil.e("%s: Unable to update file name %s", TAG, newFileKey);
209             return immediateFuture(false);
210           }
211 
212           String fileName = FILE_NAME_PREFIX + nextFileName;
213           sharedFile =
214               SharedFile.newBuilder()
215                   .setFileStatus(FileStatus.SUBSCRIBED)
216                   .setFileName(fileName)
217                   .build();
218           return PropagatedFutures.transformAsync(
219               sharedFilesMetadata.write(newFileKey, sharedFile),
220               writeSuccess -> {
221                 if (!writeSuccess) {
222                   // TODO(b/131166925): MDD dump should not use lite proto toString.
223                   LogUtil.e(
224                       "%s: Unable to write back subscription for file entry with %s",
225                       TAG, newFileKey);
226                   return immediateFuture(false);
227                 }
228                 return immediateFuture(true);
229               },
230               sequentialControlExecutor);
231         },
232         sequentialControlExecutor);
233   }
234 
235   /**
236    * Start importing a given file source if the file has not yet been downloaded/imported.
237    *
238    * <p>This method expects {@code dataFile} to have an "inlinefile:" scheme url. A
239    * DownloadException will be returned if a non-inlinefile scheme url is given.
240    *
241    * <p>If the file has already been downloaded/imported, this method is a no-op.
242    */
243   ListenableFuture<Void> startImport(
244       GroupKey groupKey,
245       DataFile dataFile,
246       NewFileKey newFileKey,
247       @Nullable DownloadConditions downloadConditions,
248       FileSource inlineFileSource) {
249     if (!dataFile.getUrlToDownload().startsWith(MddConstants.INLINE_FILE_URL_SCHEME)) {
250       return immediateFailedFuture(
251           DownloadException.builder()
252               .setDownloadResultCode(DownloadResultCode.INVALID_INLINE_FILE_URL_SCHEME)
253               .setMessage("Importing an inline file requires inlinefile scheme")
254               .build());
255     }
256     return PropagatedFutures.transformAsync(
257         sharedFilesMetadata.read(newFileKey),
258         sharedFile -> {
259           if (sharedFile == null) {
260             LogUtil.e(
261                 "%s: Start import called on file that doesn't exist. Id = %s",
262                 TAG, dataFile.getFileId());
263             SharedFileMissingException cause = new SharedFileMissingException();
264             // TODO(b/167582815): Log to Clearcut
265             return immediateFailedFuture(
266                 DownloadException.builder()
267                     .setDownloadResultCode(DownloadResultCode.SHARED_FILE_NOT_FOUND_ERROR)
268                     .setCause(cause)
269                     .build());
270           }
271 
272           // If we have already downloaded the file, then return.
273           if (sharedFile.getFileStatus() == FileStatus.DOWNLOAD_COMPLETE) {
274             return immediateVoidFuture();
275           }
276 
277           // Delta files are not supported, so only check for download transforms
278           SharedFile.Builder sharedFileBuilder = sharedFile.toBuilder();
279           String downloadFileName =
280               dataFile.hasDownloadTransforms()
281                   ? FileNameUtil.getTempFileNameWithDownloadedFileChecksum(
282                       sharedFile.getFileName(), dataFile.getDownloadedFileChecksum())
283                   : sharedFile.getFileName();
284 
285           return PropagatedFutures.transformAsync(
286               getDataFileGroupOrDefault(groupKey),
287               dataFileGroup ->
288                   getImportFuture(
289                       sharedFileBuilder,
290                       newFileKey,
291                       downloadFileName,
292                       dataFileGroup.getFileGroupVersionNumber(),
293                       dataFileGroup.getBuildId(),
294                       dataFileGroup.getVariantId(),
295                       groupKey,
296                       dataFile,
297                       downloadConditions,
298                       inlineFileSource),
299               sequentialControlExecutor);
300         },
301         sequentialControlExecutor);
302   }
303 
304   /**
305    * Gets a future that will perform the import.
306    *
307    * <p>Updates the sharedFile status to in-progress and attaches a callback to the import to handle
308    * post import actions.
309    */
310   private ListenableFuture<Void> getImportFuture(
311       SharedFile.Builder sharedFileBuilder,
312       NewFileKey newFileKey,
313       String downloadFileName,
314       int fileGroupVersionNumber,
315       long buildId,
316       String variantId,
317       GroupKey groupKey,
318       DataFile dataFile,
319       @Nullable DownloadConditions downloadConditions,
320       FileSource inlineFileSource) {
321     ListenableFuture<Uri> downloadFileOnDeviceUriFuture =
322         getDownloadFileOnDeviceUri(
323             newFileKey.getAllowedReaders(), downloadFileName, dataFile.getChecksum());
324     return PropagatedFluentFuture.from(downloadFileOnDeviceUriFuture)
325         .transformAsync(
326             unused -> {
327               sharedFileBuilder.setFileStatus(FileStatus.DOWNLOAD_IN_PROGRESS);
328 
329               // Write returns a boolean indicating if the operation was successful or not. We can
330               // ignore this because a failure to write here won't impact the import operation. We
331               // will attempt to write the final state (completed or failed) after the import
332               // operation.
333               return sharedFilesMetadata.write(newFileKey, sharedFileBuilder.build());
334             },
335             sequentialControlExecutor)
336         .transformAsync(
337             unused -> {
338               Uri downloadFileOnDeviceUri = getDone(downloadFileOnDeviceUriFuture);
339               DownloaderCallback downloaderCallback =
340                   new DownloaderCallbackImpl(
341                       sharedFilesMetadata,
342                       fileStorage,
343                       dataFile,
344                       newFileKey.getAllowedReaders(),
345                       eventLogger,
346                       groupKey,
347                       fileGroupVersionNumber,
348                       buildId,
349                       variantId,
350                       flags,
351                       sequentialControlExecutor);
352               // TODO: when partial import files are supported, notify monitor of partial
353               // progress here.
354 
355               return fileDownloader.startCopying(
356                   newFileKey.getChecksum(),
357                   downloadFileOnDeviceUri,
358                   dataFile.getUrlToDownload(),
359                   dataFile.getByteSize(),
360                   downloadConditions,
361                   downloaderCallback,
362                   inlineFileSource);
363             },
364             sequentialControlExecutor);
365   }
366 
367   /**
368    * Start downloading the file if the file has not yet been downloaded. If the file has been
369    * downloaded, this method is a no-op.
370    *
371    * @param groupKey - a Key that uniquely identify a file group.
372    * @param dataFile - the data file proto provided by client
373    * @param newFileKey - the file key to get the SharedFile.
374    * @param downloadConditions - conditions under which this file should be downloaded.
375    * @param trafficTag - Tag for the network traffic to download this dataFile.
376    * @param extraHttpHeaders - Extra Headers for this request.
377    * @return - ListenableFuture representing the download the file. The ListenableFuture fails with
378    *     {@link DownloadException} if the download is unsuccessful.
379    */
380   ListenableFuture<Void> startDownload(
381       GroupKey groupKey,
382       DataFile dataFile,
383       NewFileKey newFileKey,
384       @Nullable DownloadConditions downloadConditions,
385       int trafficTag,
386       List<ExtraHttpHeader> extraHttpHeaders) {
387     if (dataFile.getUrlToDownload().startsWith(MddConstants.INLINE_FILE_URL_SCHEME)) {
388       return immediateFailedFuture(
389           DownloadException.builder()
390               .setDownloadResultCode(DownloadResultCode.INVALID_INLINE_FILE_URL_SCHEME)
391               .setMessage(
392                   "downloading a file with an inlinefile scheme is not supported, use importFiles"
393                       + " instead.")
394               .build());
395     }
396 
397     // Start futures in parallel for various calculated properties.
398     ListenableFuture<SharedFile> sharedFileFuture = getSharedFile(newFileKey);
399 
400     ListenableFuture<@NullableType DeltaFile> firstDeltaFileFuture =
401         findFirstDeltaFileWithBaseFileDownloaded(dataFile, newFileKey.getAllowedReaders());
402 
403     ListenableFuture<String> downloadFileNameFuture =
404         PropagatedFutures.whenAllSucceed(sharedFileFuture, firstDeltaFileFuture)
405             .call(
406                 () -> {
407                   String downloadFileName = getDone(sharedFileFuture).getFileName();
408                   DeltaFile deltaFile = getDone(firstDeltaFileFuture);
409                   if (deltaFile != null) {
410                     downloadFileName =
411                         FileNameUtil.getTempFileNameWithDownloadedFileChecksum(
412                             downloadFileName, deltaFile.getChecksum());
413                   } else if (dataFile.hasDownloadTransforms()) {
414                     downloadFileName =
415                         FileNameUtil.getTempFileNameWithDownloadedFileChecksum(
416                             downloadFileName, dataFile.getDownloadedFileChecksum());
417                   }
418                   return downloadFileName;
419                 },
420                 directExecutor());
421 
422     ListenableFuture<Uri> downloadFileOnDeviceUriFuture =
423         PropagatedFutures.transformAsync(
424             downloadFileNameFuture,
425             downloadFileName ->
426                 getDownloadFileOnDeviceUri(
427                     newFileKey.getAllowedReaders(), downloadFileName, dataFile.getChecksum()),
428             sequentialControlExecutor);
429 
430     ListenableFuture<DataFileGroupInternal> dataFileGroupFuture =
431         getDataFileGroupOrDefault(groupKey);
432 
433     // Combine all futures together so all complete successfully before continuing
434     ListenableFuture<Void> combinedPropertiesFuture =
435         PropagatedFutures.whenAllSucceed(
436                 sharedFileFuture,
437                 firstDeltaFileFuture,
438                 downloadFileNameFuture,
439                 downloadFileOnDeviceUriFuture,
440                 dataFileGroupFuture)
441             .callAsync(Futures::immediateVoidFuture, directExecutor());
442 
443     return PropagatedFluentFuture.from(combinedPropertiesFuture)
444         .transformAsync(
445             unused -> {
446               SharedFile sharedFile = getDone(sharedFileFuture);
447               DeltaFile deltaFile = getDone(firstDeltaFileFuture);
448               String downloadFileName = getDone(downloadFileNameFuture);
449               Uri downloadFileOnDeviceUri = getDone(downloadFileOnDeviceUriFuture);
450               DataFileGroupInternal dataFileGroup = getDone(dataFileGroupFuture);
451 
452               // Check if download is complete
453               if (sharedFile.getFileStatus() == FileStatus.DOWNLOAD_COMPLETE) {
454                 if (downloadMonitorOptional.isPresent()) {
455                   // For the downloaded file, we don't need to monitor the file change. We just need
456                   // to inform the monitor about its current size.
457                   downloadMonitorOptional
458                       .get()
459                       .notifyCurrentFileSize(groupKey.getGroupName(), dataFile.getByteSize());
460                 }
461                 return immediateVoidFuture();
462               }
463 
464               // Check if a download is already in progress
465               if (sharedFile.getFileStatus() == FileStatus.DOWNLOAD_IN_PROGRESS) {
466                 return PropagatedFutures.transformAsync(
467                     fileDownloader.getInProgressFuture(
468                         newFileKey.getChecksum(), downloadFileOnDeviceUri),
469                     inProgressFuture -> {
470                       if (inProgressFuture.isPresent()) {
471                         mayNotifyCurrentSizeOfPartiallyDownloadedFile(
472                             groupKey, downloadFileOnDeviceUri);
473                         return inProgressFuture.get();
474                       }
475                       return getDownloadFuture(
476                           newFileKey,
477                           downloadFileName,
478                           dataFileGroup.getFileGroupVersionNumber(),
479                           dataFileGroup.getBuildId(),
480                           dataFileGroup.getVariantId(),
481                           groupKey,
482                           dataFile,
483                           deltaFile,
484                           downloadConditions,
485                           trafficTag,
486                           extraHttpHeaders);
487                     },
488                     sequentialControlExecutor);
489               }
490 
491               // Download is not in progress, start it.
492               return getDownloadFuture(
493                   newFileKey,
494                   downloadFileName,
495                   dataFileGroup.getFileGroupVersionNumber(),
496                   dataFileGroup.getBuildId(),
497                   dataFileGroup.getVariantId(),
498                   groupKey,
499                   dataFile,
500                   deltaFile,
501                   downloadConditions,
502                   trafficTag,
503                   extraHttpHeaders);
504             },
505             sequentialControlExecutor)
506         .catchingAsync(
507             SharedFileMissingException.class,
508             ex -> {
509               // TODO(b/131166925): MDD dump should not use lite proto toString.
510               LogUtil.e(
511                   "%s: Start download called on file that doesn't exist. Key = %s!",
512                   TAG, newFileKey);
513               silentFeedback.send(ex, "Shared file not found in downloadFileGroup");
514               return immediateFailedFuture(
515                   DownloadException.builder()
516                       .setDownloadResultCode(DownloadResultCode.SHARED_FILE_NOT_FOUND_ERROR)
517                       .setCause(ex)
518                       .build());
519             },
520             sequentialControlExecutor);
521   }
522 
523   private ListenableFuture<Void> getDownloadFuture(
524       NewFileKey newFileKey,
525       String downloadFileName,
526       int fileGroupVersionNumber,
527       long buildId,
528       String variantId,
529       GroupKey groupKey,
530       DataFile dataFile,
531       @Nullable DeltaFile deltaFile,
532       @Nullable DownloadConditions downloadConditions,
533       int trafficTag,
534       List<ExtraHttpHeader> extraHttpHeaders) {
535     // It's possible to hit a race condition where the caller of this method sees the file as not
536     // downloaded and by the time this method is executed, the file is already downloaded.
537     //
538     // Check the shared file status before starting the download to confirm it is not downloaded and
539     // a download is not already in progress.
540     return PropagatedFutures.transformAsync(
541         getSharedFile(newFileKey),
542         latestSharedFile -> {
543           if (latestSharedFile.getFileStatus() == FileStatus.DOWNLOAD_COMPLETE) {
544             return immediateVoidFuture();
545           }
546 
547           // Download is not complete, proceed with starting the future.
548           SharedFile.Builder sharedFileBuilder = latestSharedFile.toBuilder();
549           ListenableFuture<Uri> downloadFileOnDeviceUriFuture =
550               getDownloadFileOnDeviceUri(
551                   newFileKey.getAllowedReaders(), downloadFileName, dataFile.getChecksum());
552           return PropagatedFluentFuture.from(downloadFileOnDeviceUriFuture)
553               .transformAsync(
554                   unused -> {
555                     sharedFileBuilder.setFileStatus(FileStatus.DOWNLOAD_IN_PROGRESS);
556 
557                     // Ignoring failure to write back here, as it will just result in one
558                     // extra try
559                     // to download the file.
560                     return sharedFilesMetadata.write(newFileKey, sharedFileBuilder.build());
561                   },
562                   sequentialControlExecutor)
563               .transformAsync(
564                   unused -> {
565                     Uri downloadFileOnDeviceUri = getDone(downloadFileOnDeviceUriFuture);
566                     ListenableFuture<Void> fileDownloadFuture;
567                     if (!deltaDecoderOptional.isPresent() || deltaFile == null) {
568                       // Download full file when delta file is null
569                       DownloaderCallback downloaderCallback =
570                           new DownloaderCallbackImpl(
571                               sharedFilesMetadata,
572                               fileStorage,
573                               dataFile,
574                               newFileKey.getAllowedReaders(),
575                               eventLogger,
576                               groupKey,
577                               fileGroupVersionNumber,
578                               buildId,
579                               variantId,
580                               flags,
581                               sequentialControlExecutor);
582 
583                       mayNotifyCurrentSizeOfPartiallyDownloadedFile(
584                           groupKey, downloadFileOnDeviceUri);
585 
586                       fileDownloadFuture =
587                           fileDownloader.startDownloading(
588                               newFileKey.getChecksum(),
589                               groupKey,
590                               fileGroupVersionNumber,
591                               buildId,
592                               variantId,
593                               downloadFileOnDeviceUri,
594                               dataFile.getUrlToDownload(),
595                               dataFile.getByteSize(),
596                               downloadConditions,
597                               downloaderCallback,
598                               trafficTag,
599                               extraHttpHeaders);
600                     } else {
601                       DownloaderCallback downloaderCallback =
602                           new DeltaFileDownloaderCallbackImpl(
603                               context,
604                               sharedFilesMetadata,
605                               fileStorage,
606                               silentFeedback,
607                               dataFile,
608                               newFileKey.getAllowedReaders(),
609                               deltaDecoderOptional.get(),
610                               deltaFile,
611                               eventLogger,
612                               groupKey,
613                               fileGroupVersionNumber,
614                               buildId,
615                               variantId,
616                               instanceId,
617                               flags,
618                               sequentialControlExecutor);
619 
620                       mayNotifyCurrentSizeOfPartiallyDownloadedFile(
621                           groupKey, downloadFileOnDeviceUri);
622 
623                       fileDownloadFuture =
624                           fileDownloader.startDownloading(
625                               newFileKey.getChecksum(),
626                               groupKey,
627                               fileGroupVersionNumber,
628                               buildId,
629                               variantId,
630                               downloadFileOnDeviceUri,
631                               deltaFile.getUrlToDownload(),
632                               deltaFile.getByteSize(),
633                               downloadConditions,
634                               downloaderCallback,
635                               trafficTag,
636                               extraHttpHeaders);
637                     }
638                     return fileDownloadFuture;
639                   },
640                   sequentialControlExecutor);
641         },
642         sequentialControlExecutor);
643   }
644 
645   /**
646    * Gets the URI where the given file should be located on-device.
647    *
648    * @param allowedReaders the allowed readers of the file
649    * @param downloadFileName the name of the file
650    * @param checksum the checksum of the file
651    */
652   private ListenableFuture<Uri> getDownloadFileOnDeviceUri(
653       AllowedReaders allowedReaders, String downloadFileName, String checksum) {
654     Uri downloadFileOnDeviceUri =
655         DirectoryUtil.getOnDeviceUri(
656             context,
657             allowedReaders,
658             downloadFileName,
659             checksum,
660             silentFeedback,
661             instanceId,
662             /* androidShared= */ false);
663     if (downloadFileOnDeviceUri == null) {
664       LogUtil.e("%s: Failed to get file uri!", TAG);
665       return immediateFailedFuture(
666           DownloadException.builder()
667               .setDownloadResultCode(DownloadResultCode.UNABLE_TO_CREATE_FILE_URI_ERROR)
668               .build());
669     }
670     return immediateFuture(downloadFileOnDeviceUri);
671   }
672 
673   private void mayNotifyCurrentSizeOfPartiallyDownloadedFile(
674       GroupKey groupKey, Uri downloadFileOnDeviceUri) {
675     if (downloadMonitorOptional.isPresent()) {
676       // Inform the monitor about the current size of the partially downloaded file.
677       try {
678         long currentFileSize = fileStorage.fileSize(downloadFileOnDeviceUri);
679         if (currentFileSize > 0) {
680           downloadMonitorOptional
681               .get()
682               .notifyCurrentFileSize(groupKey.getGroupName(), currentFileSize);
683         }
684       } catch (IOException e) {
685         // Ignore any fileSize error.
686       }
687     }
688   }
689 
690   private ListenableFuture<DataFileGroupInternal> getDataFileGroupOrDefault(GroupKey groupKey) {
691     return PropagatedFutures.transformAsync(
692         fileGroupsMetadata.read(groupKey),
693         fileGroup ->
694             immediateFuture(
695                 (fileGroup == null) ? DataFileGroupInternal.getDefaultInstance() : fileGroup),
696         sequentialControlExecutor);
697   }
698 
699   /**
700    * @param dataFile - a DataFile proto object
701    * @param allowedReaders - allowed readers of the file group, assuming the base file has the same
702    *     readers set
703    * @return - the first Delta file which its base file is on device and its file status is download
704    *     completed
705    */
706   @VisibleForTesting
707   ListenableFuture<@NullableType DeltaFile> findFirstDeltaFileWithBaseFileDownloaded(
708       DataFile dataFile, AllowedReaders allowedReaders) {
709     if (Migrations.getCurrentVersion(context, silentFeedback).value
710             < FileKeyVersion.USE_CHECKSUM_ONLY.value
711         || !deltaDecoderOptional.isPresent()
712         || deltaDecoderOptional.get().getDecoderName() == DiffDecoder.UNSPECIFIED) {
713       return immediateFuture(null);
714     }
715     return findFirstDeltaFileWithBaseFileDownloaded(
716         dataFile.getDeltaFileList(), /* index= */ 0, allowedReaders);
717   }
718 
719   // We must use recursion here since the decision to continue iterating is dependent on the result
720   // of the asynchronous SharedFilesMetadata.read() operation
721   private ListenableFuture<@NullableType DeltaFile> findFirstDeltaFileWithBaseFileDownloaded(
722       List<DeltaFile> deltaFiles, int index, AllowedReaders allowedReaders) {
723     if (index == deltaFiles.size()) {
724       return immediateFuture(null);
725     }
726     DeltaFile deltaFile = deltaFiles.get(index);
727     if (deltaFile.getDiffDecoder() != deltaDecoderOptional.get().getDecoderName()) {
728       return findFirstDeltaFileWithBaseFileDownloaded(deltaFiles, index + 1, allowedReaders);
729     }
730     NewFileKey baseFileKey =
731         NewFileKey.newBuilder()
732             .setChecksum(deltaFile.getBaseFile().getChecksum())
733             .setAllowedReaders(allowedReaders)
734             .build();
735     return PropagatedFutures.transformAsync(
736         sharedFilesMetadata.read(baseFileKey),
737         baseFileMetadata -> {
738           if (baseFileMetadata != null
739               && baseFileMetadata.getFileStatus() == FileStatus.DOWNLOAD_COMPLETE) {
740             Uri baseFileUri =
741                 DirectoryUtil.getOnDeviceUri(
742                     context,
743                     baseFileKey.getAllowedReaders(),
744                     baseFileMetadata.getFileName(),
745                     baseFileKey.getChecksum(),
746                     silentFeedback,
747                     instanceId,
748                     /* androidShared= */ false);
749             if (baseFileUri != null) {
750               return immediateFuture(deltaFile);
751             }
752           }
753           return findFirstDeltaFileWithBaseFileDownloaded(deltaFiles, index + 1, allowedReaders);
754         },
755         sequentialControlExecutor);
756   }
757   /**
758    * Returns the current status of the file.
759    *
760    * @param newFileKey - the file key to get the SharedFile.
761    * @return - FileStatus representing the current state of the file. The ListenableFuture may throw
762    *     a SharedFileMissingException if the shared file metadata is missing.
763    */
764   ListenableFuture<FileStatus> getFileStatus(NewFileKey newFileKey) {
765     return PropagatedFutures.transformAsync(
766         getSharedFile(newFileKey),
767         existingSharedFile -> immediateFuture(existingSharedFile.getFileStatus()),
768         sequentialControlExecutor);
769   }
770 
771   /**
772    * Verifies that the file exists in metadata and on disk. Also performs the same validation check
773    * that's performed after download to ensure the file hasn't been deleted or corrupted.
774    *
775    * @param newFileKey - the file key to get the SharedFile.
776    * @return - The ListenableFuture may throw a SharedFileMissingException if the shared file
777    *     metadata is missing or the on disk file is corrupted.
778    */
779   ListenableFuture<Void> reVerifyFile(NewFileKey newFileKey, DataFile dataFile) {
780     return PropagatedFluentFuture.from(getSharedFile(newFileKey))
781         .transformAsync(
782             existingSharedFile -> {
783               if (existingSharedFile.getFileStatus() != FileStatus.DOWNLOAD_COMPLETE) {
784                 return immediateVoidFuture();
785               }
786               // Double check that it's really complete, and update status if it's not.
787               return PropagatedFluentFuture.from(getOnDeviceUri(newFileKey))
788                   .transformAsync(
789                       uri -> {
790                         if (uri == null) {
791                           throw DownloadException.builder()
792                               .setDownloadResultCode(
793                                   DownloadResultCode.DOWNLOADED_FILE_NOT_FOUND_ERROR)
794                               .build();
795                         }
796                         if (existingSharedFile.getAndroidShared()) {
797                           // Just check for presence. BlobStoreManager is responsible for
798                           // integrity.
799                           if (!fileStorage.exists(uri)) {
800                             throw DownloadException.builder()
801                                 .setDownloadResultCode(
802                                     DownloadResultCode.DOWNLOADED_FILE_NOT_FOUND_ERROR)
803                                 .build();
804                           }
805                         } else {
806                           FileValidator.validateDownloadedFile(
807                               fileStorage, dataFile, uri, dataFile.getChecksum());
808                         }
809                         return immediateVoidFuture();
810                       },
811                       sequentialControlExecutor)
812                   .catchingAsync(
813                       DownloadException.class,
814                       e -> {
815                         LogUtil.e(
816                             "%s: reVerifyFile lost or corrupted code %s",
817                             TAG, e.getDownloadResultCode());
818                         SharedFile updatedSharedFile =
819                             existingSharedFile.toBuilder()
820                                 .setFileStatus(FileStatus.CORRUPTED)
821                                 .build();
822                         return PropagatedFluentFuture.from(
823                                 sharedFilesMetadata.write(newFileKey, updatedSharedFile))
824                             .transformAsync(
825                                 ok -> {
826                                   SharedFileMissingException ex = new SharedFileMissingException();
827                                   if (!ok) {
828                                     throw new IOException("failed to save sharedFilesMetadata", ex);
829                                   }
830                                   throw ex;
831                                 },
832                                 sequentialControlExecutor);
833                       },
834                       sequentialControlExecutor);
835             },
836             sequentialControlExecutor);
837   }
838 
839   /**
840    * Returns the {@code SharedFile}.
841    *
842    * @param newFileKey - the file key to get the SharedFile.
843    * @return - the SharedFile representing the current metadata of the file. The ListenableFuture
844    *     may throw a SharedFileMissingException if the shared file metadata is missing.
845    */
846   ListenableFuture<SharedFile> getSharedFile(NewFileKey newFileKey) {
847     return PropagatedFutures.transformAsync(
848         sharedFilesMetadata.read(newFileKey),
849         existingSharedFile -> {
850           if (existingSharedFile == null) {
851             // TODO(b/131166925): MDD dump should not use lite proto toString.
852             LogUtil.e(
853                 "%s: getSharedFile called on file that doesn't exist! Key = %s", TAG, newFileKey);
854             return immediateFailedFuture(new SharedFileMissingException());
855           }
856           return immediateFuture(existingSharedFile);
857         },
858         sequentialControlExecutor);
859   }
860 
861   /**
862    * Sets a file entry as downloaded and android-shared. If there is an existing entry for {@code
863    * newFileKey}, overwrites it.
864    *
865    * @param newFileKey - the file key for the enry that you wish to store.
866    * @param androidSharingChecksum - the file checksum that represents a blob in the Android Sharing
867    *     Service.
868    * @param maxExpirationDateSecs - the new maximum expiration date
869    * @return - false if unable to commit the write operation
870    */
871   ListenableFuture<Boolean> setAndroidSharedDownloadedFileEntry(
872       NewFileKey newFileKey, String androidSharingChecksum, long maxExpirationDateSecs) {
873     SharedFile newSharedFile =
874         SharedFile.newBuilder()
875             .setFileStatus(FileStatus.DOWNLOAD_COMPLETE)
876             .setFileName("android_shared_" + androidSharingChecksum)
877             .setAndroidShared(true)
878             .setMaxExpirationDateSecs(maxExpirationDateSecs)
879             .setAndroidSharingChecksum(androidSharingChecksum)
880             .build();
881     return sharedFilesMetadata.write(newFileKey, newSharedFile);
882   }
883 
884   /**
885    * If necessary, updates the {@code max_expiration_date} date stored in the shared file metadata
886    * associated to {@code newFileKey}. No-op otherwise.
887    *
888    * @param newFileKey - the file key for the enry that you wish to update.
889    * @param fileExpirationDateSecs - the expiration date of the current file.
890    * @return - false if unable to commit the write operation. The ListenableFuture may throw a
891    *     SharedFileMissingException if the shared file metadata is missing.
892    */
893   ListenableFuture<Boolean> updateMaxExpirationDateSecs(
894       NewFileKey newFileKey, long fileExpirationDateSecs) {
895     return PropagatedFutures.transformAsync(
896         getSharedFile(newFileKey),
897         existingSharedFile -> {
898           if (fileExpirationDateSecs > existingSharedFile.getMaxExpirationDateSecs()) {
899             SharedFile updatedSharedFile =
900                 existingSharedFile.toBuilder()
901                     .setMaxExpirationDateSecs(fileExpirationDateSecs)
902                     .build();
903             return sharedFilesMetadata.write(newFileKey, updatedSharedFile);
904           }
905           return immediateFuture(true);
906         },
907         sequentialControlExecutor);
908   }
909 
910   /**
911    * Returns future resolving to uri for the file.
912    *
913    * @param newFileKey - the file key to get the SharedFile.
914    * @return - a future resolving to the MobStore android Uri that is associated with the file. The
915    *     uri will be null if the SharedFileManager doesn't have an entry matching that file or there
916    *     is an error populating the uri of the file.
917    */
918   public ListenableFuture<@NullableType Uri> getOnDeviceUri(NewFileKey newFileKey) {
919     return PropagatedFutures.transform(
920         getOnDeviceUris(ImmutableSet.of(newFileKey)),
921         uris -> uris.get(newFileKey),
922         directExecutor());
923   }
924 
925   /**
926    * Get the known on-device uris for a given list of {@link NewFileKey}s
927    *
928    * <p>The returned map may or may not have an entry for each NewFileKey on the list, depending on
929    * if it was possible to create the uri (see {@link DirectoryUtil#getOnDeviceUri()} for more
930    * details).
931    *
932    * <p>If any {@link NewFileKey} does not map to a {@link SharedFile}, the returned future will be
933    * a failure containing {@link SharedFileMissingException}.
934    */
935   ListenableFuture<ImmutableMap<NewFileKey, Uri>> getOnDeviceUris(
936       ImmutableSet<NewFileKey> newFileKeys) {
937     return PropagatedFluentFuture.from(sharedFilesMetadata.readAll(newFileKeys))
938         .transformAsync(
939             sharedFileMap -> {
940               ImmutableMap.Builder<NewFileKey, Uri> uriMapBuilder = ImmutableMap.builder();
941               for (NewFileKey newFileKey : newFileKeys) {
942                 // Make sure all SharedFiles exist.
943                 if (!sharedFileMap.containsKey(newFileKey)) {
944                   // TODO(b/131166925): MDD dump should not use lite proto toString.
945                   LogUtil.e(
946                       "%s: getOnDeviceUris called on file that doesn't exist. Key = %s!",
947                       TAG, newFileKey);
948                   return immediateFailedFuture(new SharedFileMissingException());
949                 }
950 
951                 SharedFile sharedFile = sharedFileMap.get(newFileKey);
952 
953                 Uri onDeviceUri =
954                     DirectoryUtil.getOnDeviceUri(
955                         context,
956                         newFileKey.getAllowedReaders(),
957                         sharedFile.getFileName(),
958                         sharedFile.getAndroidSharingChecksum(),
959                         silentFeedback,
960                         instanceId,
961                         sharedFile.getAndroidShared());
962                 if (onDeviceUri != null) {
963                   uriMapBuilder.put(newFileKey, onDeviceUri);
964                 }
965               }
966               return immediateFuture(uriMapBuilder.buildKeepingLast());
967             },
968             sequentialControlExecutor);
969   }
970 
971   /**
972    * Removes the file entry corresponding to newFileKey. If the file hasn't been fully downloaded,
973    * the partial file is deleted from the device and the download is cancelled.
974    *
975    * @param newFileKey - the key of the file entry to remove.
976    * @return - false if there is no entry with this key or unable to remove the entry
977    */
978   // TODO - refactor to throw Exception when write to SharedPreferences fails
979   ListenableFuture<Boolean> removeFileEntry(NewFileKey newFileKey) {
980     return PropagatedFutures.transformAsync(
981         sharedFilesMetadata.read(newFileKey),
982         sharedFile -> {
983           if (sharedFile == null) {
984             // TODO(b/131166925): MDD dump should not use lite proto toString.
985             LogUtil.e("%s: No file entry with key %s", TAG, newFileKey);
986             return immediateFuture(false);
987           }
988 
989           Uri onDeviceUri =
990               DirectoryUtil.getOnDeviceUri(
991                   context,
992                   newFileKey.getAllowedReaders(),
993                   sharedFile.getFileName(),
994                   newFileKey.getChecksum(),
995                   silentFeedback,
996                   instanceId,
997                   /* androidShared= */ false);
998           if (onDeviceUri != null) {
999             fileDownloader.stopDownloading(newFileKey.getChecksum(), onDeviceUri);
1000           }
1001           return PropagatedFutures.transformAsync(
1002               sharedFilesMetadata.remove(newFileKey),
1003               removeSuccess -> {
1004                 if (!removeSuccess) {
1005                   // TODO(b/131166925): MDD dump should not use lite proto toString.
1006                   LogUtil.e("%s: Unable to modify file subscription for key %s", TAG, newFileKey);
1007                   return immediateFuture(false);
1008                 }
1009                 return immediateFuture(true);
1010               },
1011               sequentialControlExecutor);
1012         },
1013         sequentialControlExecutor);
1014   }
1015 
1016   /**
1017    * Clears all storage used by the SharedFileManager and deletes all files that have been
1018    * downloaded to MDD's directory.
1019    */
1020 
1021   // TODO(b/124072754): Change to package private once all code is refactored.
1022   public ListenableFuture<Void> clear() {
1023     // If sdk is R+, try release all leases that the MDD Client may have acquired. This
1024     // prevents from leaving zombie files in the blob storage.
1025     if (VERSION.SDK_INT >= VERSION_CODES.R) {
1026       releaseAllAndroidSharedFiles();
1027     }
1028     try {
1029       fileStorage.deleteRecursively(DirectoryUtil.getBaseDownloadDirectory(context, instanceId));
1030     } catch (IOException e) {
1031       silentFeedback.send(e, "Failure while deleting mdd storage during clear");
1032     }
1033     return immediateVoidFuture();
1034   }
1035 
1036   private void releaseAllAndroidSharedFiles() {
1037     try {
1038       Uri allLeasesUri = DirectoryUtil.getBlobStoreAllLeasesUri(context);
1039       fileStorage.deleteFile(allLeasesUri);
1040       eventLogger.logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
1041     } catch (UnsupportedFileStorageOperation e) {
1042       LogUtil.v(
1043           "%s: Failed to release the leases in the android shared storage."
1044               + " UnsupportedFileStorageOperation was thrown",
1045           TAG);
1046     } catch (IOException e) {
1047       LogUtil.e(e, "%s: Failed to release the leases in the android shared storage", TAG);
1048       eventLogger.logEventSampled(MddClientEvent.Code.EVENT_CODE_UNSPECIFIED);
1049     }
1050   }
1051 
1052   public ListenableFuture<Void> cancelDownloadAndClear() {
1053     return PropagatedFutures.transformAsync(
1054         sharedFilesMetadata.getAllFileKeys(),
1055         newFileKeyList -> {
1056           List<ListenableFuture<Void>> cancelDownloadFutures = new ArrayList<>();
1057           try {
1058             // Clear is called in case something fails and we want to clear all of MDD internal
1059             // storage. Catching any exception in cancelling downloads is better than clear failing,
1060             // as it can leave the system in a non-recoverable state.
1061             for (NewFileKey newFileKey : newFileKeyList) {
1062               cancelDownloadFutures.add(cancelDownload(newFileKey));
1063             }
1064           } catch (Exception e) {
1065             silentFeedback.send(e, "Failed to cancel all downloads during clear");
1066           }
1067           return PropagatedFutures.whenAllComplete(cancelDownloadFutures)
1068               .callAsync(this::clear, sequentialControlExecutor);
1069         },
1070         sequentialControlExecutor);
1071   }
1072 
1073   public ListenableFuture<Void> cancelDownload(NewFileKey newFileKey) {
1074     return PropagatedFutures.transformAsync(
1075         sharedFilesMetadata.read(newFileKey),
1076         sharedFile -> {
1077           if (sharedFile == null) {
1078             LogUtil.e("%s: Unable to read sharedFile from shared preferences.", TAG);
1079             return immediateFailedFuture(new SharedFileMissingException());
1080           }
1081           if (sharedFile.getFileStatus() != FileStatus.DOWNLOAD_COMPLETE) {
1082             Uri onDeviceUri =
1083                 DirectoryUtil.getOnDeviceUri(
1084                     context,
1085                     newFileKey.getAllowedReaders(),
1086                     sharedFile.getFileName(),
1087                     newFileKey.getChecksum(),
1088                     silentFeedback,
1089                     instanceId,
1090                     /* androidShared= */ false); // while downloading androidShared is always false
1091             if (onDeviceUri != null) {
1092               fileDownloader.stopDownloading(newFileKey.getChecksum(), onDeviceUri);
1093             }
1094             // If the download was in progress, reset it back to subscribed, so it can be properly
1095             // restarted.
1096             if (sharedFile.getFileStatus() == FileStatus.DOWNLOAD_IN_PROGRESS) {
1097               return PropagatedFutures.transformAsync(
1098                   sharedFilesMetadata.write(
1099                       newFileKey,
1100                       sharedFile.toBuilder().setFileStatus(FileStatus.SUBSCRIBED).build()),
1101                   unused -> immediateVoidFuture(),
1102                   sequentialControlExecutor);
1103             }
1104           }
1105           return immediateVoidFuture();
1106         },
1107         sequentialControlExecutor);
1108   }
1109 
1110   /** Dumps the current internal state of the SharedFileManager. */
1111   public ListenableFuture<Void> dump(final PrintWriter writer) {
1112     writer.println("==== MDD_SHARED_FILES ====");
1113     return PropagatedFutures.transformAsync(
1114         sharedFilesMetadata.getAllFileKeys(),
1115         allFileKeys -> {
1116           ListenableFuture<Void> writeFilesFuture = immediateVoidFuture();
1117           for (NewFileKey newFileKey : allFileKeys) {
1118             writeFilesFuture =
1119                 PropagatedFutures.transformAsync(
1120                     writeFilesFuture,
1121                     voidArg ->
1122                         PropagatedFutures.transformAsync(
1123                             sharedFilesMetadata.read(newFileKey),
1124                             sharedFile -> {
1125                               if (sharedFile == null) {
1126                                 LogUtil.e(
1127                                     "%s: Unable to read sharedFile from shared preferences.", TAG);
1128                                 return immediateVoidFuture();
1129                               }
1130                               // TODO(b/131166925): MDD dump should not use lite proto toString.
1131                               writer.format(
1132                                   "FileKey: %s\nFileName: %s\nSharedFile: %s\n",
1133                                   newFileKey, sharedFile.getFileName(), sharedFile.toString());
1134                               if (sharedFile.getAndroidShared()) {
1135                                 writer.format(
1136                                     "Checksum Android-shared file: %s\n",
1137                                     sharedFile.getAndroidSharingChecksum());
1138                               } else {
1139                                 Uri serializedUri =
1140                                     DirectoryUtil.getOnDeviceUri(
1141                                         context,
1142                                         newFileKey.getAllowedReaders(),
1143                                         sharedFile.getFileName(),
1144                                         newFileKey.getChecksum(),
1145                                         silentFeedback,
1146                                         instanceId,
1147                                         /* androidShared= */ false);
1148                                 if (serializedUri != null) {
1149                                   writer.format(
1150                                       "Checksum downloaded file: %s\n",
1151                                       FileValidator.computeSha1Digest(fileStorage, serializedUri));
1152                                 }
1153                               }
1154                               return immediateVoidFuture();
1155                             },
1156                             sequentialControlExecutor),
1157                     sequentialControlExecutor);
1158           }
1159           return writeFilesFuture;
1160         },
1161         sequentialControlExecutor);
1162   }
1163 }
1164