• 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.downloader;
17 
18 import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
19 import static com.google.common.util.concurrent.Futures.immediateVoidFuture;
20 
21 import android.content.Context;
22 import android.net.Uri;
23 import android.os.Build.VERSION;
24 import android.os.Build.VERSION_CODES;
25 import com.google.android.libraries.mobiledatadownload.DownloadException;
26 import com.google.android.libraries.mobiledatadownload.DownloadException.DownloadResultCode;
27 import com.google.android.libraries.mobiledatadownload.Flags;
28 import com.google.android.libraries.mobiledatadownload.SilentFeedback;
29 import com.google.android.libraries.mobiledatadownload.delta.DeltaDecoder;
30 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage;
31 import com.google.android.libraries.mobiledatadownload.internal.SharedFilesMetadata;
32 import com.google.android.libraries.mobiledatadownload.internal.downloader.MddFileDownloader.DownloaderCallback;
33 import com.google.android.libraries.mobiledatadownload.internal.logging.EventLogger;
34 import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil;
35 import com.google.android.libraries.mobiledatadownload.internal.util.DirectoryUtil;
36 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFluentFuture;
37 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures;
38 import com.google.common.base.Ascii;
39 import com.google.common.base.Optional;
40 import com.google.common.util.concurrent.ListenableFuture;
41 import com.google.mobiledatadownload.LogProto.DataDownloadFileGroupStats;
42 import com.google.mobiledatadownload.internal.MetadataProto.DataFile;
43 import com.google.mobiledatadownload.internal.MetadataProto.DataFileGroupInternal.AllowedReaders;
44 import com.google.mobiledatadownload.internal.MetadataProto.DeltaFile;
45 import com.google.mobiledatadownload.internal.MetadataProto.FileStatus;
46 import com.google.mobiledatadownload.internal.MetadataProto.GroupKey;
47 import com.google.mobiledatadownload.internal.MetadataProto.NewFileKey;
48 import java.io.IOException;
49 import java.util.concurrent.Executor;
50 
51 /**
52  * Impl for {@link DownloaderCallback} that handles delta download file, to restore full file with
53  * on device base file and the downloaded delta file.
54  */
55 public final class DeltaFileDownloaderCallbackImpl implements DownloaderCallback {
56   private static final String TAG = "DeltaFileDownloaderCallbackImpl";
57 
58   private final Context context;
59   private final SharedFilesMetadata sharedFilesMetadata;
60   private final SynchronousFileStorage fileStorage;
61   private final SilentFeedback silentFeedback;
62   private final DataFile dataFile;
63   private final AllowedReaders allowedReaders;
64   private final DeltaDecoder deltaDecoder;
65   private final DeltaFile deltaFile;
66   private final EventLogger eventLogger;
67   private final GroupKey groupKey;
68   private final int fileGroupVersionNumber;
69   private final long buildId;
70   private final String variantId;
71   private final Optional<String> instanceId;
72   private final Flags flags;
73   private final Executor sequentialControlExecutor;
74 
DeltaFileDownloaderCallbackImpl( Context context, SharedFilesMetadata sharedFilesMetadata, SynchronousFileStorage fileStorage, SilentFeedback silentFeedback, DataFile dataFile, AllowedReaders allowedReaders, DeltaDecoder deltaDecoder, DeltaFile deltaFile, EventLogger eventLogger, GroupKey groupKey, int fileGroupVersionNumber, long buildId, String variantId, Optional<String> instanceId, Flags flags, Executor sequentialControlExecutor)75   public DeltaFileDownloaderCallbackImpl(
76       Context context,
77       SharedFilesMetadata sharedFilesMetadata,
78       SynchronousFileStorage fileStorage,
79       SilentFeedback silentFeedback,
80       DataFile dataFile,
81       AllowedReaders allowedReaders,
82       DeltaDecoder deltaDecoder,
83       DeltaFile deltaFile,
84       EventLogger eventLogger,
85       GroupKey groupKey,
86       int fileGroupVersionNumber,
87       long buildId,
88       String variantId,
89       Optional<String> instanceId,
90       Flags flags,
91       Executor sequentialControlExecutor) {
92     this.context = context;
93     this.sharedFilesMetadata = sharedFilesMetadata;
94     this.fileStorage = fileStorage;
95     this.silentFeedback = silentFeedback;
96     this.dataFile = dataFile;
97     this.allowedReaders = allowedReaders;
98     this.deltaDecoder = deltaDecoder;
99     this.deltaFile = deltaFile;
100     this.eventLogger = eventLogger;
101     this.groupKey = groupKey;
102     this.fileGroupVersionNumber = fileGroupVersionNumber;
103     this.buildId = buildId;
104     this.variantId = variantId;
105     this.instanceId = instanceId;
106     this.flags = flags;
107     this.sequentialControlExecutor = sequentialControlExecutor;
108   }
109 
110   @Override
onDownloadComplete(Uri deltaFileUri)111   public ListenableFuture<Void> onDownloadComplete(Uri deltaFileUri) {
112     LogUtil.d("%s: Successfully downloaded delta file %s", TAG, deltaFileUri);
113 
114     if (!FileValidator.verifyChecksum(fileStorage, deltaFileUri, deltaFile.getChecksum())) {
115       LogUtil.e(
116           "%s: Downloaded delta file at uri = %s, checksum = %s verification failed",
117           TAG, deltaFileUri, deltaFile.getChecksum());
118       DownloadException exception =
119           DownloadException.builder()
120               .setDownloadResultCode(DownloadResultCode.DOWNLOADED_FILE_CHECKSUM_MISMATCH_ERROR)
121               .build();
122       // File was downloaded successfully, but failed checksum mismatch error. This indicates a
123       // corrupted file that should be deleted so MDD can attempt to redownload from scratch.
124       return PropagatedFluentFuture.from(
125               DownloaderCallbackImpl.maybeDeleteFileOnChecksumMismatch(
126                   sharedFilesMetadata,
127                   dataFile,
128                   allowedReaders,
129                   fileStorage,
130                   deltaFileUri,
131                   deltaFile.getChecksum(),
132                   eventLogger,
133                   flags,
134                   sequentialControlExecutor))
135           .catchingAsync(
136               IOException.class,
137               ioException -> {
138                 // Delete on checksum failed, add it as a suppressed exception if supported (API
139                 // level 19 or higher).
140                 if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
141                   exception.addSuppressed(ioException);
142                 }
143                 return immediateVoidFuture();
144               },
145               sequentialControlExecutor)
146           .transformAsync(unused -> immediateFailedFuture(exception), sequentialControlExecutor);
147     }
148 
149     Uri fullFileUri = FileNameUtil.getFinalFileUriWithTempDownloadedFile(deltaFileUri);
150     return PropagatedFutures.transformAsync(
151         handleDeltaDownloadFile(fullFileUri, deltaFileUri),
152         voidArg -> {
153           // TODO(b/149260496): once DeltaDownloader supports shared files, which have ChecksumType
154           // == SHA256, change from DataFile.ChecksumType.DFEFAULT to dataFile.getChecksumType().
155           if (!FileValidator.verifyChecksum(fileStorage, fullFileUri, dataFile.getChecksum())) {
156             LogUtil.e("%s: Final file checksum verification failed. %s.", TAG, fullFileUri);
157             return immediateFailedFuture(
158                 DownloadException.builder()
159                     .setDownloadResultCode(DownloadResultCode.FINAL_FILE_CHECKSUM_MISMATCH_ERROR)
160                     .build());
161           }
162 
163           return DownloaderCallbackImpl.updateFileStatus(
164               FileStatus.DOWNLOAD_COMPLETE,
165               dataFile,
166               allowedReaders,
167               sharedFilesMetadata,
168               sequentialControlExecutor);
169         },
170         sequentialControlExecutor);
171   }
172 
173   @Override
174   public ListenableFuture<Void> onDownloadFailed(DownloadException exception) {
175     LogUtil.d("%s: Failed to download file(delta) %s", TAG, dataFile.getChecksum());
176     if (exception
177         .getDownloadResultCode()
178         .equals(DownloadResultCode.DOWNLOADED_FILE_CHECKSUM_MISMATCH_ERROR)) {
179       return DownloaderCallbackImpl.updateFileStatus(
180           FileStatus.CORRUPTED,
181           dataFile,
182           allowedReaders,
183           sharedFilesMetadata,
184           sequentialControlExecutor);
185     }
186     return DownloaderCallbackImpl.updateFileStatus(
187         FileStatus.DOWNLOAD_FAILED,
188         dataFile,
189         allowedReaders,
190         sharedFilesMetadata,
191         sequentialControlExecutor);
192   }
193 
194   private ListenableFuture<Void> handleDeltaDownloadFile(Uri fullFileUri, Uri deltaFileUri) {
195     NewFileKey baseFileKey =
196         NewFileKey.newBuilder()
197             .setChecksum(deltaFile.getBaseFile().getChecksum())
198             .setAllowedReaders(allowedReaders)
199             .build();
200     return PropagatedFutures.transformAsync(
201         sharedFilesMetadata.read(baseFileKey),
202         baseFileMetadata -> {
203           Uri baseFileUri = null;
204           if (baseFileMetadata != null
205               && baseFileMetadata.getFileStatus() == FileStatus.DOWNLOAD_COMPLETE) {
206             baseFileUri =
207                 DirectoryUtil.getOnDeviceUri(
208                     context,
209                     allowedReaders,
210                     baseFileMetadata.getFileName(),
211                     baseFileKey.getChecksum(),
212                     silentFeedback,
213                     instanceId,
214                     /* androidShared= */ false);
215           }
216 
217           if (baseFileUri == null) {
218             return immediateFailedFuture(
219                 DownloadException.builder()
220                     .setDownloadResultCode(
221                         DownloadResultCode.DELTA_DOWNLOAD_BASE_FILE_NOT_FOUND_ERROR)
222                     .build());
223           }
224 
225           try {
226             decodeDeltaFile(baseFileUri, fullFileUri, deltaFileUri);
227           } catch (IOException e) {
228             LogUtil.e(
229                 e,
230                 "%s: Failed to decode delta file with url = %s failed. checksum = %s ",
231                 TAG,
232                 deltaFile.getUrlToDownload(),
233                 dataFile.getChecksum());
234             silentFeedback.send(e, "Failed to decode delta file.");
235             return immediateFailedFuture(
236                 DownloadException.builder()
237                     .setDownloadResultCode(DownloadResultCode.DELTA_DOWNLOAD_DECODE_IO_ERROR)
238                     .setCause(e)
239                     .build());
240           }
241           DataDownloadFileGroupStats fileGroupStats =
242               DataDownloadFileGroupStats.newBuilder()
243                   .setFileGroupName(groupKey.getGroupName())
244                   .setFileGroupVersionNumber(fileGroupVersionNumber)
245                   .setOwnerPackage(groupKey.getOwnerPackage())
246                   .setBuildId(buildId)
247                   .setVariantId(variantId)
248                   .build();
249           eventLogger.logMddNetworkSavings(
250               fileGroupStats,
251               0,
252               dataFile.getByteSize(),
253               deltaFile.getByteSize(),
254               dataFile.getFileId(),
255               getDeltaFileIndex());
256           return immediateVoidFuture();
257         },
258         sequentialControlExecutor);
259   }
260 
261   private int getDeltaFileIndex() {
262     for (int i = 0; i < dataFile.getDeltaFileCount(); i++) {
263       if (Ascii.equalsIgnoreCase(dataFile.getDeltaFile(i).getChecksum(), deltaFile.getChecksum())) {
264         return i + 1;
265       }
266     }
267     return 0;
268   }
269 
270   private void decodeDeltaFile(Uri baseFileUri, Uri fullFileUri, Uri deltaFileUri)
271       throws IOException {
272     if (fileStorage.exists(fullFileUri)) {
273       // Delete if the full file was partially downloaded before.
274       fileStorage.deleteFile(fullFileUri);
275     }
276     deltaDecoder.decode(baseFileUri, deltaFileUri, fullFileUri);
277     // Only delete delta file on success case. Not delete and re-download if decode fails as it is
278     // most likely configuration issue.
279     // TODO(b/123584890): Delete delta file on decode failure once MDD server test is in place.
280     fileStorage.deleteFile(deltaFileUri);
281   }
282 }
283