1803 lines
81 KiB
Java
1803 lines
81 KiB
Java
/*
|
|
* Copyright 2022 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
package com.google.android.libraries.mobiledatadownload;
|
|
|
|
import static com.google.android.libraries.mobiledatadownload.tracing.TracePropagation.propagateAsyncFunction;
|
|
import static com.google.android.libraries.mobiledatadownload.tracing.TracePropagation.propagateRunnable;
|
|
import static com.google.common.base.Preconditions.checkNotNull;
|
|
import static com.google.common.util.concurrent.Futures.getDone;
|
|
import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
|
|
import static com.google.common.util.concurrent.Futures.immediateFuture;
|
|
import static com.google.common.util.concurrent.Futures.immediateVoidFuture;
|
|
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
|
|
|
|
import android.accounts.Account;
|
|
import android.content.Context;
|
|
import android.net.Uri;
|
|
import android.text.TextUtils;
|
|
import androidx.core.app.NotificationCompat;
|
|
import androidx.core.app.NotificationManagerCompat;
|
|
import com.google.android.libraries.mobiledatadownload.DownloadException.DownloadResultCode;
|
|
import com.google.android.libraries.mobiledatadownload.TaskScheduler.ConstraintOverrides;
|
|
import com.google.android.libraries.mobiledatadownload.TaskScheduler.NetworkState;
|
|
import com.google.android.libraries.mobiledatadownload.account.AccountUtil;
|
|
import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage;
|
|
import com.google.android.libraries.mobiledatadownload.foreground.ForegroundDownloadKey;
|
|
import com.google.android.libraries.mobiledatadownload.foreground.NotificationUtil;
|
|
import com.google.android.libraries.mobiledatadownload.internal.DownloadGroupState;
|
|
import com.google.android.libraries.mobiledatadownload.internal.ExceptionToMddResultMapper;
|
|
import com.google.android.libraries.mobiledatadownload.internal.MddConstants;
|
|
import com.google.android.libraries.mobiledatadownload.internal.MobileDataDownloadManager;
|
|
import com.google.android.libraries.mobiledatadownload.internal.collect.GroupKeyAndGroup;
|
|
import com.google.android.libraries.mobiledatadownload.internal.collect.GroupPair;
|
|
import com.google.android.libraries.mobiledatadownload.internal.logging.EventLogger;
|
|
import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil;
|
|
import com.google.android.libraries.mobiledatadownload.internal.util.DownloadFutureMap;
|
|
import com.google.android.libraries.mobiledatadownload.internal.util.MddLiteConversionUtil;
|
|
import com.google.android.libraries.mobiledatadownload.internal.util.ProtoConversionUtil;
|
|
import com.google.android.libraries.mobiledatadownload.lite.Downloader;
|
|
import com.google.android.libraries.mobiledatadownload.monitor.DownloadProgressMonitor;
|
|
import com.google.android.libraries.mobiledatadownload.tracing.PropagatedExecutionSequencer;
|
|
import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFluentFuture;
|
|
import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures;
|
|
import com.google.common.base.Optional;
|
|
import com.google.common.base.Preconditions;
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.google.common.collect.ImmutableSet;
|
|
import com.google.common.util.concurrent.AsyncFunction;
|
|
import com.google.common.util.concurrent.FutureCallback;
|
|
import com.google.common.util.concurrent.Futures;
|
|
import com.google.common.util.concurrent.ListenableFuture;
|
|
import com.google.common.util.concurrent.ListenableFutureTask;
|
|
import com.google.mobiledatadownload.ClientConfigProto.ClientFile;
|
|
import com.google.mobiledatadownload.ClientConfigProto.ClientFileGroup;
|
|
import com.google.mobiledatadownload.DownloadConfigProto;
|
|
import com.google.mobiledatadownload.DownloadConfigProto.DataFileGroup;
|
|
import com.google.mobiledatadownload.internal.MetadataProto.DataFile;
|
|
import com.google.mobiledatadownload.internal.MetadataProto.DataFileGroupInternal;
|
|
import com.google.mobiledatadownload.internal.MetadataProto.DownloadConditions;
|
|
import com.google.mobiledatadownload.internal.MetadataProto.DownloadConditions.DeviceNetworkPolicy;
|
|
import com.google.mobiledatadownload.internal.MetadataProto.GroupKey;
|
|
import com.google.mobiledatadownload.LogProto.DataDownloadFileGroupStats;
|
|
import com.google.protobuf.Any;
|
|
import com.google.protobuf.InvalidProtocolBufferException;
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.PrintWriter;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.concurrent.ExecutionException;
|
|
import java.util.concurrent.Executor;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.TimeoutException;
|
|
import javax.annotation.Nullable;
|
|
|
|
/**
|
|
* Default implementation for {@link
|
|
* com.google.android.libraries.mobiledatadownload.MobileDataDownload}.
|
|
*/
|
|
class MobileDataDownloadImpl implements MobileDataDownload {
|
|
|
|
private static final String TAG = "MobileDataDownload";
|
|
private static final long DUMP_DEBUG_INFO_TIMEOUT = 3;
|
|
|
|
private final Context context;
|
|
private final EventLogger eventLogger;
|
|
private final List<FileGroupPopulator> fileGroupPopulatorList;
|
|
private final Optional<TaskScheduler> taskSchedulerOptional;
|
|
private final MobileDataDownloadManager mobileDataDownloadManager;
|
|
private final SynchronousFileStorage fileStorage;
|
|
private final Flags flags;
|
|
private final Downloader singleFileDownloader;
|
|
|
|
// Track all the on-going foreground downloads. This map is keyed by ForegroundDownloadKey.
|
|
private final DownloadFutureMap<ClientFileGroup> foregroundDownloadFutureMap;
|
|
|
|
// Track all on-going background download requests started by downloadFileGroup. This map is keyed
|
|
// by ForegroundDownloadKey so request can be kept in sync with foregroundDownloadFutureMap.
|
|
private final DownloadFutureMap<ClientFileGroup> downloadFutureMap;
|
|
|
|
// This executor will execute tasks sequentially.
|
|
private final Executor sequentialControlExecutor;
|
|
// ExecutionSequencer will execute a ListenableFuture and its Futures.transforms before taking the
|
|
// next task (<internal>). Most of MDD API should use
|
|
// ExecutionSequencer to guarantee Metadata synchronization. Currently only downloadFileGroup and
|
|
// handleTask APIs do not use ExecutionSequencer since their execution could take long time and
|
|
// using ExecutionSequencer would block other APIs.
|
|
private final PropagatedExecutionSequencer futureSerializer =
|
|
PropagatedExecutionSequencer.create();
|
|
private final Optional<DownloadProgressMonitor> downloadMonitorOptional;
|
|
private final Optional<Class<?>> foregroundDownloadServiceClassOptional;
|
|
private final AsyncFunction<DataFileGroupInternal, Boolean> customFileGroupValidator;
|
|
private final TimeSource timeSource;
|
|
|
|
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) {
|
|
this.context = context;
|
|
this.eventLogger = eventLogger;
|
|
this.fileGroupPopulatorList = fileGroupPopulatorList;
|
|
this.taskSchedulerOptional = taskSchedulerOptional;
|
|
this.sequentialControlExecutor = sequentialControlExecutor;
|
|
this.mobileDataDownloadManager = mobileDataDownloadManager;
|
|
this.fileStorage = fileStorage;
|
|
this.downloadMonitorOptional = downloadMonitorOptional;
|
|
this.foregroundDownloadServiceClassOptional = foregroundDownloadServiceClassOptional;
|
|
this.flags = flags;
|
|
this.singleFileDownloader = singleFileDownloader;
|
|
this.customFileGroupValidator =
|
|
createCustomFileGroupValidator(
|
|
customValidatorOptional,
|
|
mobileDataDownloadManager,
|
|
sequentialControlExecutor,
|
|
fileStorage);
|
|
this.downloadFutureMap = DownloadFutureMap.create(sequentialControlExecutor);
|
|
this.foregroundDownloadFutureMap =
|
|
DownloadFutureMap.create(
|
|
sequentialControlExecutor,
|
|
createCallbacksForForegroundService(context, foregroundDownloadServiceClassOptional));
|
|
this.timeSource = timeSource;
|
|
}
|
|
|
|
// Wraps the custom validator because the validation at a lower level of the stack where
|
|
// the ClientFileGroup is not available, yet ClientFileGroup is the client-facing API we'd
|
|
// like to expose.
|
|
private static AsyncFunction<DataFileGroupInternal, Boolean> createCustomFileGroupValidator(
|
|
Optional<CustomFileGroupValidator> validatorOptional,
|
|
MobileDataDownloadManager mobileDataDownloadManager,
|
|
Executor executor,
|
|
SynchronousFileStorage fileStorage) {
|
|
if (!validatorOptional.isPresent()) {
|
|
return unused -> immediateFuture(true);
|
|
}
|
|
|
|
return internalFileGroup ->
|
|
PropagatedFutures.transformAsync(
|
|
createClientFileGroup(
|
|
internalFileGroup,
|
|
/* account= */ null,
|
|
ClientFileGroup.Status.PENDING_CUSTOM_VALIDATION,
|
|
/* preserveZipDirectories= */ false,
|
|
/* verifyIsolatedStructure= */ true,
|
|
mobileDataDownloadManager,
|
|
executor,
|
|
fileStorage),
|
|
propagateAsyncFunction(
|
|
clientFileGroup -> validatorOptional.get().validateFileGroup(clientFileGroup)),
|
|
executor);
|
|
}
|
|
|
|
/**
|
|
* Functional interface used as callback for logging file group stats. Used to create file group
|
|
* stats from the result of the future.
|
|
*
|
|
* @see attachMddApiLogging
|
|
*/
|
|
private interface StatsFromApiResultCreator<T> {
|
|
DataDownloadFileGroupStats create(T result);
|
|
}
|
|
|
|
/**
|
|
* Functional interface used as callback when logging API result. Used to get the API result code
|
|
* from the result of the API future if it succeeds.
|
|
*
|
|
* <p>Note: The need for this is due to {@link addFileGroup} returning false instead of an
|
|
* exception if it fails. For other APIs with proper exception handling, it should suffice to
|
|
* immediately return the success code.
|
|
*
|
|
* <p>TODO(b/143572409): Remove once addGroupForDownload is updated to return void.
|
|
*
|
|
* @see attachMddApiLogging
|
|
*/
|
|
private interface ResultCodeFromApiResultGetter<T> {
|
|
int get(T result);
|
|
}
|
|
|
|
/**
|
|
* Helper function used to log mdd api stats. Adds FutureCallback to the {@code resultFuture}
|
|
* which is the result of mdd api call and logs in onSuccess and onFailure functions of callback.
|
|
*
|
|
* @param apiName Code of the api being logged.
|
|
* @param resultFuture Future result of the api call.
|
|
* @param startTimeNs start time in ns.
|
|
* @param defaultFileGroupStats Initial file group stats.
|
|
* @param statsCreator This functional interface is invoked from the onSuccess of FutureCallback
|
|
* with the result of the future. File group stats returned here is merged with the initial
|
|
* stats and logged.
|
|
*/
|
|
private <T> void attachMddApiLogging(
|
|
int apiName,
|
|
ListenableFuture<T> resultFuture,
|
|
long startTimeNs,
|
|
DataDownloadFileGroupStats defaultFileGroupStats,
|
|
StatsFromApiResultCreator<T> statsCreator,
|
|
ResultCodeFromApiResultGetter<T> resultCodeGetter) {
|
|
// Using listener instead of transform since we need to log even if the future fails.
|
|
// Note: Listener is being registered on directexecutor for accurate latency measurement.
|
|
resultFuture.addListener(
|
|
propagateRunnable(
|
|
() -> {
|
|
long latencyNs = timeSource.elapsedRealtimeNanos() - startTimeNs;
|
|
// Log the stats asynchronously.
|
|
// Note: To avoid adding latency to mdd api calls, log asynchronously.
|
|
var unused =
|
|
PropagatedFutures.submit(
|
|
() -> {
|
|
int resultCode;
|
|
T result = null;
|
|
DataDownloadFileGroupStats fileGroupStats = defaultFileGroupStats;
|
|
try {
|
|
result = Futures.getDone(resultFuture);
|
|
resultCode = resultCodeGetter.get(result);
|
|
} catch (Throwable t) {
|
|
resultCode = ExceptionToMddResultMapper.map(t);
|
|
}
|
|
|
|
// Merge stats created from result of api with the default stats.
|
|
if (result != null) {
|
|
fileGroupStats =
|
|
fileGroupStats.toBuilder()
|
|
.mergeFrom(statsCreator.create(result))
|
|
.build();
|
|
}
|
|
|
|
Void resultLog = null;
|
|
|
|
eventLogger.logMddLibApiResultLog(resultLog);
|
|
},
|
|
sequentialControlExecutor);
|
|
}),
|
|
directExecutor());
|
|
}
|
|
|
|
@Override
|
|
public ListenableFuture<Boolean> addFileGroup(AddFileGroupRequest addFileGroupRequest) {
|
|
long startTimeNs = timeSource.elapsedRealtimeNanos();
|
|
|
|
ListenableFuture<Boolean> resultFuture =
|
|
futureSerializer.submitAsync(
|
|
() -> addFileGroupHelper(addFileGroupRequest), sequentialControlExecutor);
|
|
|
|
DataDownloadFileGroupStats defaultFileGroupStats =
|
|
DataDownloadFileGroupStats.newBuilder()
|
|
.setFileGroupName(addFileGroupRequest.dataFileGroup().getGroupName())
|
|
.setBuildId(addFileGroupRequest.dataFileGroup().getBuildId())
|
|
.setVariantId(addFileGroupRequest.dataFileGroup().getVariantId())
|
|
.setHasAccount(addFileGroupRequest.accountOptional().isPresent())
|
|
.setFileGroupVersionNumber(
|
|
addFileGroupRequest.dataFileGroup().getFileGroupVersionNumber())
|
|
.setOwnerPackage(addFileGroupRequest.dataFileGroup().getOwnerPackage())
|
|
.setFileCount(addFileGroupRequest.dataFileGroup().getFileCount())
|
|
.build();
|
|
attachMddApiLogging(
|
|
0,
|
|
resultFuture,
|
|
startTimeNs,
|
|
defaultFileGroupStats,
|
|
/* statsCreator= */ unused -> defaultFileGroupStats,
|
|
/* resultCodeGetter= */ succeeded -> succeeded ? 0 : 0);
|
|
|
|
return resultFuture;
|
|
}
|
|
|
|
private ListenableFuture<Boolean> addFileGroupHelper(AddFileGroupRequest addFileGroupRequest) {
|
|
LogUtil.d(
|
|
"%s: Adding for download group = '%s', variant = '%s', buildId = '%d' and"
|
|
+ " associating it with account = '%s', variant = '%s'",
|
|
TAG,
|
|
addFileGroupRequest.dataFileGroup().getGroupName(),
|
|
addFileGroupRequest.dataFileGroup().getVariantId(),
|
|
addFileGroupRequest.dataFileGroup().getBuildId(),
|
|
String.valueOf(addFileGroupRequest.accountOptional().orNull()),
|
|
String.valueOf(addFileGroupRequest.variantIdOptional().orNull()));
|
|
|
|
DataFileGroup dataFileGroup = addFileGroupRequest.dataFileGroup();
|
|
|
|
// Ensure that the owner package is always set as the host app.
|
|
if (!dataFileGroup.hasOwnerPackage()) {
|
|
dataFileGroup = dataFileGroup.toBuilder().setOwnerPackage(context.getPackageName()).build();
|
|
} else if (!context.getPackageName().equals(dataFileGroup.getOwnerPackage())) {
|
|
LogUtil.e(
|
|
"%s: Added group = '%s' with wrong owner package: '%s' v.s. '%s' ",
|
|
TAG,
|
|
dataFileGroup.getGroupName(),
|
|
context.getPackageName(),
|
|
dataFileGroup.getOwnerPackage());
|
|
return immediateFuture(false);
|
|
}
|
|
|
|
GroupKey.Builder groupKeyBuilder =
|
|
GroupKey.newBuilder()
|
|
.setGroupName(dataFileGroup.getGroupName())
|
|
.setOwnerPackage(dataFileGroup.getOwnerPackage());
|
|
|
|
if (addFileGroupRequest.accountOptional().isPresent()) {
|
|
groupKeyBuilder.setAccount(
|
|
AccountUtil.serialize(addFileGroupRequest.accountOptional().get()));
|
|
}
|
|
|
|
if (addFileGroupRequest.variantIdOptional().isPresent()) {
|
|
groupKeyBuilder.setVariantId(addFileGroupRequest.variantIdOptional().get());
|
|
}
|
|
|
|
try {
|
|
DataFileGroupInternal dataFileGroupInternal = ProtoConversionUtil.convert(dataFileGroup);
|
|
return mobileDataDownloadManager.addGroupForDownloadInternal(
|
|
groupKeyBuilder.build(), dataFileGroupInternal, customFileGroupValidator);
|
|
} catch (InvalidProtocolBufferException e) {
|
|
// TODO(b/118137672): Consider rethrow exception instead of returning false.
|
|
LogUtil.e(e, "%s: Unable to convert from DataFileGroup to DataFileGroupInternal.", TAG);
|
|
return immediateFuture(false);
|
|
}
|
|
}
|
|
|
|
// TODO: Change to return ListenableFuture<Void>.
|
|
@Override
|
|
public ListenableFuture<Boolean> removeFileGroup(RemoveFileGroupRequest removeFileGroupRequest) {
|
|
return futureSerializer.submitAsync(
|
|
() -> {
|
|
GroupKey.Builder groupKeyBuilder =
|
|
GroupKey.newBuilder()
|
|
.setGroupName(removeFileGroupRequest.groupName())
|
|
.setOwnerPackage(context.getPackageName());
|
|
if (removeFileGroupRequest.accountOptional().isPresent()) {
|
|
groupKeyBuilder.setAccount(
|
|
AccountUtil.serialize(removeFileGroupRequest.accountOptional().get()));
|
|
}
|
|
if (removeFileGroupRequest.variantIdOptional().isPresent()) {
|
|
groupKeyBuilder.setVariantId(removeFileGroupRequest.variantIdOptional().get());
|
|
}
|
|
|
|
GroupKey groupKey = groupKeyBuilder.build();
|
|
return PropagatedFutures.transform(
|
|
mobileDataDownloadManager.removeFileGroup(
|
|
groupKey, removeFileGroupRequest.pendingOnly()),
|
|
voidArg -> true,
|
|
sequentialControlExecutor);
|
|
},
|
|
sequentialControlExecutor);
|
|
}
|
|
|
|
@Override
|
|
public ListenableFuture<RemoveFileGroupsByFilterResponse> removeFileGroupsByFilter(
|
|
RemoveFileGroupsByFilterRequest removeFileGroupsByFilterRequest) {
|
|
return futureSerializer.submitAsync(
|
|
() ->
|
|
PropagatedFluentFuture.from(mobileDataDownloadManager.getAllFreshGroups())
|
|
.transformAsync(
|
|
allFreshGroupKeyAndGroups -> {
|
|
ImmutableSet.Builder<GroupKey> groupKeysToRemoveBuilder =
|
|
ImmutableSet.builder();
|
|
for (GroupKeyAndGroup groupKeyAndGroup : allFreshGroupKeyAndGroups) {
|
|
if (applyRemoveFileGroupsFilter(
|
|
removeFileGroupsByFilterRequest, groupKeyAndGroup)) {
|
|
// Remove downloaded status so pending/downloaded versions of the same
|
|
// group are treated as one.
|
|
groupKeysToRemoveBuilder.add(
|
|
groupKeyAndGroup.groupKey().toBuilder().clearDownloaded().build());
|
|
}
|
|
}
|
|
ImmutableSet<GroupKey> groupKeysToRemove = groupKeysToRemoveBuilder.build();
|
|
if (groupKeysToRemove.isEmpty()) {
|
|
return immediateFuture(
|
|
RemoveFileGroupsByFilterResponse.newBuilder()
|
|
.setRemovedFileGroupsCount(0)
|
|
.build());
|
|
}
|
|
return PropagatedFutures.transform(
|
|
mobileDataDownloadManager.removeFileGroups(groupKeysToRemove.asList()),
|
|
unused ->
|
|
RemoveFileGroupsByFilterResponse.newBuilder()
|
|
.setRemovedFileGroupsCount(groupKeysToRemove.size())
|
|
.build(),
|
|
sequentialControlExecutor);
|
|
},
|
|
sequentialControlExecutor),
|
|
sequentialControlExecutor);
|
|
}
|
|
|
|
// Perform filtering using options from RemoveFileGroupsByFilterRequest
|
|
private static boolean applyRemoveFileGroupsFilter(
|
|
RemoveFileGroupsByFilterRequest removeFileGroupsByFilterRequest,
|
|
GroupKeyAndGroup groupKeyAndGroup) {
|
|
// If request filters by account, ensure account is present and is equal
|
|
Optional<Account> accountOptional = removeFileGroupsByFilterRequest.accountOptional();
|
|
if (!accountOptional.isPresent() && groupKeyAndGroup.groupKey().hasAccount()) {
|
|
// Account must explicitly be provided in order to remove account associated file groups.
|
|
return false;
|
|
}
|
|
if (accountOptional.isPresent()
|
|
&& !AccountUtil.serialize(accountOptional.get())
|
|
.equals(groupKeyAndGroup.groupKey().getAccount())) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Helper function to create {@link DataDownloadFileGroupStats} object from {@link
|
|
* GetFileGroupRequest} for getFileGroup() logging.
|
|
*
|
|
* <p>Used when the matching file group is not found or a failure occurred.
|
|
* file_group_version_number and build_id are set to -1 by default.
|
|
*/
|
|
private DataDownloadFileGroupStats createFileGroupStatsFromGetFileGroupRequest(
|
|
GetFileGroupRequest getFileGroupRequest) {
|
|
DataDownloadFileGroupStats.Builder fileGroupStatsBuilder =
|
|
DataDownloadFileGroupStats.newBuilder();
|
|
fileGroupStatsBuilder.setFileGroupName(getFileGroupRequest.groupName());
|
|
if (getFileGroupRequest.variantIdOptional().isPresent()) {
|
|
fileGroupStatsBuilder.setVariantId(getFileGroupRequest.variantIdOptional().get());
|
|
}
|
|
if (getFileGroupRequest.accountOptional().isPresent()) {
|
|
fileGroupStatsBuilder.setHasAccount(true);
|
|
} else {
|
|
fileGroupStatsBuilder.setHasAccount(false);
|
|
}
|
|
|
|
fileGroupStatsBuilder.setFileGroupVersionNumber(
|
|
MddConstants.FILE_GROUP_NOT_FOUND_FILE_GROUP_VERSION_NUMBER);
|
|
fileGroupStatsBuilder.setBuildId(MddConstants.FILE_GROUP_NOT_FOUND_BUILD_ID);
|
|
|
|
return fileGroupStatsBuilder.build();
|
|
}
|
|
|
|
// TODO: Futures.immediateFuture(null) uses a different annotation for Nullable.
|
|
@SuppressWarnings("nullness")
|
|
@Override
|
|
public ListenableFuture<ClientFileGroup> getFileGroup(GetFileGroupRequest getFileGroupRequest) {
|
|
long startTimeNs = timeSource.elapsedRealtimeNanos();
|
|
|
|
ListenableFuture<ClientFileGroup> resultFuture =
|
|
futureSerializer.submitAsync(
|
|
() -> {
|
|
GroupKey groupKey =
|
|
createGroupKey(
|
|
getFileGroupRequest.groupName(),
|
|
getFileGroupRequest.accountOptional(),
|
|
getFileGroupRequest.variantIdOptional());
|
|
return PropagatedFutures.transformAsync(
|
|
mobileDataDownloadManager.getFileGroup(groupKey, /* downloaded= */ true),
|
|
dataFileGroup ->
|
|
createClientFileGroupAndLogQueryStats(
|
|
groupKey,
|
|
dataFileGroup,
|
|
/* downloaded= */ true,
|
|
getFileGroupRequest.preserveZipDirectories(),
|
|
getFileGroupRequest.verifyIsolatedStructure()),
|
|
sequentialControlExecutor);
|
|
},
|
|
sequentialControlExecutor);
|
|
|
|
attachMddApiLogging(
|
|
0,
|
|
resultFuture,
|
|
startTimeNs,
|
|
createFileGroupStatsFromGetFileGroupRequest(getFileGroupRequest),
|
|
/* statsCreator= */ result -> createFileGroupDetails(result),
|
|
/* resultCodeGetter= */ unused -> 0);
|
|
return resultFuture;
|
|
}
|
|
|
|
@SuppressWarnings("nullness")
|
|
@Override
|
|
public ListenableFuture<DataFileGroup> readDataFileGroup(
|
|
ReadDataFileGroupRequest readDataFileGroupRequest) {
|
|
return futureSerializer.submitAsync(
|
|
() -> {
|
|
GroupKey groupKey =
|
|
createGroupKey(
|
|
readDataFileGroupRequest.groupName(),
|
|
readDataFileGroupRequest.accountOptional(),
|
|
readDataFileGroupRequest.variantIdOptional());
|
|
return PropagatedFutures.transformAsync(
|
|
mobileDataDownloadManager.getFileGroup(groupKey, /* downloaded= */ true),
|
|
internalFileGroup -> immediateFuture(ProtoConversionUtil.reverse(internalFileGroup)),
|
|
sequentialControlExecutor);
|
|
},
|
|
sequentialControlExecutor);
|
|
}
|
|
|
|
private GroupKey createGroupKey(
|
|
String groupName, Optional<Account> accountOptional, Optional<String> variantOptional) {
|
|
GroupKey.Builder groupKeyBuilder =
|
|
GroupKey.newBuilder().setGroupName(groupName).setOwnerPackage(context.getPackageName());
|
|
|
|
if (accountOptional.isPresent()) {
|
|
groupKeyBuilder.setAccount(AccountUtil.serialize(accountOptional.get()));
|
|
}
|
|
|
|
if (variantOptional.isPresent()) {
|
|
groupKeyBuilder.setVariantId(variantOptional.get());
|
|
}
|
|
|
|
return groupKeyBuilder.build();
|
|
}
|
|
|
|
private ListenableFuture<ClientFileGroup> createClientFileGroupAndLogQueryStats(
|
|
GroupKey groupKey,
|
|
@Nullable DataFileGroupInternal dataFileGroup,
|
|
boolean downloaded,
|
|
boolean preserveZipDirectories,
|
|
boolean verifyIsolatedStructure) {
|
|
return PropagatedFutures.transform(
|
|
createClientFileGroup(
|
|
dataFileGroup,
|
|
groupKey.hasAccount() ? groupKey.getAccount() : null,
|
|
downloaded ? ClientFileGroup.Status.DOWNLOADED : ClientFileGroup.Status.PENDING,
|
|
preserveZipDirectories,
|
|
verifyIsolatedStructure,
|
|
mobileDataDownloadManager,
|
|
sequentialControlExecutor,
|
|
fileStorage),
|
|
clientFileGroup -> {
|
|
if (clientFileGroup != null) {
|
|
eventLogger.logMddQueryStats(createFileGroupDetails(clientFileGroup));
|
|
}
|
|
return clientFileGroup;
|
|
},
|
|
sequentialControlExecutor);
|
|
}
|
|
|
|
@SuppressWarnings("nullness")
|
|
private static ListenableFuture<ClientFileGroup> createClientFileGroup(
|
|
@Nullable DataFileGroupInternal dataFileGroup,
|
|
@Nullable String account,
|
|
ClientFileGroup.Status status,
|
|
boolean preserveZipDirectories,
|
|
boolean verifyIsolatedStructure,
|
|
MobileDataDownloadManager manager,
|
|
Executor executor,
|
|
SynchronousFileStorage fileStorage) {
|
|
if (dataFileGroup == null) {
|
|
return immediateFuture(null);
|
|
}
|
|
ClientFileGroup.Builder clientFileGroupBuilder =
|
|
ClientFileGroup.newBuilder()
|
|
.setGroupName(dataFileGroup.getGroupName())
|
|
.setOwnerPackage(dataFileGroup.getOwnerPackage())
|
|
.setVersionNumber(dataFileGroup.getFileGroupVersionNumber())
|
|
// .setCustomProperty(dataFileGroup.getCustomProperty())
|
|
.setBuildId(dataFileGroup.getBuildId())
|
|
.setVariantId(dataFileGroup.getVariantId())
|
|
.setStatus(status)
|
|
.addAllLocale(dataFileGroup.getLocaleList());
|
|
|
|
if (account != null) {
|
|
clientFileGroupBuilder.setAccount(account);
|
|
}
|
|
|
|
if (dataFileGroup.hasCustomMetadata()) {
|
|
clientFileGroupBuilder.setCustomMetadata(dataFileGroup.getCustomMetadata());
|
|
}
|
|
|
|
List<DataFile> dataFiles = dataFileGroup.getFileList();
|
|
ListenableFuture<Void> addOnDeviceUrisFuture = immediateVoidFuture();
|
|
if (status == ClientFileGroup.Status.DOWNLOADED
|
|
|| status == ClientFileGroup.Status.PENDING_CUSTOM_VALIDATION) {
|
|
addOnDeviceUrisFuture =
|
|
PropagatedFluentFuture.from(
|
|
manager.getDataFileUris(dataFileGroup, verifyIsolatedStructure))
|
|
.transformAsync(
|
|
dataFileUriMap -> {
|
|
for (DataFile dataFile : dataFiles) {
|
|
if (!dataFileUriMap.containsKey(dataFile)) {
|
|
return immediateFailedFuture(
|
|
DownloadException.builder()
|
|
.setDownloadResultCode(
|
|
DownloadResultCode.DOWNLOADED_FILE_NOT_FOUND_ERROR)
|
|
.setMessage("getDataFileUris() resolved to null")
|
|
.build());
|
|
}
|
|
Uri uri = dataFileUriMap.get(dataFile);
|
|
|
|
try {
|
|
if (!preserveZipDirectories && fileStorage.isDirectory(uri)) {
|
|
String rootPath = uri.getPath();
|
|
if (rootPath != null) {
|
|
clientFileGroupBuilder.addAllFile(
|
|
listAllClientFilesOfDirectory(fileStorage, uri, rootPath));
|
|
}
|
|
} else {
|
|
clientFileGroupBuilder.addFile(
|
|
createClientFile(
|
|
dataFile.getFileId(),
|
|
dataFile.getByteSize(),
|
|
dataFile.getDownloadedFileByteSize(),
|
|
uri.toString(),
|
|
dataFile.hasCustomMetadata()
|
|
? dataFile.getCustomMetadata()
|
|
: null));
|
|
}
|
|
} catch (IOException e) {
|
|
LogUtil.e(e, "Failed to list files under directory:" + uri);
|
|
}
|
|
}
|
|
return immediateVoidFuture();
|
|
},
|
|
executor);
|
|
} else {
|
|
for (DataFile dataFile : dataFiles) {
|
|
clientFileGroupBuilder.addFile(
|
|
createClientFile(
|
|
dataFile.getFileId(),
|
|
dataFile.getByteSize(),
|
|
dataFile.getDownloadedFileByteSize(),
|
|
/* uri= */ null,
|
|
dataFile.hasCustomMetadata() ? dataFile.getCustomMetadata() : null));
|
|
}
|
|
}
|
|
|
|
return PropagatedFluentFuture.from(addOnDeviceUrisFuture)
|
|
.transform(unused -> clientFileGroupBuilder.build(), executor)
|
|
.catching(DownloadException.class, exn -> null, executor);
|
|
}
|
|
|
|
private static ClientFile createClientFile(
|
|
String fileId,
|
|
int byteSize,
|
|
int downloadByteSize,
|
|
@Nullable String uri,
|
|
@Nullable Any customMetadata) {
|
|
ClientFile.Builder clientFileBuilder =
|
|
ClientFile.newBuilder().setFileId(fileId).setFullSizeInBytes(byteSize);
|
|
if (downloadByteSize > 0) {
|
|
// Files with downloaded transforms like compress and zip could have different downloaded
|
|
// file size than the final file size on disk. Return the downloaded file size for client to
|
|
// track and calculate the download progress.
|
|
clientFileBuilder.setDownloadSizeInBytes(downloadByteSize);
|
|
}
|
|
if (uri != null) {
|
|
clientFileBuilder.setFileUri(uri);
|
|
}
|
|
if (customMetadata != null) {
|
|
clientFileBuilder.setCustomMetadata(customMetadata);
|
|
}
|
|
return clientFileBuilder.build();
|
|
}
|
|
|
|
private static List<ClientFile> listAllClientFilesOfDirectory(
|
|
SynchronousFileStorage fileStorage, Uri dirUri, String rootDir) throws IOException {
|
|
List<ClientFile> clientFileList = new ArrayList<>();
|
|
for (Uri childUri : fileStorage.children(dirUri)) {
|
|
if (fileStorage.isDirectory(childUri)) {
|
|
clientFileList.addAll(listAllClientFilesOfDirectory(fileStorage, childUri, rootDir));
|
|
} else {
|
|
String childPath = childUri.getPath();
|
|
if (childPath != null) {
|
|
ClientFile clientFile =
|
|
ClientFile.newBuilder()
|
|
.setFileId(childPath.replaceFirst(rootDir, ""))
|
|
.setFullSizeInBytes((int) fileStorage.fileSize(childUri))
|
|
.setFileUri(childUri.toString())
|
|
.build();
|
|
clientFileList.add(clientFile);
|
|
}
|
|
}
|
|
}
|
|
return clientFileList;
|
|
}
|
|
|
|
@Override
|
|
public ListenableFuture<ImmutableList<ClientFileGroup>> getFileGroupsByFilter(
|
|
GetFileGroupsByFilterRequest getFileGroupsByFilterRequest) {
|
|
return futureSerializer.submitAsync(
|
|
() ->
|
|
PropagatedFutures.transformAsync(
|
|
mobileDataDownloadManager.getAllFreshGroups(),
|
|
allFreshGroupKeyAndGroups -> {
|
|
ListenableFuture<ImmutableList.Builder<ClientFileGroup>>
|
|
clientFileGroupsBuilderFuture =
|
|
immediateFuture(ImmutableList.<ClientFileGroup>builder());
|
|
for (GroupKeyAndGroup groupKeyAndGroup : allFreshGroupKeyAndGroups) {
|
|
clientFileGroupsBuilderFuture =
|
|
PropagatedFutures.transformAsync(
|
|
clientFileGroupsBuilderFuture,
|
|
clientFileGroupsBuilder -> {
|
|
GroupKey groupKey = groupKeyAndGroup.groupKey();
|
|
DataFileGroupInternal dataFileGroup =
|
|
groupKeyAndGroup.dataFileGroup();
|
|
if (applyFilter(
|
|
getFileGroupsByFilterRequest, groupKey, dataFileGroup)) {
|
|
return PropagatedFutures.transform(
|
|
createClientFileGroupAndLogQueryStats(
|
|
groupKey,
|
|
dataFileGroup,
|
|
groupKey.getDownloaded(),
|
|
getFileGroupsByFilterRequest.preserveZipDirectories(),
|
|
getFileGroupsByFilterRequest.verifyIsolatedStructure()),
|
|
clientFileGroup -> {
|
|
if (clientFileGroup != null) {
|
|
clientFileGroupsBuilder.add(clientFileGroup);
|
|
}
|
|
return clientFileGroupsBuilder;
|
|
},
|
|
sequentialControlExecutor);
|
|
}
|
|
return immediateFuture(clientFileGroupsBuilder);
|
|
},
|
|
sequentialControlExecutor);
|
|
}
|
|
|
|
return PropagatedFutures.transform(
|
|
clientFileGroupsBuilderFuture,
|
|
ImmutableList.Builder::build,
|
|
sequentialControlExecutor);
|
|
},
|
|
sequentialControlExecutor),
|
|
sequentialControlExecutor);
|
|
}
|
|
|
|
// Perform filtering using options from GetFileGroupsByFilterRequest
|
|
private static boolean applyFilter(
|
|
GetFileGroupsByFilterRequest getFileGroupsByFilterRequest,
|
|
GroupKey groupKey,
|
|
DataFileGroupInternal fileGroup) {
|
|
if (getFileGroupsByFilterRequest.includeAllGroups()) {
|
|
return true;
|
|
}
|
|
|
|
// If request filters by group name, ensure name is equal
|
|
Optional<String> groupNameOptional = getFileGroupsByFilterRequest.groupNameOptional();
|
|
if (groupNameOptional.isPresent()
|
|
&& !TextUtils.equals(groupNameOptional.get(), groupKey.getGroupName())) {
|
|
return false;
|
|
}
|
|
|
|
// When the caller requests account independent groups only.
|
|
if (getFileGroupsByFilterRequest.groupWithNoAccountOnly()) {
|
|
return !groupKey.hasAccount();
|
|
}
|
|
|
|
// When the caller requests account dependent groups as well.
|
|
if (getFileGroupsByFilterRequest.accountOptional().isPresent()
|
|
&& !AccountUtil.serialize(getFileGroupsByFilterRequest.accountOptional().get())
|
|
.equals(groupKey.getAccount())) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Creates {@link DataDownloadFileGroupStats} from {@link ClientFileGroup} for remote logging
|
|
* purposes.
|
|
*/
|
|
private static DataDownloadFileGroupStats createFileGroupDetails(
|
|
ClientFileGroup clientFileGroup) {
|
|
return DataDownloadFileGroupStats.newBuilder()
|
|
.setFileGroupName(clientFileGroup.getGroupName())
|
|
.setOwnerPackage(clientFileGroup.getOwnerPackage())
|
|
.setFileGroupVersionNumber(clientFileGroup.getVersionNumber())
|
|
.setFileCount(clientFileGroup.getFileCount())
|
|
.setVariantId(clientFileGroup.getVariantId())
|
|
.setBuildId(clientFileGroup.getBuildId())
|
|
.build();
|
|
}
|
|
|
|
@Override
|
|
public ListenableFuture<Void> importFiles(ImportFilesRequest importFilesRequest) {
|
|
GroupKey.Builder groupKeyBuilder =
|
|
GroupKey.newBuilder()
|
|
.setGroupName(importFilesRequest.groupName())
|
|
.setOwnerPackage(context.getPackageName());
|
|
|
|
if (importFilesRequest.accountOptional().isPresent()) {
|
|
groupKeyBuilder.setAccount(AccountUtil.serialize(importFilesRequest.accountOptional().get()));
|
|
}
|
|
|
|
GroupKey groupKey = groupKeyBuilder.build();
|
|
|
|
ImmutableList.Builder<DataFile> updatedDataFileListBuilder =
|
|
ImmutableList.builderWithExpectedSize(importFilesRequest.updatedDataFileList().size());
|
|
for (DownloadConfigProto.DataFile dataFile : importFilesRequest.updatedDataFileList()) {
|
|
updatedDataFileListBuilder.add(ProtoConversionUtil.convertDataFile(dataFile));
|
|
}
|
|
|
|
return futureSerializer.submitAsync(
|
|
() ->
|
|
mobileDataDownloadManager.importFiles(
|
|
groupKey,
|
|
importFilesRequest.buildId(),
|
|
importFilesRequest.variantId(),
|
|
updatedDataFileListBuilder.build(),
|
|
importFilesRequest.inlineFileMap(),
|
|
importFilesRequest.customPropertyOptional(),
|
|
customFileGroupValidator),
|
|
sequentialControlExecutor);
|
|
}
|
|
|
|
@Override
|
|
public ListenableFuture<Void> downloadFile(SingleFileDownloadRequest singleFileDownloadRequest) {
|
|
return singleFileDownloader.download(
|
|
MddLiteConversionUtil.convertToDownloadRequest(singleFileDownloadRequest));
|
|
}
|
|
|
|
@Override
|
|
public ListenableFuture<ClientFileGroup> downloadFileGroup(
|
|
DownloadFileGroupRequest downloadFileGroupRequest) {
|
|
// Submit the call to sequentialControlExecutor, but don't use futureSerializer. This will
|
|
// ensure that multiple calls are enqueued to the executor in a FIFO order, but these calls
|
|
// won't block each other when the download is in progress.
|
|
return PropagatedFutures.submitAsync(
|
|
() ->
|
|
PropagatedFutures.transformAsync(
|
|
// Check if requested file group has already been downloaded
|
|
getDownloadGroupState(downloadFileGroupRequest),
|
|
downloadGroupState -> {
|
|
switch (downloadGroupState.getKind()) {
|
|
case IN_PROGRESS_FUTURE:
|
|
// If the file group download is in progress, return that future immediately
|
|
return downloadGroupState.inProgressFuture();
|
|
case DOWNLOADED_GROUP:
|
|
// If the file group is already downloaded, return that immediately.
|
|
return immediateFuture(downloadGroupState.downloadedGroup());
|
|
case PENDING_GROUP:
|
|
return downloadPendingFileGroup(downloadFileGroupRequest);
|
|
}
|
|
throw new AssertionError(
|
|
String.format(
|
|
"received unsupported DownloadGroupState kind %s",
|
|
downloadGroupState.getKind()));
|
|
},
|
|
sequentialControlExecutor),
|
|
sequentialControlExecutor);
|
|
}
|
|
|
|
/** Helper method to download a group after it's determined to be pending. */
|
|
private ListenableFuture<ClientFileGroup> downloadPendingFileGroup(
|
|
DownloadFileGroupRequest downloadFileGroupRequest) {
|
|
String groupName = downloadFileGroupRequest.groupName();
|
|
GroupKey.Builder groupKeyBuilder =
|
|
GroupKey.newBuilder().setGroupName(groupName).setOwnerPackage(context.getPackageName());
|
|
|
|
if (downloadFileGroupRequest.accountOptional().isPresent()) {
|
|
groupKeyBuilder.setAccount(
|
|
AccountUtil.serialize(downloadFileGroupRequest.accountOptional().get()));
|
|
}
|
|
if (downloadFileGroupRequest.variantIdOptional().isPresent()) {
|
|
groupKeyBuilder.setVariantId(downloadFileGroupRequest.variantIdOptional().get());
|
|
}
|
|
|
|
GroupKey groupKey = groupKeyBuilder.build();
|
|
|
|
if (downloadFileGroupRequest.listenerOptional().isPresent()) {
|
|
if (downloadMonitorOptional.isPresent()) {
|
|
downloadMonitorOptional
|
|
.get()
|
|
.addDownloadListener(groupName, downloadFileGroupRequest.listenerOptional().get());
|
|
} else {
|
|
return immediateFailedFuture(
|
|
DownloadException.builder()
|
|
.setDownloadResultCode(DownloadResultCode.DOWNLOAD_MONITOR_NOT_PROVIDED_ERROR)
|
|
.setMessage(
|
|
"downloadFileGroup: DownloadListener is present but Download Monitor"
|
|
+ " is not provided!")
|
|
.build());
|
|
}
|
|
}
|
|
|
|
Optional<DownloadConditions> downloadConditions;
|
|
try {
|
|
downloadConditions =
|
|
downloadFileGroupRequest.downloadConditionsOptional().isPresent()
|
|
? Optional.of(
|
|
ProtoConversionUtil.convert(
|
|
downloadFileGroupRequest.downloadConditionsOptional().get()))
|
|
: Optional.absent();
|
|
} catch (InvalidProtocolBufferException e) {
|
|
return immediateFailedFuture(e);
|
|
}
|
|
|
|
// Get the key used for the download future map
|
|
ForegroundDownloadKey downloadKey =
|
|
ForegroundDownloadKey.ofFileGroup(
|
|
downloadFileGroupRequest.groupName(),
|
|
downloadFileGroupRequest.accountOptional(),
|
|
downloadFileGroupRequest.variantIdOptional());
|
|
|
|
// Create a ListenableFutureTask to delay starting the downloadFuture until we can add the
|
|
// future to our map.
|
|
ListenableFutureTask<Void> startTask = ListenableFutureTask.create(() -> null);
|
|
ListenableFuture<ClientFileGroup> downloadFuture =
|
|
PropagatedFluentFuture.from(startTask)
|
|
.transformAsync(
|
|
unused ->
|
|
mobileDataDownloadManager.downloadFileGroup(
|
|
groupKey, downloadConditions, customFileGroupValidator),
|
|
sequentialControlExecutor)
|
|
.transformAsync(
|
|
dataFileGroup ->
|
|
createClientFileGroup(
|
|
dataFileGroup,
|
|
downloadFileGroupRequest.accountOptional().isPresent()
|
|
? AccountUtil.serialize(
|
|
downloadFileGroupRequest.accountOptional().get())
|
|
: null,
|
|
ClientFileGroup.Status.DOWNLOADED,
|
|
downloadFileGroupRequest.preserveZipDirectories(),
|
|
downloadFileGroupRequest.verifyIsolatedStructure(),
|
|
mobileDataDownloadManager,
|
|
sequentialControlExecutor,
|
|
fileStorage),
|
|
sequentialControlExecutor)
|
|
.transform(Preconditions::checkNotNull, sequentialControlExecutor);
|
|
|
|
// Get a handle on the download task so we can get the CFG during transforms
|
|
PropagatedFluentFuture<ClientFileGroup> downloadTaskFuture =
|
|
PropagatedFluentFuture.from(downloadFutureMap.add(downloadKey.toString(), downloadFuture))
|
|
.transformAsync(
|
|
unused -> {
|
|
// Now that the download future is added, start the task and return the future
|
|
startTask.run();
|
|
return downloadFuture;
|
|
},
|
|
sequentialControlExecutor);
|
|
|
|
ListenableFuture<ClientFileGroup> transformFuture =
|
|
downloadTaskFuture
|
|
.transformAsync(
|
|
unused -> downloadFutureMap.remove(downloadKey.toString()),
|
|
sequentialControlExecutor)
|
|
.transformAsync(
|
|
unused -> {
|
|
ClientFileGroup clientFileGroup = getDone(downloadTaskFuture);
|
|
|
|
if (downloadFileGroupRequest.listenerOptional().isPresent()) {
|
|
try {
|
|
downloadFileGroupRequest.listenerOptional().get().onComplete(clientFileGroup);
|
|
} catch (Exception e) {
|
|
LogUtil.w(
|
|
e,
|
|
"%s: Listener onComplete failed for group %s",
|
|
TAG,
|
|
clientFileGroup.getGroupName());
|
|
}
|
|
if (downloadMonitorOptional.isPresent()) {
|
|
downloadMonitorOptional.get().removeDownloadListener(groupName);
|
|
}
|
|
}
|
|
return immediateFuture(clientFileGroup);
|
|
},
|
|
sequentialControlExecutor);
|
|
|
|
PropagatedFutures.addCallback(
|
|
transformFuture,
|
|
new FutureCallback<ClientFileGroup>() {
|
|
@Override
|
|
public void onSuccess(ClientFileGroup result) {}
|
|
|
|
@Override
|
|
public void onFailure(Throwable t) {
|
|
if (downloadFileGroupRequest.listenerOptional().isPresent()) {
|
|
downloadFileGroupRequest.listenerOptional().get().onFailure(t);
|
|
|
|
if (downloadMonitorOptional.isPresent()) {
|
|
downloadMonitorOptional.get().removeDownloadListener(groupName);
|
|
}
|
|
}
|
|
|
|
// Remove future from map
|
|
ListenableFuture<Void> unused = downloadFutureMap.remove(downloadKey.toString());
|
|
}
|
|
},
|
|
sequentialControlExecutor);
|
|
|
|
return transformFuture;
|
|
}
|
|
|
|
@Override
|
|
public ListenableFuture<Void> downloadFileWithForegroundService(
|
|
SingleFileDownloadRequest singleFileDownloadRequest) {
|
|
return singleFileDownloader.downloadWithForegroundService(
|
|
MddLiteConversionUtil.convertToDownloadRequest(singleFileDownloadRequest));
|
|
}
|
|
|
|
@Override
|
|
public ListenableFuture<ClientFileGroup> downloadFileGroupWithForegroundService(
|
|
DownloadFileGroupRequest downloadFileGroupRequest) {
|
|
LogUtil.d("%s: downloadFileGroupWithForegroundService start.", TAG);
|
|
if (!foregroundDownloadServiceClassOptional.isPresent()) {
|
|
return immediateFailedFuture(
|
|
new IllegalArgumentException(
|
|
"downloadFileGroupWithForegroundService: ForegroundDownloadService is not"
|
|
+ " provided!"));
|
|
}
|
|
|
|
if (!downloadMonitorOptional.isPresent()) {
|
|
return immediateFailedFuture(
|
|
DownloadException.builder()
|
|
.setDownloadResultCode(DownloadResultCode.DOWNLOAD_MONITOR_NOT_PROVIDED_ERROR)
|
|
.setMessage(
|
|
"downloadFileGroupWithForegroundService: Download Monitor is not provided!")
|
|
.build());
|
|
}
|
|
|
|
// Submit the call to sequentialControlExecutor, but don't use futureSerializer. This will
|
|
// ensure that multiple calls are enqueued to the executor in a FIFO order, but these calls
|
|
// won't block each other when the download is in progress.
|
|
return PropagatedFutures.submitAsync(
|
|
() ->
|
|
PropagatedFutures.transformAsync(
|
|
// Check if requested file group has already been downloaded
|
|
getDownloadGroupState(downloadFileGroupRequest),
|
|
downloadGroupState -> {
|
|
switch (downloadGroupState.getKind()) {
|
|
case IN_PROGRESS_FUTURE:
|
|
// If the file group download is in progress, return that future immediately
|
|
return downloadGroupState.inProgressFuture();
|
|
case DOWNLOADED_GROUP:
|
|
// If the file group is already downloaded, return that immediately
|
|
return immediateFuture(downloadGroupState.downloadedGroup());
|
|
case PENDING_GROUP:
|
|
return downloadPendingFileGroupWithForegroundService(
|
|
downloadFileGroupRequest, downloadGroupState.pendingGroup());
|
|
}
|
|
throw new AssertionError(
|
|
String.format(
|
|
"received unsupported DownloadGroupState kind %s",
|
|
downloadGroupState.getKind()));
|
|
},
|
|
sequentialControlExecutor),
|
|
sequentialControlExecutor);
|
|
}
|
|
|
|
/**
|
|
* Helper method to download a file group in the foreground after it has been confirmed to be
|
|
* pending.
|
|
*/
|
|
private ListenableFuture<ClientFileGroup> downloadPendingFileGroupWithForegroundService(
|
|
DownloadFileGroupRequest downloadFileGroupRequest, DataFileGroupInternal pendingGroup) {
|
|
// It's OK to recreate the NotificationChannel since it can also be used to restore a
|
|
// deleted channel and to update an existing channel's name, description, group, and/or
|
|
// importance.
|
|
NotificationUtil.createNotificationChannel(context);
|
|
|
|
String groupName = downloadFileGroupRequest.groupName();
|
|
GroupKey.Builder groupKeyBuilder =
|
|
GroupKey.newBuilder().setGroupName(groupName).setOwnerPackage(context.getPackageName());
|
|
|
|
if (downloadFileGroupRequest.accountOptional().isPresent()) {
|
|
groupKeyBuilder.setAccount(
|
|
AccountUtil.serialize(downloadFileGroupRequest.accountOptional().get()));
|
|
}
|
|
if (downloadFileGroupRequest.variantIdOptional().isPresent()) {
|
|
groupKeyBuilder.setVariantId(downloadFileGroupRequest.variantIdOptional().get());
|
|
}
|
|
|
|
GroupKey groupKey = groupKeyBuilder.build();
|
|
ForegroundDownloadKey foregroundDownloadKey =
|
|
ForegroundDownloadKey.ofFileGroup(
|
|
groupName,
|
|
downloadFileGroupRequest.accountOptional(),
|
|
downloadFileGroupRequest.variantIdOptional());
|
|
|
|
DownloadListener downloadListenerWithNotification =
|
|
createDownloadListenerWithNotification(downloadFileGroupRequest, pendingGroup);
|
|
// The downloadMonitor will trigger the DownloadListener.
|
|
downloadMonitorOptional
|
|
.get()
|
|
.addDownloadListener(
|
|
downloadFileGroupRequest.groupName(), downloadListenerWithNotification);
|
|
|
|
Optional<DownloadConditions> downloadConditions;
|
|
try {
|
|
downloadConditions =
|
|
downloadFileGroupRequest.downloadConditionsOptional().isPresent()
|
|
? Optional.of(
|
|
ProtoConversionUtil.convert(
|
|
downloadFileGroupRequest.downloadConditionsOptional().get()))
|
|
: Optional.absent();
|
|
} catch (InvalidProtocolBufferException e) {
|
|
return immediateFailedFuture(e);
|
|
}
|
|
|
|
// Create a ListenableFutureTask to delay starting the downloadFuture until we can add the
|
|
// future to our map.
|
|
ListenableFutureTask<Void> startTask = ListenableFutureTask.create(() -> null);
|
|
PropagatedFluentFuture<ClientFileGroup> downloadFileGroupFuture =
|
|
PropagatedFluentFuture.from(startTask)
|
|
.transformAsync(
|
|
unused ->
|
|
mobileDataDownloadManager.downloadFileGroup(
|
|
groupKey, downloadConditions, customFileGroupValidator),
|
|
sequentialControlExecutor)
|
|
.transformAsync(
|
|
dataFileGroup ->
|
|
createClientFileGroup(
|
|
dataFileGroup,
|
|
downloadFileGroupRequest.accountOptional().isPresent()
|
|
? AccountUtil.serialize(
|
|
downloadFileGroupRequest.accountOptional().get())
|
|
: null,
|
|
ClientFileGroup.Status.DOWNLOADED,
|
|
downloadFileGroupRequest.preserveZipDirectories(),
|
|
downloadFileGroupRequest.verifyIsolatedStructure(),
|
|
mobileDataDownloadManager,
|
|
sequentialControlExecutor,
|
|
fileStorage),
|
|
sequentialControlExecutor)
|
|
.transform(Preconditions::checkNotNull, sequentialControlExecutor);
|
|
|
|
ListenableFuture<ClientFileGroup> transformFuture =
|
|
PropagatedFutures.transformAsync(
|
|
foregroundDownloadFutureMap.add(
|
|
foregroundDownloadKey.toString(), downloadFileGroupFuture),
|
|
unused -> {
|
|
// Now that the download future is added, start the task and return the future
|
|
startTask.run();
|
|
return downloadFileGroupFuture;
|
|
},
|
|
sequentialControlExecutor);
|
|
|
|
PropagatedFutures.addCallback(
|
|
transformFuture,
|
|
new FutureCallback<ClientFileGroup>() {
|
|
@Override
|
|
public void onSuccess(ClientFileGroup clientFileGroup) {
|
|
// Currently the MobStore monitor does not support onSuccess so we have to add
|
|
// callback to the download future here.
|
|
try {
|
|
downloadListenerWithNotification.onComplete(clientFileGroup);
|
|
} catch (Exception e) {
|
|
LogUtil.w(
|
|
e,
|
|
"%s: Listener onComplete failed for group %s",
|
|
TAG,
|
|
clientFileGroup.getGroupName());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onFailure(Throwable t) {
|
|
// Currently the MobStore monitor does not support onFailure so we have to add
|
|
// callback to the download future here.
|
|
downloadListenerWithNotification.onFailure(t);
|
|
}
|
|
},
|
|
sequentialControlExecutor);
|
|
|
|
return transformFuture;
|
|
}
|
|
|
|
/** Helper method to return a {@link DownloadGroupState} for the given request. */
|
|
private ListenableFuture<DownloadGroupState> getDownloadGroupState(
|
|
DownloadFileGroupRequest downloadFileGroupRequest) {
|
|
ForegroundDownloadKey foregroundDownloadKey =
|
|
ForegroundDownloadKey.ofFileGroup(
|
|
downloadFileGroupRequest.groupName(),
|
|
downloadFileGroupRequest.accountOptional(),
|
|
downloadFileGroupRequest.variantIdOptional());
|
|
|
|
String groupName = downloadFileGroupRequest.groupName();
|
|
GroupKey.Builder groupKeyBuilder =
|
|
GroupKey.newBuilder().setGroupName(groupName).setOwnerPackage(context.getPackageName());
|
|
|
|
if (downloadFileGroupRequest.accountOptional().isPresent()) {
|
|
groupKeyBuilder.setAccount(
|
|
AccountUtil.serialize(downloadFileGroupRequest.accountOptional().get()));
|
|
}
|
|
|
|
if (downloadFileGroupRequest.variantIdOptional().isPresent()) {
|
|
groupKeyBuilder.setVariantId(downloadFileGroupRequest.variantIdOptional().get());
|
|
}
|
|
|
|
boolean isDownloadListenerPresent = downloadFileGroupRequest.listenerOptional().isPresent();
|
|
GroupKey groupKey = groupKeyBuilder.build();
|
|
|
|
return futureSerializer.submitAsync(
|
|
() -> {
|
|
ListenableFuture<Optional<ListenableFuture<ClientFileGroup>>>
|
|
foregroundDownloadFutureOptional =
|
|
foregroundDownloadFutureMap.get(foregroundDownloadKey.toString());
|
|
ListenableFuture<Optional<ListenableFuture<ClientFileGroup>>>
|
|
backgroundDownloadFutureOptional =
|
|
downloadFutureMap.get(foregroundDownloadKey.toString());
|
|
|
|
return PropagatedFutures.whenAllSucceed(
|
|
foregroundDownloadFutureOptional, backgroundDownloadFutureOptional)
|
|
.callAsync(
|
|
() -> {
|
|
if (getDone(foregroundDownloadFutureOptional).isPresent()) {
|
|
return immediateFuture(
|
|
DownloadGroupState.ofInProgressFuture(
|
|
getDone(foregroundDownloadFutureOptional).get()));
|
|
} else if (getDone(backgroundDownloadFutureOptional).isPresent()) {
|
|
return immediateFuture(
|
|
DownloadGroupState.ofInProgressFuture(
|
|
getDone(backgroundDownloadFutureOptional).get()));
|
|
}
|
|
|
|
// Get pending and downloaded versions to tell if we should return downloaded
|
|
// version early
|
|
ListenableFuture<GroupPair> fileGroupVersionsFuture =
|
|
PropagatedFutures.transformAsync(
|
|
mobileDataDownloadManager.getFileGroup(
|
|
groupKey, /* downloaded= */ false),
|
|
pendingDataFileGroup ->
|
|
PropagatedFutures.transform(
|
|
mobileDataDownloadManager.getFileGroup(
|
|
groupKey, /* downloaded= */ true),
|
|
downloadedDataFileGroup ->
|
|
GroupPair.create(
|
|
pendingDataFileGroup, downloadedDataFileGroup),
|
|
sequentialControlExecutor),
|
|
sequentialControlExecutor);
|
|
|
|
return PropagatedFutures.transformAsync(
|
|
fileGroupVersionsFuture,
|
|
fileGroupVersionsPair -> {
|
|
// if pending version is not null, return pending version
|
|
if (fileGroupVersionsPair.pendingGroup() != null) {
|
|
return immediateFuture(
|
|
DownloadGroupState.ofPendingGroup(
|
|
checkNotNull(fileGroupVersionsPair.pendingGroup())));
|
|
}
|
|
// If both groups are null, return group not found failure
|
|
if (fileGroupVersionsPair.downloadedGroup() == null) {
|
|
// TODO(b/174808410): Add Logging
|
|
// file group is not pending nor downloaded -- return failure.
|
|
DownloadException failure =
|
|
DownloadException.builder()
|
|
.setDownloadResultCode(DownloadResultCode.GROUP_NOT_FOUND_ERROR)
|
|
.setMessage(
|
|
"Nothing to download for file group: "
|
|
+ groupKey.getGroupName())
|
|
.build();
|
|
if (isDownloadListenerPresent) {
|
|
downloadFileGroupRequest.listenerOptional().get().onFailure(failure);
|
|
}
|
|
return immediateFailedFuture(failure);
|
|
}
|
|
|
|
DataFileGroupInternal downloadedDataFileGroup =
|
|
checkNotNull(fileGroupVersionsPair.downloadedGroup());
|
|
|
|
// Notify download listener (if present) that file group has been
|
|
// downloaded.
|
|
if (isDownloadListenerPresent) {
|
|
downloadMonitorOptional
|
|
.get()
|
|
.addDownloadListener(
|
|
downloadFileGroupRequest.groupName(),
|
|
downloadFileGroupRequest.listenerOptional().get());
|
|
}
|
|
PropagatedFluentFuture<ClientFileGroup> transformFuture =
|
|
PropagatedFluentFuture.from(
|
|
createClientFileGroup(
|
|
downloadedDataFileGroup,
|
|
downloadFileGroupRequest.accountOptional().isPresent()
|
|
? AccountUtil.serialize(
|
|
downloadFileGroupRequest.accountOptional().get())
|
|
: null,
|
|
ClientFileGroup.Status.DOWNLOADED,
|
|
downloadFileGroupRequest.preserveZipDirectories(),
|
|
downloadFileGroupRequest.verifyIsolatedStructure(),
|
|
mobileDataDownloadManager,
|
|
sequentialControlExecutor,
|
|
fileStorage))
|
|
.transform(Preconditions::checkNotNull, sequentialControlExecutor)
|
|
.transform(
|
|
clientFileGroup -> {
|
|
if (isDownloadListenerPresent) {
|
|
try {
|
|
downloadFileGroupRequest
|
|
.listenerOptional()
|
|
.get()
|
|
.onComplete(clientFileGroup);
|
|
} catch (Exception e) {
|
|
LogUtil.w(
|
|
e,
|
|
"%s: Listener onComplete failed for group %s",
|
|
TAG,
|
|
clientFileGroup.getGroupName());
|
|
}
|
|
downloadMonitorOptional
|
|
.get()
|
|
.removeDownloadListener(groupName);
|
|
}
|
|
return clientFileGroup;
|
|
},
|
|
sequentialControlExecutor);
|
|
transformFuture.addCallback(
|
|
new FutureCallback<ClientFileGroup>() {
|
|
@Override
|
|
public void onSuccess(ClientFileGroup result) {}
|
|
|
|
@Override
|
|
public void onFailure(Throwable t) {
|
|
if (isDownloadListenerPresent) {
|
|
downloadMonitorOptional.get().removeDownloadListener(groupName);
|
|
}
|
|
}
|
|
},
|
|
sequentialControlExecutor);
|
|
|
|
// Use directExecutor here since we are performing a trivial operation.
|
|
return transformFuture.transform(
|
|
DownloadGroupState::ofDownloadedGroup, directExecutor());
|
|
},
|
|
sequentialControlExecutor);
|
|
},
|
|
sequentialControlExecutor);
|
|
},
|
|
sequentialControlExecutor);
|
|
}
|
|
|
|
private DownloadListener createDownloadListenerWithNotification(
|
|
DownloadFileGroupRequest downloadRequest, DataFileGroupInternal fileGroup) {
|
|
|
|
String networkPausedMessage = getNetworkPausedMessage(downloadRequest, fileGroup);
|
|
|
|
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
|
ForegroundDownloadKey foregroundDownloadKey =
|
|
ForegroundDownloadKey.ofFileGroup(
|
|
downloadRequest.groupName(),
|
|
downloadRequest.accountOptional(),
|
|
downloadRequest.variantIdOptional());
|
|
|
|
NotificationCompat.Builder notification =
|
|
NotificationUtil.createNotificationBuilder(
|
|
context,
|
|
downloadRequest.groupSizeBytes(),
|
|
downloadRequest.contentTitleOptional().or(downloadRequest.groupName()),
|
|
downloadRequest.contentTextOptional().or(downloadRequest.groupName()));
|
|
int notificationKey = NotificationUtil.notificationKeyForKey(downloadRequest.groupName());
|
|
|
|
if (downloadRequest.showNotifications() == DownloadFileGroupRequest.ShowNotifications.ALL) {
|
|
NotificationUtil.createCancelAction(
|
|
context,
|
|
foregroundDownloadServiceClassOptional.get(),
|
|
foregroundDownloadKey.toString(),
|
|
notification,
|
|
notificationKey);
|
|
|
|
notificationManager.notify(notificationKey, notification.build());
|
|
}
|
|
|
|
return new DownloadListener() {
|
|
@Override
|
|
public void onProgress(long currentSize) {
|
|
// TODO(b/229123693): return this future once DownloadListener has an async api.
|
|
// There can be a race condition, where onProgress can be called
|
|
// after onComplete or onFailure which removes the future and the notification.
|
|
// Check foregroundDownloadFutureMap first before updating notification.
|
|
ListenableFuture<?> unused =
|
|
PropagatedFutures.transformAsync(
|
|
foregroundDownloadFutureMap.containsKey(foregroundDownloadKey.toString()),
|
|
futureInProgress -> {
|
|
if (futureInProgress
|
|
&& downloadRequest.showNotifications()
|
|
== DownloadFileGroupRequest.ShowNotifications.ALL) {
|
|
notification
|
|
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
|
|
.setSmallIcon(android.R.drawable.stat_sys_download)
|
|
.setProgress(
|
|
downloadRequest.groupSizeBytes(),
|
|
(int) currentSize,
|
|
/* indeterminate= */ downloadRequest.groupSizeBytes() <= 0);
|
|
notificationManager.notify(notificationKey, notification.build());
|
|
}
|
|
if (downloadRequest.listenerOptional().isPresent()) {
|
|
downloadRequest.listenerOptional().get().onProgress(currentSize);
|
|
}
|
|
return immediateVoidFuture();
|
|
},
|
|
sequentialControlExecutor);
|
|
}
|
|
|
|
@Override
|
|
public void pausedForConnectivity() {
|
|
// TODO(b/229123693): return this future once DownloadListener has an async api.
|
|
// There can be a race condition, where pausedForConnectivity can be called
|
|
// after onComplete or onFailure which removes the future and the notification.
|
|
// Check foregroundDownloadFutureMap first before updating notification.
|
|
ListenableFuture<?> unused =
|
|
PropagatedFutures.transformAsync(
|
|
foregroundDownloadFutureMap.containsKey(foregroundDownloadKey.toString()),
|
|
futureInProgress -> {
|
|
if (futureInProgress
|
|
&& downloadRequest.showNotifications()
|
|
== DownloadFileGroupRequest.ShowNotifications.ALL) {
|
|
notification
|
|
.setCategory(NotificationCompat.CATEGORY_STATUS)
|
|
.setContentText(networkPausedMessage)
|
|
.setSmallIcon(android.R.drawable.stat_sys_download)
|
|
.setOngoing(true)
|
|
// hide progress bar.
|
|
.setProgress(0, 0, false);
|
|
notificationManager.notify(notificationKey, notification.build());
|
|
}
|
|
if (downloadRequest.listenerOptional().isPresent()) {
|
|
downloadRequest.listenerOptional().get().pausedForConnectivity();
|
|
}
|
|
return immediateVoidFuture();
|
|
},
|
|
sequentialControlExecutor);
|
|
}
|
|
|
|
@Override
|
|
public void onComplete(ClientFileGroup clientFileGroup) {
|
|
// TODO(b/229123693): return this future once DownloadListener has an async api.
|
|
ListenableFuture<?> unused =
|
|
PropagatedFutures.submitAsync(
|
|
() -> {
|
|
boolean onCompleteFailed = false;
|
|
if (downloadRequest.listenerOptional().isPresent()) {
|
|
try {
|
|
downloadRequest.listenerOptional().get().onComplete(clientFileGroup);
|
|
} catch (Exception e) {
|
|
LogUtil.w(
|
|
e,
|
|
"%s: Delegate onComplete failed for group %s, showing failure"
|
|
+ " notification.",
|
|
TAG,
|
|
clientFileGroup.getGroupName());
|
|
onCompleteFailed = true;
|
|
}
|
|
}
|
|
|
|
// Clear the notification action.
|
|
if (downloadRequest.showNotifications()
|
|
== DownloadFileGroupRequest.ShowNotifications.ALL) {
|
|
notification.mActions.clear();
|
|
|
|
if (onCompleteFailed) {
|
|
// Show download failed in notification.
|
|
notification
|
|
.setCategory(NotificationCompat.CATEGORY_STATUS)
|
|
.setContentText(NotificationUtil.getDownloadFailedMessage(context))
|
|
.setOngoing(false)
|
|
.setSmallIcon(android.R.drawable.stat_sys_warning)
|
|
// hide progress bar.
|
|
.setProgress(0, 0, false);
|
|
|
|
notificationManager.notify(notificationKey, notification.build());
|
|
} else {
|
|
NotificationUtil.cancelNotificationForKey(
|
|
context, downloadRequest.groupName());
|
|
}
|
|
}
|
|
|
|
downloadMonitorOptional.get().removeDownloadListener(downloadRequest.groupName());
|
|
|
|
return foregroundDownloadFutureMap.remove(foregroundDownloadKey.toString());
|
|
},
|
|
sequentialControlExecutor);
|
|
}
|
|
|
|
@Override
|
|
public void onFailure(Throwable t) {
|
|
// TODO(b/229123693): return this future once DownloadListener has an async api.
|
|
ListenableFuture<?> unused =
|
|
PropagatedFutures.submitAsync(
|
|
() -> {
|
|
if (downloadRequest.showNotifications()
|
|
== DownloadFileGroupRequest.ShowNotifications.ALL) {
|
|
// Clear the notification action.
|
|
notification.mActions.clear();
|
|
|
|
// Show download failed in notification.
|
|
notification
|
|
.setCategory(NotificationCompat.CATEGORY_STATUS)
|
|
.setContentText(NotificationUtil.getDownloadFailedMessage(context))
|
|
.setOngoing(false)
|
|
.setSmallIcon(android.R.drawable.stat_sys_warning)
|
|
// hide progress bar.
|
|
.setProgress(0, 0, false);
|
|
|
|
notificationManager.notify(notificationKey, notification.build());
|
|
}
|
|
|
|
if (downloadRequest.listenerOptional().isPresent()) {
|
|
downloadRequest.listenerOptional().get().onFailure(t);
|
|
}
|
|
downloadMonitorOptional.get().removeDownloadListener(downloadRequest.groupName());
|
|
|
|
return foregroundDownloadFutureMap.remove(foregroundDownloadKey.toString());
|
|
},
|
|
sequentialControlExecutor);
|
|
}
|
|
};
|
|
}
|
|
|
|
// Helper method to get the correct network paused message
|
|
private String getNetworkPausedMessage(
|
|
DownloadFileGroupRequest downloadRequest, DataFileGroupInternal fileGroup) {
|
|
DeviceNetworkPolicy networkPolicyForDownload =
|
|
fileGroup.getDownloadConditions().getDeviceNetworkPolicy();
|
|
if (downloadRequest.downloadConditionsOptional().isPresent()) {
|
|
try {
|
|
networkPolicyForDownload =
|
|
ProtoConversionUtil.convert(downloadRequest.downloadConditionsOptional().get())
|
|
.getDeviceNetworkPolicy();
|
|
} catch (InvalidProtocolBufferException unused) {
|
|
// Do nothing -- we will rely on the file group's network policy.
|
|
}
|
|
}
|
|
|
|
switch (networkPolicyForDownload) {
|
|
case DOWNLOAD_FIRST_ON_WIFI_THEN_ON_ANY_NETWORK: // fallthrough
|
|
case DOWNLOAD_ONLY_ON_WIFI:
|
|
return NotificationUtil.getDownloadPausedWifiMessage(context);
|
|
default:
|
|
return NotificationUtil.getDownloadPausedMessage(context);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void cancelForegroundDownload(String downloadKey) {
|
|
LogUtil.d("%s: CancelForegroundDownload for key = %s", TAG, downloadKey);
|
|
ListenableFuture<?> unused =
|
|
PropagatedFutures.transformAsync(
|
|
foregroundDownloadFutureMap.get(downloadKey),
|
|
downloadFuture -> {
|
|
if (downloadFuture.isPresent()) {
|
|
LogUtil.v(
|
|
"%s: CancelForegroundDownload future found for key = %s, cancelling...",
|
|
TAG, downloadKey);
|
|
downloadFuture.get().cancel(false);
|
|
}
|
|
return immediateVoidFuture();
|
|
},
|
|
sequentialControlExecutor);
|
|
// Attempt cancel with internal MDD Lite instance in case it's a single file uri (cancel call is
|
|
// a noop if internal MDD Lite doesn't know about it).
|
|
singleFileDownloader.cancelForegroundDownload(downloadKey);
|
|
}
|
|
|
|
@Override
|
|
public void schedulePeriodicTasks() {
|
|
schedulePeriodicTasksInternal(Optional.absent());
|
|
}
|
|
|
|
@Override
|
|
public ListenableFuture<Void> schedulePeriodicBackgroundTasks() {
|
|
return futureSerializer.submit(
|
|
() -> {
|
|
schedulePeriodicTasksInternal(/* constraintOverridesMap= */ Optional.absent());
|
|
return null;
|
|
},
|
|
sequentialControlExecutor);
|
|
}
|
|
|
|
@Override
|
|
public ListenableFuture<Void> schedulePeriodicBackgroundTasks(
|
|
Optional<Map<String, ConstraintOverrides>> constraintOverridesMap) {
|
|
return futureSerializer.submit(
|
|
() -> {
|
|
schedulePeriodicTasksInternal(constraintOverridesMap);
|
|
return null;
|
|
},
|
|
sequentialControlExecutor);
|
|
}
|
|
|
|
private void schedulePeriodicTasksInternal(
|
|
Optional<Map<String, ConstraintOverrides>> constraintOverridesMap) {
|
|
if (!taskSchedulerOptional.isPresent()) {
|
|
LogUtil.e(
|
|
"%s: Called schedulePeriodicTasksInternal when taskScheduler is not provided.", TAG);
|
|
return;
|
|
}
|
|
|
|
TaskScheduler taskScheduler = taskSchedulerOptional.get();
|
|
|
|
// Schedule task that runs on charging without any network, every 6 hours.
|
|
taskScheduler.schedulePeriodicTask(
|
|
TaskScheduler.CHARGING_PERIODIC_TASK,
|
|
flags.chargingGcmTaskPeriod(),
|
|
NetworkState.NETWORK_STATE_ANY,
|
|
getConstraintOverrides(constraintOverridesMap, TaskScheduler.CHARGING_PERIODIC_TASK));
|
|
|
|
// Schedule maintenance task that runs on charging, once every day.
|
|
// This task should run even if mdd is disabled, to handle cleanup.
|
|
taskScheduler.schedulePeriodicTask(
|
|
TaskScheduler.MAINTENANCE_PERIODIC_TASK,
|
|
flags.maintenanceGcmTaskPeriod(),
|
|
NetworkState.NETWORK_STATE_ANY,
|
|
getConstraintOverrides(constraintOverridesMap, TaskScheduler.MAINTENANCE_PERIODIC_TASK));
|
|
|
|
// Schedule task that runs on cellular+charging, every 6 hours.
|
|
taskScheduler.schedulePeriodicTask(
|
|
TaskScheduler.CELLULAR_CHARGING_PERIODIC_TASK,
|
|
flags.cellularChargingGcmTaskPeriod(),
|
|
NetworkState.NETWORK_STATE_CONNECTED,
|
|
getConstraintOverrides(
|
|
constraintOverridesMap, TaskScheduler.CELLULAR_CHARGING_PERIODIC_TASK));
|
|
|
|
// Schedule task that runs on wifi+charging, every 6 hours.
|
|
taskScheduler.schedulePeriodicTask(
|
|
TaskScheduler.WIFI_CHARGING_PERIODIC_TASK,
|
|
flags.wifiChargingGcmTaskPeriod(),
|
|
NetworkState.NETWORK_STATE_UNMETERED,
|
|
getConstraintOverrides(constraintOverridesMap, TaskScheduler.WIFI_CHARGING_PERIODIC_TASK));
|
|
}
|
|
|
|
private static Optional<ConstraintOverrides> getConstraintOverrides(
|
|
Optional<Map<String, ConstraintOverrides>> constraintOverridesMap,
|
|
String maintenancePeriodicTask) {
|
|
return constraintOverridesMap.isPresent()
|
|
? Optional.fromNullable(constraintOverridesMap.get().get(maintenancePeriodicTask))
|
|
: Optional.absent();
|
|
}
|
|
|
|
@Override
|
|
public ListenableFuture<Void> cancelPeriodicBackgroundTasks() {
|
|
return futureSerializer.submit(
|
|
() -> {
|
|
cancelPeriodicTasksInternal();
|
|
return null;
|
|
},
|
|
sequentialControlExecutor);
|
|
}
|
|
|
|
private void cancelPeriodicTasksInternal() {
|
|
if (!taskSchedulerOptional.isPresent()) {
|
|
LogUtil.w("%s: Called cancelPeriodicTasksInternal when taskScheduler is not provided.", TAG);
|
|
return;
|
|
}
|
|
|
|
TaskScheduler taskScheduler = taskSchedulerOptional.get();
|
|
|
|
taskScheduler.cancelPeriodicTask(TaskScheduler.CHARGING_PERIODIC_TASK);
|
|
taskScheduler.cancelPeriodicTask(TaskScheduler.MAINTENANCE_PERIODIC_TASK);
|
|
taskScheduler.cancelPeriodicTask(TaskScheduler.CELLULAR_CHARGING_PERIODIC_TASK);
|
|
taskScheduler.cancelPeriodicTask(TaskScheduler.WIFI_CHARGING_PERIODIC_TASK);
|
|
}
|
|
|
|
@Override
|
|
public ListenableFuture<Void> handleTask(String tag) {
|
|
// All work done here that touches metadata (MobileDataDownloadManager) should be serialized
|
|
// through sequentialControlExecutor.
|
|
switch (tag) {
|
|
case TaskScheduler.MAINTENANCE_PERIODIC_TASK:
|
|
return futureSerializer.submitAsync(
|
|
mobileDataDownloadManager::maintenance, sequentialControlExecutor);
|
|
|
|
case TaskScheduler.CHARGING_PERIODIC_TASK:
|
|
ListenableFuture<Void> refreshFileGroupsFuture = refreshFileGroups();
|
|
return PropagatedFutures.transformAsync(
|
|
refreshFileGroupsFuture,
|
|
propagateAsyncFunction(
|
|
v -> mobileDataDownloadManager.verifyAllPendingGroups(customFileGroupValidator)),
|
|
sequentialControlExecutor);
|
|
|
|
case TaskScheduler.CELLULAR_CHARGING_PERIODIC_TASK:
|
|
return refreshAndDownload(false /*onWifi*/);
|
|
|
|
case TaskScheduler.WIFI_CHARGING_PERIODIC_TASK:
|
|
return refreshAndDownload(true /*onWifi*/);
|
|
|
|
default:
|
|
LogUtil.d("%s: gcm task doesn't belong to MDD", TAG);
|
|
return immediateFailedFuture(
|
|
new IllegalArgumentException("Unknown task tag sent to MDD.handleTask() " + tag));
|
|
}
|
|
}
|
|
|
|
private ListenableFuture<Void> refreshAndDownload(boolean onWifi) {
|
|
// We will do 2 passes to support 2-step downloads. In each step, we will refresh and then
|
|
// download.
|
|
return PropagatedFluentFuture.from(refreshFileGroups())
|
|
.transformAsync(
|
|
v ->
|
|
mobileDataDownloadManager.downloadAllPendingGroups(
|
|
onWifi, customFileGroupValidator),
|
|
sequentialControlExecutor)
|
|
.transformAsync(v -> refreshFileGroups(), sequentialControlExecutor)
|
|
.transformAsync(
|
|
v ->
|
|
mobileDataDownloadManager.downloadAllPendingGroups(
|
|
onWifi, customFileGroupValidator),
|
|
sequentialControlExecutor);
|
|
}
|
|
|
|
private ListenableFuture<Void> refreshFileGroups() {
|
|
List<ListenableFuture<Void>> refreshFutures = new ArrayList<>();
|
|
for (FileGroupPopulator fileGroupPopulator : fileGroupPopulatorList) {
|
|
refreshFutures.add(fileGroupPopulator.refreshFileGroups(this));
|
|
}
|
|
|
|
return PropagatedFutures.whenAllComplete(refreshFutures)
|
|
.call(() -> null, sequentialControlExecutor);
|
|
}
|
|
|
|
@Override
|
|
public ListenableFuture<Void> maintenance() {
|
|
return handleTask(TaskScheduler.MAINTENANCE_PERIODIC_TASK);
|
|
}
|
|
|
|
@Override
|
|
public ListenableFuture<Void> collectGarbage() {
|
|
return futureSerializer.submitAsync(
|
|
mobileDataDownloadManager::removeExpiredGroupsAndFiles, sequentialControlExecutor);
|
|
}
|
|
|
|
@Override
|
|
public ListenableFuture<Void> clear() {
|
|
return futureSerializer.submitAsync(
|
|
mobileDataDownloadManager::clear, sequentialControlExecutor);
|
|
}
|
|
|
|
// incompatible argument for parameter msg of e.
|
|
// incompatible types in return.
|
|
@Override
|
|
public String getDebugInfoAsString() {
|
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
|
PrintWriter writer = new PrintWriter(out);
|
|
try {
|
|
// Okay to block here because this method is for debugging only.
|
|
mobileDataDownloadManager.dump(writer).get(DUMP_DEBUG_INFO_TIMEOUT, TimeUnit.SECONDS);
|
|
writer.println("==== MOBSTORE_DEBUG_INFO ====");
|
|
writer.print(fileStorage.getDebugInfo());
|
|
} catch (ExecutionException | TimeoutException e) {
|
|
String errString = String.format("%s: Couldn't get debug info: %s", TAG, e);
|
|
LogUtil.e(errString);
|
|
return errString;
|
|
} catch (InterruptedException e) {
|
|
// see <internal>
|
|
Thread.currentThread().interrupt();
|
|
String errString = String.format("%s: Couldn't get debug info: %s", TAG, e);
|
|
LogUtil.e(errString);
|
|
return errString;
|
|
}
|
|
writer.flush();
|
|
return out.toString();
|
|
}
|
|
|
|
@Override
|
|
public ListenableFuture<Void> reportUsage(UsageEvent usageEvent) {
|
|
eventLogger.logMddUsageEvent(createFileGroupDetails(usageEvent.clientFileGroup()), null);
|
|
|
|
return immediateVoidFuture();
|
|
}
|
|
|
|
private static DownloadFutureMap.StateChangeCallbacks createCallbacksForForegroundService(
|
|
Context context, Optional<Class<?>> foregroundDownloadServiceClassOptional) {
|
|
return new DownloadFutureMap.StateChangeCallbacks() {
|
|
@Override
|
|
public void onAdd(String key, int newSize) {
|
|
// Only start foreground service if this is the first future we are adding.
|
|
if (newSize == 1 && foregroundDownloadServiceClassOptional.isPresent()) {
|
|
NotificationUtil.startForegroundDownloadService(
|
|
context, foregroundDownloadServiceClassOptional.get(), key);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onRemove(String key, int newSize) {
|
|
// Only stop foreground service if there are no more futures remaining.
|
|
if (newSize == 0 && foregroundDownloadServiceClassOptional.isPresent()) {
|
|
NotificationUtil.stopForegroundDownloadService(
|
|
context, foregroundDownloadServiceClassOptional.get(), key);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|