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