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