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