blob: 682045fb7d4c14f20ae5c6ba9dfaee2956d3c463 [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.downloader.offroad;
import android.net.Uri;
import android.util.Pair;
import com.google.android.downloader.DownloadConstraints;
import com.google.android.downloader.DownloadConstraints.NetworkType;
import com.google.android.downloader.DownloadDestination;
import com.google.android.downloader.DownloadRequest;
import com.google.android.downloader.DownloadResult;
import com.google.android.downloader.Downloader;
import com.google.android.downloader.OAuthTokenProvider;
import com.google.android.libraries.mobiledatadownload.DownloadException;
import com.google.android.libraries.mobiledatadownload.DownloadException.DownloadResultCode;
import com.google.android.libraries.mobiledatadownload.downloader.CheckContentChangeRequest;
import com.google.android.libraries.mobiledatadownload.downloader.CheckContentChangeResponse;
import com.google.android.libraries.mobiledatadownload.downloader.FileDownloader;
import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage;
import com.google.android.libraries.mobiledatadownload.file.common.MalformedUriException;
import com.google.android.libraries.mobiledatadownload.file.integration.downloader.DownloadDestinationOpener;
import com.google.android.libraries.mobiledatadownload.file.integration.downloader.DownloadMetadataStore;
import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil;
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.Strings;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.IOException;
import java.net.URI;
import java.util.concurrent.Executor;
import javax.annotation.Nullable;
/**
* An implementation of the {@link
* com.google.android.libraries.mobiledatadownload.downloader.FileDownloader} using <internal>
*/
public final class Offroad2FileDownloader implements FileDownloader {
private static final String TAG = "Offroad2FileDownloader";
private final Downloader downloader;
private final SynchronousFileStorage fileStorage;
private final Executor downloadExecutor;
private final DownloadMetadataStore downloadMetadataStore;
private final ExceptionHandler exceptionHandler;
private final Optional<Integer> defaultTrafficTag;
@Nullable private final OAuthTokenProvider authTokenProvider;
// TODO(b/208703042): refactor injection to remove dependency on ProtoDataStore
public Offroad2FileDownloader(
Downloader downloader,
SynchronousFileStorage fileStorage,
Executor downloadExecutor,
@Nullable OAuthTokenProvider authTokenProvider,
DownloadMetadataStore downloadMetadataStore,
ExceptionHandler exceptionHandler,
Optional<Integer> defaultTrafficTag) {
this.downloader = downloader;
this.fileStorage = fileStorage;
this.downloadExecutor = downloadExecutor;
this.authTokenProvider = authTokenProvider;
this.downloadMetadataStore = downloadMetadataStore;
this.exceptionHandler = exceptionHandler;
this.defaultTrafficTag = defaultTrafficTag;
}
@Override
public ListenableFuture<Void> startDownloading(
com.google.android.libraries.mobiledatadownload.downloader.DownloadRequest
fileDownloaderRequest) {
String fileName = Strings.nullToEmpty(fileDownloaderRequest.fileUri().getLastPathSegment());
DownloadDestination downloadDestination;
try {
downloadDestination = buildDownloadDestination(fileDownloaderRequest.fileUri());
} catch (DownloadException e) {
return Futures.immediateFailedFuture(e);
}
DownloadRequest offroad2DownloadRequest =
buildDownloadRequest(fileDownloaderRequest, downloadDestination);
FluentFuture<DownloadResult> resultFuture = downloader.execute(offroad2DownloadRequest);
LogUtil.d(
"%s: Data download scheduled for file: %s", TAG, fileDownloaderRequest.urlToDownload());
return PropagatedFluentFuture.from(resultFuture)
.catchingAsync(
Exception.class,
cause -> {
LogUtil.d(
cause,
"%s: Failed to download file %s due to: %s",
TAG,
fileName,
Strings.nullToEmpty(cause.getMessage()));
DownloadException exception =
exceptionHandler.mapToDownloadException("failure in download!", cause);
return Futures.immediateFailedFuture(exception);
},
downloadExecutor)
.transformAsync(
(DownloadResult result) -> {
LogUtil.d(
"%s: Downloaded file %s, bytes written: %d",
TAG, fileName, result.bytesWritten());
return PropagatedFutures.catchingAsync(
downloadMetadataStore.delete(fileDownloaderRequest.fileUri()),
Exception.class,
e -> {
// Failing to clean up metadata shouldn't cause a failure in the future, log and
// return void.
LogUtil.d(e, "%s: Failed to cleanup metadata", TAG);
return Futures.immediateVoidFuture();
},
downloadExecutor);
},
downloadExecutor);
}
@Override
public ListenableFuture<CheckContentChangeResponse> isContentChanged(
CheckContentChangeRequest checkContentChangeRequest) {
return Futures.immediateFailedFuture(
new UnsupportedOperationException(
"Checking for content changes is currently unsupported for Downloader2"));
}
private DownloadDestination buildDownloadDestination(Uri destinationUri)
throws DownloadException {
try {
// Create DownloadDestination using mobstore
return fileStorage.open(
destinationUri, DownloadDestinationOpener.create(downloadMetadataStore));
} catch (IOException e) {
if (e instanceof MalformedUriException || e.getCause() instanceof IllegalArgumentException) {
LogUtil.e("%s: The file uri is invalid, uri = %s", TAG, destinationUri);
throw DownloadException.builder()
.setDownloadResultCode(DownloadResultCode.MALFORMED_FILE_URI_ERROR)
.setCause(e)
.build();
} else {
LogUtil.e(e, "%s: Unable to create DownloadDestination for file %s", TAG, destinationUri);
// TODO: the result code is the most equivalent to downloader1 -- consider
// creating a separate result code that's more appropriate for downloader2.
throw DownloadException.builder()
.setDownloadResultCode(
DownloadResultCode.UNABLE_TO_CREATE_MOBSTORE_RESPONSE_WRITER_ERROR)
.setCause(e)
.build();
}
}
}
private DownloadRequest buildDownloadRequest(
com.google.android.libraries.mobiledatadownload.downloader.DownloadRequest
fileDownloaderRequest,
DownloadDestination downloadDestination) {
DownloadRequest.Builder requestBuilder =
downloader.newRequestBuilder(
URI.create(fileDownloaderRequest.urlToDownload()), downloadDestination);
requestBuilder.setOAuthTokenProvider(authTokenProvider);
if (com.google.android.libraries.mobiledatadownload.downloader.DownloadConstraints
.NETWORK_CONNECTED
== fileDownloaderRequest.downloadConstraints()) {
requestBuilder.setDownloadConstraints(DownloadConstraints.NETWORK_CONNECTED);
} else {
// Use all network types except cellular and require unmetered network.
requestBuilder.setDownloadConstraints(
DownloadConstraints.builder()
.addRequiredNetworkType(NetworkType.WIFI)
.addRequiredNetworkType(NetworkType.ETHERNET)
.addRequiredNetworkType(NetworkType.BLUETOOTH)
.setRequireUnmeteredNetwork(true)
.build());
}
if (fileDownloaderRequest.trafficTag() > 0) {
// Prefer traffic tag from request.
requestBuilder.setTrafficStatsTag(fileDownloaderRequest.trafficTag());
} else if (defaultTrafficTag.isPresent() && defaultTrafficTag.get() > 0) {
// Use default traffic tag as a fallback if present.
requestBuilder.setTrafficStatsTag(defaultTrafficTag.get());
}
for (Pair<String, String> header : fileDownloaderRequest.extraHttpHeaders()) {
requestBuilder.addHeader(header.first, header.second);
}
return requestBuilder.build();
}
}