blob: e9f75c157c274755d2598fab2d694fbd17247eb0 [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* 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.android.textclassifier.downloader;
import static com.google.common.base.Predicates.instanceOf;
import static com.google.common.base.Throwables.getCausalChain;
import android.os.RemoteException;
import com.android.textclassifier.common.base.TcLog;
import com.google.android.downloader.AndroidDownloaderLogger;
import com.google.android.downloader.ConnectivityHandler;
import com.google.android.downloader.DownloadConstraints;
import com.google.android.downloader.DownloadRequest;
import com.google.android.downloader.DownloadResult;
import com.google.android.downloader.Downloader;
import com.google.android.downloader.PlatformUrlEngine;
import com.google.android.downloader.RequestException;
import com.google.android.downloader.SimpleFileDownloadDestination;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
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.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import java.io.File;
import java.net.URI;
import java.util.concurrent.ExecutorService;
import javax.annotation.concurrent.ThreadSafe;
/** IModelDownloaderService implementation with Android Downloader library. */
@ThreadSafe
final class ModelDownloaderServiceImpl extends IModelDownloaderService.Stub {
private static final String TAG = "ModelDownloaderServiceImpl";
// Connectivity constraints will be checked by WorkManager instead.
private static class NoOpConnectivityHandler implements ConnectivityHandler {
@Override
public ListenableFuture<Void> checkConnectivity(DownloadConstraints constraints) {
return Futures.immediateVoidFuture();
}
}
private final ExecutorService bgExecutorService;
private final Downloader downloader;
public ModelDownloaderServiceImpl(
ExecutorService bgExecutorService, ListeningExecutorService transportExecutorService) {
this.bgExecutorService = bgExecutorService;
this.downloader =
new Downloader.Builder()
// This executor is for callbacks, not network IO. See discussions in cl/337156844
.withIOExecutor(bgExecutorService)
.withConnectivityHandler(new NoOpConnectivityHandler())
.addUrlEngine(
// clear text traffic won't actually work without a manifest change, so http link
// is still not supported on production builds.
// Adding "http" here only for testing purposes.
ImmutableList.of("https", "http"),
new PlatformUrlEngine(
// This executor handles network transportation and can stall for long
transportExecutorService,
/* connectTimeoutMs= */ 60 * 1000,
/* readTimeoutMs= */ 60 * 1000))
.withLogger(new AndroidDownloaderLogger())
.build();
}
@VisibleForTesting
ModelDownloaderServiceImpl(ExecutorService bgExecutorService, Downloader downloader) {
this.bgExecutorService = Preconditions.checkNotNull(bgExecutorService);
this.downloader = Preconditions.checkNotNull(downloader);
}
@Override
public void download(String uri, String targetFilePath, IModelDownloaderCallback callback) {
TcLog.v(TAG, "Download request received: " + uri);
try {
File targetFile = new File(targetFilePath);
File tempMetadataFile = getMetadataFile(targetFile);
DownloadRequest request =
downloader
.newRequestBuilder(
URI.create(uri), new SimpleFileDownloadDestination(targetFile, tempMetadataFile))
.build();
downloader
.execute(request)
.transform(DownloadResult::bytesWritten, MoreExecutors.directExecutor())
.addCallback(
new FutureCallback<Long>() {
@Override
public void onSuccess(Long bytesWritten) {
tempMetadataFile.delete();
dispatchOnSuccessSafely(callback, bytesWritten);
}
@Override
public void onFailure(Throwable t) {
TcLog.e(TAG, "onFailure", t);
// TODO(licha): We may be able to resume the download if we keep those files
targetFile.delete();
tempMetadataFile.delete();
// Try to infer the failure reason
RequestException requestException =
(RequestException)
Iterables.find(
getCausalChain(t),
instanceOf(RequestException.class),
/* defaultValue= */ null);
// TODO(licha): might be better to pass back the raw error code (instead of
// using the ErrorCode defined inside the ModelDownloadException).
int errorCode =
(requestException != null
&& requestException.getErrorDetails().getHttpStatusCode() == 404)
? ModelDownloadException.FAILED_TO_DOWNLOAD_404_ERROR
: ModelDownloadException.FAILED_TO_DOWNLOAD_OTHER;
dispatchOnFailureSafely(callback, errorCode, t);
}
},
bgExecutorService);
} catch (Throwable t) {
dispatchOnFailureSafely(callback, ModelDownloadException.FAILED_TO_DOWNLOAD_OTHER, t);
}
}
@VisibleForTesting
static File getMetadataFile(File targetFile) {
return new File(targetFile.getParentFile(), targetFile.getName() + ".metadata");
}
private static void dispatchOnSuccessSafely(
IModelDownloaderCallback callback, long bytesWritten) {
try {
callback.onSuccess(bytesWritten);
} catch (RemoteException e) {
TcLog.e(TAG, "Unable to notify successful download", e);
}
}
private static void dispatchOnFailureSafely(
IModelDownloaderCallback callback,
@ModelDownloadException.ErrorCode int errorCode,
Throwable throwable) {
try {
callback.onFailure(errorCode, throwable.getMessage());
} catch (RemoteException e) {
TcLog.e(TAG, "Unable to notify failures in download", e);
}
}
}