blob: 66d26ab556a7540e285b4cafb2f15ca299e2057a [file] [log] [blame]
/*
* 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.populator;
import static com.google.android.libraries.mobiledatadownload.tracing.TracePropagation.propagateAsyncCallable;
import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
import static com.google.common.util.concurrent.Futures.immediateVoidFuture;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.VisibleForTesting;
import com.google.mobiledatadownload.populator.MetadataProto.ManifestFileBookkeeping;
import com.google.mobiledatadownload.populator.MetadataProto.ManifestFileBookkeeping.Status;
import com.google.android.libraries.mobiledatadownload.AggregateException;
import com.google.android.libraries.mobiledatadownload.DownloadException;
import com.google.android.libraries.mobiledatadownload.DownloadException.DownloadResultCode;
import com.google.android.libraries.mobiledatadownload.FileGroupPopulator;
import com.google.android.libraries.mobiledatadownload.Flags;
import com.google.android.libraries.mobiledatadownload.Logger;
import com.google.android.libraries.mobiledatadownload.MobileDataDownload;
import com.google.android.libraries.mobiledatadownload.downloader.CheckContentChangeRequest;
import com.google.android.libraries.mobiledatadownload.downloader.CheckContentChangeResponse;
import com.google.android.libraries.mobiledatadownload.downloader.DownloadConstraints;
import com.google.android.libraries.mobiledatadownload.downloader.DownloadRequest;
import com.google.android.libraries.mobiledatadownload.downloader.FileDownloader;
import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage;
import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil;
import com.google.android.libraries.mobiledatadownload.internal.util.DirectoryUtil;
import com.google.android.libraries.mobiledatadownload.logger.FileGroupPopulatorLogger;
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.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ExecutionSequencer;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.mobiledatadownload.DownloadConfigProto.DataFileGroup;
import com.google.mobiledatadownload.DownloadConfigProto.ManifestConfig;
import com.google.mobiledatadownload.DownloadConfigProto.ManifestFileFlag;
import java.io.IOException;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nullable;
import javax.inject.Singleton;
/**
* File group populator that gets {@link ManifestFileFlag} from the caller, downloads the
* corresponding manifest file, parses the file into {@link ManifestConfig}, and processes {@link
* ManifestConfig}.
*
* <p>Client can set an optional {@link ManifestConfigOverrider} to return a list of {@link
* DataFileGroup}'s to be added to MDD. The overrider will enable the on device targeting.
*
* <p>Client is responsible of reading {@link ManifestFileFlag} from P/H, and this populator would
* get the flag via {@link Supplier<ManifestFileFlag>}.
*
* <p>On calling {@link #refreshFileGroups(MobileDataDownload)}, this populator would sync up with
* server to verify if the manifest file on server has changed since last download. It would
* re-download the file if a newer version is available. More specifically, there are 3 scenarios:
*
* <ul>
* <li>1. Current file up-to-date, status PENDING. Resume download.
* <li>2. Current file up-to-date, status (DOWNLOADED | COMMITTED). No download will happen.
* <li>3. Current file outdated. Delete the outdated file and re-download.
* </ul>
*
* <p>To ensure that each time we download the most up-to-date manifest file correctly, we will
* check for {@link FileDownloader#isContentChanged(CheckContentChangeRequest)} twice:
*
* <ul>
* <li>1. Before the download to check if the new download is necessary.
* <li>2. After the download to make sure that the content is not out of date.
* </ul>
*
* <p>Note that the current prerequisite of using {@link ManifestFileGroupPopulator} is that, the
* hosting service needs to support ETag (e.g. Lorry), otherwise the behavior will be unexpected.
* Talk to <internal>@ if you are not sure if the hosting service supports ETag.
*
* <p>Note that {@link SynchronousFileStorage} and {@link ProtoDataStoreFactory} passed to builder
* must be @Singleton.
*
* <p>This class is @Singleton, because it provides the guarantee that all the operations are
* serialized correctly by {@link ExecutionSequencer}.
*/
@Singleton
public final class ManifestFileGroupPopulator implements FileGroupPopulator {
private static final String TAG = "ManifestFileGroupPopulator";
/** The parser of the manifest file. */
public interface ManifestConfigParser {
/** Parses the input file and returns the {@link ManifestConfig}. */
ListenableFuture<ManifestConfig> parse(Uri fileUri);
}
/** Builder for {@link ManifestFileGroupPopulator}. */
public static final class Builder {
private boolean allowsInsecureHttp = false;
private boolean dedupDownloadWithEtag = true;
private Context context;
private Supplier<ManifestFileFlag> manifestFileFlagSupplier;
private Supplier<FileDownloader> fileDownloader;
private ManifestConfigParser manifestConfigParser;
private SynchronousFileStorage fileStorage;
private Executor backgroundExecutor;
private ManifestFileMetadataStore manifestFileMetadataStore;
private Logger logger;
private Optional<ManifestConfigOverrider> overriderOptional = Optional.absent();
private Optional<String> instanceIdOptional = Optional.absent();
private Flags flags = new Flags() {};
/**
* Sets the flag that allows insecure http.
*
* <p>For testing only.
*/
@VisibleForTesting
Builder setAllowsInsecureHttp(boolean allowsInsecureHttp) {
this.allowsInsecureHttp = allowsInsecureHttp;
return this;
}
/**
* By default, an HTTP HEAD request is made to avoid duplicate downloads of the manifest file.
* Setting this to false disables that behavior.
*/
public Builder setDedupDownloadWithEtag(boolean dedup) {
this.dedupDownloadWithEtag = dedup;
return this;
}
/** Sets the context. */
public Builder setContext(Context context) {
this.context = context.getApplicationContext();
return this;
}
/** Sets the manifest file flag. */
public Builder setManifestFileFlagSupplier(
Supplier<ManifestFileFlag> manifestFileFlagSupplier) {
this.manifestFileFlagSupplier = manifestFileFlagSupplier;
return this;
}
/** Sets the file downloader. */
public Builder setFileDownloader(Supplier<FileDownloader> fileDownloader) {
this.fileDownloader = fileDownloader;
return this;
}
/** Sets the manifest config parser that takes file uri and returns {@link ManifestConfig}. */
public Builder setManifestConfigParser(ManifestConfigParser manifestConfigParser) {
this.manifestConfigParser = manifestConfigParser;
return this;
}
/** Sets the mobstore file storage. Mobstore file storage must be singleton. */
public Builder setFileStorage(SynchronousFileStorage fileStorage) {
this.fileStorage = fileStorage;
return this;
}
/** Sets the background executor that executes populator's tasks sequentially. */
public Builder setBackgroundExecutor(Executor backgroundExecutor) {
this.backgroundExecutor = backgroundExecutor;
return this;
}
/** Sets the ManifestFileMetadataStore. */
public Builder setMetadataStore(ManifestFileMetadataStore manifestFileMetadataStore) {
this.manifestFileMetadataStore = manifestFileMetadataStore;
return this;
}
/** Sets the MDD logger. */
public Builder setLogger(Logger logger) {
this.logger = logger;
return this;
}
/** Sets the optional manifest config overrider. */
public Builder setOverriderOptional(Optional<ManifestConfigOverrider> overriderOptional) {
this.overriderOptional = overriderOptional;
return this;
}
/** Sets the optional instance ID. */
public Builder setInstanceIdOptional(Optional<String> instanceIdOptional) {
this.instanceIdOptional = instanceIdOptional;
return this;
}
public Builder setFlags(Flags flags) {
this.flags = flags;
return this;
}
public ManifestFileGroupPopulator build() {
Preconditions.checkNotNull(context, "Must call setContext() before build().");
Preconditions.checkNotNull(
manifestFileFlagSupplier, "Must call setManifestFileFlagSupplier() before build().");
Preconditions.checkNotNull(fileDownloader, "Must call setFileDownloader() before build().");
Preconditions.checkNotNull(
manifestConfigParser, "Must call setManifestConfigParser() before build().");
Preconditions.checkNotNull(fileStorage, "Must call setFileStorage() before build().");
Preconditions.checkNotNull(
backgroundExecutor, "Must call setBackgroundExecutor() before build().");
Preconditions.checkNotNull(
manifestFileMetadataStore, "Must call manifestFileMetadataStore() before build().");
Preconditions.checkNotNull(logger, "Must call setLogger() before build().");
return new ManifestFileGroupPopulator(this);
}
}
private final boolean allowsInsecureHttp;
private final boolean dedupDownloadWithEtag;
private final Context context;
private final Uri manifestDirectoryUri;
private final Supplier<ManifestFileFlag> manifestFileFlagSupplier;
private final Supplier<FileDownloader> fileDownloader;
private final ManifestConfigParser manifestConfigParser;
private final SynchronousFileStorage fileStorage;
private final Executor backgroundExecutor;
private final Optional<ManifestConfigOverrider> overriderOptional;
private final ManifestFileMetadataStore manifestFileMetadataStore;
private final FileGroupPopulatorLogger eventLogger;
// We use futureSerializer for synchronization.
private final ExecutionSequencer futureSerializer = ExecutionSequencer.create();
/** Returns a Builder for {@link ManifestFileGroupPopulator}. */
public static Builder builder() {
return new Builder();
}
private ManifestFileGroupPopulator(Builder builder) {
this.allowsInsecureHttp = builder.allowsInsecureHttp;
this.dedupDownloadWithEtag = builder.dedupDownloadWithEtag;
this.context = builder.context;
this.manifestDirectoryUri =
DirectoryUtil.getManifestDirectory(builder.context, builder.instanceIdOptional);
this.manifestFileFlagSupplier = builder.manifestFileFlagSupplier;
this.fileDownloader = builder.fileDownloader;
this.manifestConfigParser = builder.manifestConfigParser;
this.fileStorage = builder.fileStorage;
this.backgroundExecutor = builder.backgroundExecutor;
this.overriderOptional = builder.overriderOptional;
this.eventLogger = new FileGroupPopulatorLogger(builder.logger, builder.flags);
this.manifestFileMetadataStore = builder.manifestFileMetadataStore;
}
@Override
public ListenableFuture<Void> refreshFileGroups(MobileDataDownload mobileDataDownload) {
return futureSerializer.submitAsync(
propagateAsyncCallable(
() -> {
LogUtil.d("%s: Add groups from ManifestFileFlag to MDD.", TAG);
// We will return immediately if the flag is null or empty. This could happen if P/H
// has not synced the flag or we fail to parse the flag.
ManifestFileFlag manifestFileFlag = manifestFileFlagSupplier.get();
if (manifestFileFlag == null
|| manifestFileFlag.equals(ManifestFileFlag.getDefaultInstance())) {
LogUtil.w("%s: The ManifestFileFlag is empty.", TAG);
logRefreshResult(0, ManifestFileFlag.getDefaultInstance());
return immediateVoidFuture();
}
return refreshFileGroups(mobileDataDownload, manifestFileFlag);
}),
backgroundExecutor);
}
private ListenableFuture<Void> refreshFileGroups(
MobileDataDownload mobileDataDownload, ManifestFileFlag manifestFileFlag) {
if (!validate(manifestFileFlag)) {
logRefreshResult(0, manifestFileFlag);
LogUtil.e("%s: Invalid manifest config from manifest flag.", TAG);
return immediateFailedFuture(new IllegalArgumentException("Invalid manifest flag."));
}
String manifestFileUrl = manifestFileFlag.getManifestFileUrl();
// Manifest files are named and identified with their manifest ID.
Uri manifestFileUri =
manifestDirectoryUri.buildUpon().appendPath(manifestFileFlag.getManifestId()).build();
// Represents the internal state of the metadata. Using AtomicReference here because the
// variable captured by lambda needs to be final.
final AtomicReference<ManifestFileBookkeeping> bookkeepingRef =
new AtomicReference<>(createDefaultManifestFileBookkeeping(manifestFileUrl));
ListenableFuture<Void> checkFuture =
PropagatedFluentFuture.from(readBookeeping(manifestFileFlag.getManifestId()))
.transform(
(final Optional<ManifestFileBookkeeping> bookkeepingOptional) -> {
if (bookkeepingOptional.isPresent()) {
bookkeepingRef.set(bookkeepingOptional.get());
}
return (Void) null;
},
backgroundExecutor)
.transformAsync(
voidArg ->
// We need to call checkForContentChangeBeforeDownload to sync back the latest
// ETag, even when there is no entry for bookkeeping.
checkForContentChangeBeforeDownload(
manifestFileUrl, manifestFileUri, bookkeepingRef),
backgroundExecutor);
ListenableFuture<Optional<Throwable>> transformCheckFuture =
PropagatedFluentFuture.from(checkFuture)
.transform(voidArg -> Optional.<Throwable>absent(), backgroundExecutor)
.catching(Throwable.class, Optional::of, backgroundExecutor);
ListenableFuture<Void> processFuture =
PropagatedFluentFuture.from(transformCheckFuture)
.transformAsync(
(final Optional<Throwable> throwableOptional) -> {
// We do not want to proceed if transformCheckFuture contains failures, so return
// early.
if (throwableOptional.isPresent()) {
return immediateVoidFuture();
}
ManifestFileBookkeeping bookkeeping = bookkeepingRef.get();
if (bookkeeping.getStatus() == Status.COMMITTED) {
LogUtil.d("%s: Manifest file was committed.", TAG);
if (!overriderOptional.isPresent()) {
return immediateVoidFuture();
}
// When the overrider is present, it may produce different configs each time the
// caller triggers refresh. Therefore, we need to recommit to MDD.
LogUtil.d("%s: Overrider is present, commit again.", TAG);
return parseAndCommitManifestFile(
mobileDataDownload, manifestFileUri, bookkeepingRef);
}
if (bookkeeping.getStatus() == Status.DOWNLOADED) {
LogUtil.d("%s: Manifest file was downloaded.", TAG);
return parseAndCommitManifestFile(
mobileDataDownload, manifestFileUri, bookkeepingRef);
}
return PropagatedFluentFuture.from(
downloadManifestFile(manifestFileUrl, manifestFileUri))
.transformAsync(
voidArgInner ->
checkForContentChangeAfterDownload(
manifestFileUrl, manifestFileUri, bookkeepingRef),
backgroundExecutor)
.transformAsync(
voidArgInner ->
parseAndCommitManifestFile(
mobileDataDownload, manifestFileUri, bookkeepingRef),
backgroundExecutor);
},
backgroundExecutor);
ListenableFuture<Void> catchingProcessFuture =
PropagatedFutures.catchingAsync(
processFuture,
Throwable.class,
(Throwable unused) -> {
ManifestFileBookkeeping bookkeeping = bookkeepingRef.get();
bookkeepingRef.set(bookkeeping.toBuilder().setStatus(Status.PENDING).build());
deleteManifestFileChecked(manifestFileUri);
return immediateVoidFuture();
},
backgroundExecutor);
ListenableFuture<Void> updateFuture =
PropagatedFutures.transformAsync(
catchingProcessFuture,
voidArg -> writeBookkeeping(manifestFileFlag.getManifestId(), bookkeepingRef.get()),
backgroundExecutor);
return PropagatedFutures.transformAsync(
updateFuture,
voidArg -> {
logAndThrowIfFailed(
ImmutableList.of(checkFuture, processFuture, updateFuture),
"Failed to refresh file groups",
manifestFileFlag);
// If there is any failure, it should have been thrown already. Therefore, we log refresh
// success here.
logRefreshResult(0, manifestFileFlag);
return immediateVoidFuture();
},
backgroundExecutor);
}
private boolean validate(@Nullable ManifestFileFlag manifestFileFlag) {
if (manifestFileFlag == null) {
return false;
}
if (!manifestFileFlag.hasManifestId() || manifestFileFlag.getManifestId().isEmpty()) {
return false;
}
if (!manifestFileFlag.hasManifestFileUrl()
|| (!allowsInsecureHttp && !manifestFileFlag.getManifestFileUrl().startsWith("https"))) {
return false;
}
return true;
}
private ListenableFuture<Void> parseAndCommitManifestFile(
MobileDataDownload mobileDataDownload,
Uri manifestFileUri,
AtomicReference<ManifestFileBookkeeping> bookkeepingRef) {
return PropagatedFluentFuture.from(parseManifestFile(manifestFileUri))
.transformAsync(
(final ManifestConfig manifestConfig) ->
ManifestConfigHelper.refreshFromManifestConfig(
mobileDataDownload, manifestConfig, overriderOptional),
backgroundExecutor)
.transformAsync(
voidArg -> {
ManifestFileBookkeeping bookkeeping = bookkeepingRef.get();
bookkeepingRef.set(bookkeeping.toBuilder().setStatus(Status.COMMITTED).build());
return immediateVoidFuture();
},
backgroundExecutor);
}
private ListenableFuture<Void> downloadManifestFile(String urlToDownload, Uri destinationUri) {
LogUtil.d(
"%s: Start downloading the manifest file from %s to %s.",
TAG, urlToDownload, destinationUri.toString());
// We now download manifest file on any network (similar to P/H). In the future, we may want to
// restrict the download only on WiFi, and need to introduce network policy. (However, some
// users are never on WiFi)
//
// Note: Right now, if the download of manifest config file is set to WiFi only but this
// populator is triggered in CELLULAR_CHARGING task, then the downloading will be blocked.
DownloadConstraints downloadConstraints = DownloadConstraints.NETWORK_CONNECTED;
return fileDownloader
.get()
.startDownloading(
DownloadRequest.newBuilder()
.setUrlToDownload(urlToDownload)
.setFileUri(destinationUri)
.setDownloadConstraints(downloadConstraints)
.build());
}
private ListenableFuture<ManifestConfig> parseManifestFile(Uri manifestFileUri) {
LogUtil.d("%s: Parse the manifest file at %s.", TAG, manifestFileUri);
ListenableFuture<ManifestConfig> parseFuture = manifestConfigParser.parse(manifestFileUri);
return DownloadException.wrapIfFailed(
parseFuture,
DownloadResultCode.MANIFEST_FILE_GROUP_POPULATOR_PARSE_MANIFEST_FILE_ERROR,
"Failed to parse the manifest file.");
}
private ListenableFuture<Void> checkForContentChangeBeforeDownload(
String urlToDownload,
Uri manifestFileUri,
AtomicReference<ManifestFileBookkeeping> bookkeepingRef) {
LogUtil.d("%s: Prepare for downloading manifest file.", TAG);
if (!dedupDownloadWithEtag) {
return immediateVoidFuture();
}
ManifestFileBookkeeping bookkeeping = bookkeepingRef.get();
ListenableFuture<CheckContentChangeResponse> isContentChangedFuture =
fileDownloader
.get()
.isContentChanged(
CheckContentChangeRequest.newBuilder()
.setUrl(urlToDownload)
.setCachedETagOptional(getCachedETag(bookkeeping))
.build());
return PropagatedFutures.transformAsync(
isContentChangedFuture,
(final CheckContentChangeResponse response) -> {
Status currentStatus = bookkeepingRef.get().getStatus();
// If the manifest file on server side has been modified since last download, then the
// manifest file previously downloaded is now stale. We need to delete it and re-download
// the latest version.
//
// In case of url changes, we still want to send the network request to fetch the ETag.
boolean urlUpdated = !urlToDownload.equals(bookkeeping.getManifestFileUrl());
if (urlUpdated || response.contentChanged()) {
LogUtil.d(
"%s: Manifest file on server updated, will re-download; urlToDownload = %s;"
+ " manifestFileUri = %s",
TAG, urlToDownload, manifestFileUri);
currentStatus = Status.PENDING;
deleteManifestFileChecked(manifestFileUri);
}
bookkeepingRef.set(
createManifestFileBookkeeping(
urlToDownload, currentStatus, response.freshETagOptional()));
return immediateVoidFuture();
},
backgroundExecutor);
}
private ListenableFuture<Void> checkForContentChangeAfterDownload(
String urlToDownload,
Uri manifestFileUri,
AtomicReference<ManifestFileBookkeeping> bookkeepingRef) {
LogUtil.d("%s: Finalize for downloading manifest file.", TAG);
if (!dedupDownloadWithEtag) {
return immediateVoidFuture();
}
ManifestFileBookkeeping bookkeeping = bookkeepingRef.get();
ListenableFuture<CheckContentChangeResponse> isContentChangedFuture =
fileDownloader
.get()
.isContentChanged(
CheckContentChangeRequest.newBuilder()
.setUrl(urlToDownload)
.setCachedETagOptional(getCachedETag(bookkeeping))
.build());
return PropagatedFutures.transformAsync(
isContentChangedFuture,
(final CheckContentChangeResponse response) -> {
// If the manifest file on server has changed during download. The manifest file we just
// downloaded is stale during the download.
if (response.contentChanged()) {
LogUtil.e(
"%s: Manifest file on server changed during download, download failed;"
+ " urlToDownload = %s; manifestFileUri = %s",
TAG, urlToDownload, manifestFileUri);
return immediateFailedFuture(
DownloadException.builder()
.setDownloadResultCode(
DownloadResultCode
.MANIFEST_FILE_GROUP_POPULATOR_CONTENT_CHANGED_DURING_DOWNLOAD_ERROR)
.setMessage("Manifest file on server changed during download.")
.build());
}
bookkeepingRef.set(
createManifestFileBookkeeping(
urlToDownload, Status.DOWNLOADED, response.freshETagOptional()));
return immediateVoidFuture();
},
backgroundExecutor);
}
private ListenableFuture<Optional<ManifestFileBookkeeping>> readBookeeping(String manifestId) {
return DownloadException.wrapIfFailed(
manifestFileMetadataStore.read(manifestId),
DownloadResultCode.MANIFEST_FILE_GROUP_POPULATOR_METADATA_IO_ERROR,
"Failed to read bookkeeping.");
}
private ListenableFuture<Void> writeBookkeeping(
String manifestId, ManifestFileBookkeeping value) {
return DownloadException.wrapIfFailed(
manifestFileMetadataStore.upsert(manifestId, value),
DownloadResultCode.MANIFEST_FILE_GROUP_POPULATOR_METADATA_IO_ERROR,
"Failed to write bookkeeping.");
}
private void deleteManifestFileChecked(Uri manifestFileUri) throws DownloadException {
try {
deleteManifestFile(manifestFileUri);
} catch (IOException e) {
throw DownloadException.builder()
.setCause(e)
.setDownloadResultCode(
DownloadResultCode.MANIFEST_FILE_GROUP_POPULATOR_DELETE_MANIFEST_FILE_ERROR)
.setMessage("Failed to delete manifest file.")
.build();
}
}
private void deleteManifestFile(Uri manifestFileUri) throws IOException {
if (fileStorage.exists(manifestFileUri)) {
LogUtil.d("%s: Removing manifest file at: %s", TAG, manifestFileUri);
fileStorage.deleteFile(manifestFileUri);
} else {
LogUtil.d("%s: Manifest file doesn't exist: %s", TAG, manifestFileUri);
}
}
private void logRefreshResult(DownloadException e, ManifestFileFlag manifestFileFlag) {
eventLogger.logManifestFileGroupPopulatorRefreshResult(
0,
manifestFileFlag.getManifestId(),
context.getPackageName(),
manifestFileFlag.getManifestFileUrl());
}
private void logRefreshResult(int code, ManifestFileFlag manifestFileFlag) {
eventLogger.logManifestFileGroupPopulatorRefreshResult(
code,
manifestFileFlag.getManifestId(),
context.getPackageName(),
manifestFileFlag.getManifestFileUrl());
}
private void logAndThrowIfFailed(
ImmutableList<ListenableFuture<Void>> futures,
String message,
ManifestFileFlag manifestFileFlag)
throws AggregateException {
FutureCallback<Void> logRefreshResultCallback =
new FutureCallback<Void>() {
@Override
public void onSuccess(Void unused) {}
@Override
public void onFailure(Throwable t) {
if (t instanceof DownloadException) {
logRefreshResult((DownloadException) t, manifestFileFlag);
} else {
// Here, we encountered an error that is unchecked. If UNKNOWN_ERROR is observed, we
// will need to investigate the cause and have it checked.
logRefreshResult(
DownloadException.builder()
.setCause(t)
.setDownloadResultCode(DownloadResultCode.UNKNOWN_ERROR)
.setMessage("Refresh failed.")
.build(),
manifestFileFlag);
}
}
};
AggregateException.throwIfFailed(futures, Optional.of(logRefreshResultCallback), message);
}
private static ManifestFileBookkeeping createDefaultManifestFileBookkeeping(
String manifestFileUrl) {
return createManifestFileBookkeeping(
manifestFileUrl, Status.PENDING, /* eTagOptional = */ Optional.absent());
}
private static ManifestFileBookkeeping createManifestFileBookkeeping(
String manifestFileUrl, Status status, Optional<String> eTagOptional) {
ManifestFileBookkeeping.Builder bookkeeping =
ManifestFileBookkeeping.newBuilder().setManifestFileUrl(manifestFileUrl).setStatus(status);
if (eTagOptional.isPresent()) {
bookkeeping.setCachedEtag(eTagOptional.get());
}
return bookkeeping.build();
}
private static Optional<String> getCachedETag(ManifestFileBookkeeping bookkeeping) {
return bookkeeping.hasCachedEtag()
? Optional.of(bookkeeping.getCachedEtag())
: Optional.absent();
}
}