1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.textclassifier.downloader; 18 19 import static com.android.textclassifier.downloader.ModelDownloadException.DEFAULT_DOWNLOADER_LIB_ERROR_CODE; 20 import static com.google.common.base.Predicates.instanceOf; 21 import static com.google.common.base.Throwables.getCausalChain; 22 23 import android.os.RemoteException; 24 import com.android.textclassifier.common.base.TcLog; 25 import com.google.android.downloader.AndroidDownloaderLogger; 26 import com.google.android.downloader.ConnectivityHandler; 27 import com.google.android.downloader.DownloadConstraints; 28 import com.google.android.downloader.DownloadRequest; 29 import com.google.android.downloader.DownloadResult; 30 import com.google.android.downloader.Downloader; 31 import com.google.android.downloader.PlatformUrlEngine; 32 import com.google.android.downloader.RequestException; 33 import com.google.android.downloader.SimpleFileDownloadDestination; 34 import com.google.common.annotations.VisibleForTesting; 35 import com.google.common.base.Preconditions; 36 import com.google.common.collect.ImmutableList; 37 import com.google.common.collect.Iterables; 38 import com.google.common.util.concurrent.FutureCallback; 39 import com.google.common.util.concurrent.Futures; 40 import com.google.common.util.concurrent.ListenableFuture; 41 import com.google.common.util.concurrent.ListeningExecutorService; 42 import com.google.common.util.concurrent.MoreExecutors; 43 import java.io.File; 44 import java.net.URI; 45 import java.util.concurrent.ExecutorService; 46 import javax.annotation.concurrent.ThreadSafe; 47 48 /** IModelDownloaderService implementation with Android Downloader library. */ 49 @ThreadSafe 50 final class ModelDownloaderServiceImpl extends IModelDownloaderService.Stub { 51 private static final String TAG = "ModelDownloaderServiceImpl"; 52 53 // Connectivity constraints will be checked by WorkManager instead. 54 private static class NoOpConnectivityHandler implements ConnectivityHandler { 55 @Override checkConnectivity(DownloadConstraints constraints)56 public ListenableFuture<Void> checkConnectivity(DownloadConstraints constraints) { 57 return Futures.immediateVoidFuture(); 58 } 59 } 60 61 private final ExecutorService bgExecutorService; 62 private final Downloader downloader; 63 ModelDownloaderServiceImpl( ExecutorService bgExecutorService, ListeningExecutorService transportExecutorService)64 public ModelDownloaderServiceImpl( 65 ExecutorService bgExecutorService, ListeningExecutorService transportExecutorService) { 66 this.bgExecutorService = bgExecutorService; 67 this.downloader = 68 new Downloader.Builder() 69 // This executor is for callbacks, not network IO. See discussions in cl/337156844 70 .withIOExecutor(bgExecutorService) 71 .withConnectivityHandler(new NoOpConnectivityHandler()) 72 .addUrlEngine( 73 // clear text traffic won't actually work without a manifest change, so http link 74 // is still not supported on production builds. 75 // Adding "http" here only for testing purposes. 76 ImmutableList.of("https", "http"), 77 new PlatformUrlEngine( 78 // This executor handles network transportation and can stall for long 79 transportExecutorService, 80 /* connectTimeoutMs= */ 60 * 1000, 81 /* readTimeoutMs= */ 60 * 1000)) 82 .withLogger(new AndroidDownloaderLogger()) 83 .build(); 84 } 85 86 @VisibleForTesting ModelDownloaderServiceImpl(ExecutorService bgExecutorService, Downloader downloader)87 ModelDownloaderServiceImpl(ExecutorService bgExecutorService, Downloader downloader) { 88 this.bgExecutorService = Preconditions.checkNotNull(bgExecutorService); 89 this.downloader = Preconditions.checkNotNull(downloader); 90 } 91 92 @Override download(String uri, String targetFilePath, IModelDownloaderCallback callback)93 public void download(String uri, String targetFilePath, IModelDownloaderCallback callback) { 94 TcLog.d(TAG, "Download request received: " + uri); 95 try { 96 File targetFile = new File(targetFilePath); 97 File tempMetadataFile = getMetadataFile(targetFile); 98 DownloadRequest request = 99 downloader 100 .newRequestBuilder( 101 URI.create(uri), new SimpleFileDownloadDestination(targetFile, tempMetadataFile)) 102 .build(); 103 downloader 104 .execute(request) 105 .transform(DownloadResult::bytesWritten, MoreExecutors.directExecutor()) 106 .addCallback( 107 new FutureCallback<Long>() { 108 @Override 109 public void onSuccess(Long bytesWritten) { 110 tempMetadataFile.delete(); 111 dispatchOnSuccessSafely(callback, bytesWritten); 112 } 113 114 @Override 115 public void onFailure(Throwable t) { 116 TcLog.e(TAG, "onFailure", t); 117 // TODO(licha): We may be able to resume the download if we keep those files 118 targetFile.delete(); 119 tempMetadataFile.delete(); 120 // Try to infer the failure reason 121 RequestException requestException = 122 (RequestException) 123 Iterables.find( 124 getCausalChain(t), 125 instanceOf(RequestException.class), 126 /* defaultValue= */ null); 127 // TODO(b/181805039): Use error code once downloader lib supports it. 128 int downloaderLibErrorCode = 129 requestException != null 130 ? requestException.getErrorDetails().getHttpStatusCode() 131 : DEFAULT_DOWNLOADER_LIB_ERROR_CODE; 132 dispatchOnFailureSafely(callback, downloaderLibErrorCode, t); 133 } 134 }, 135 bgExecutorService); 136 } catch (Throwable t) { 137 dispatchOnFailureSafely(callback, DEFAULT_DOWNLOADER_LIB_ERROR_CODE, t); 138 } 139 } 140 141 @VisibleForTesting getMetadataFile(File targetFile)142 static File getMetadataFile(File targetFile) { 143 return new File(targetFile.getParentFile(), targetFile.getName() + ".metadata"); 144 } 145 dispatchOnSuccessSafely( IModelDownloaderCallback callback, long bytesWritten)146 private static void dispatchOnSuccessSafely( 147 IModelDownloaderCallback callback, long bytesWritten) { 148 try { 149 callback.onSuccess(bytesWritten); 150 } catch (RemoteException e) { 151 TcLog.e(TAG, "Unable to notify successful download", e); 152 } 153 } 154 dispatchOnFailureSafely( IModelDownloaderCallback callback, int downloaderLibErrorCode, Throwable throwable)155 private static void dispatchOnFailureSafely( 156 IModelDownloaderCallback callback, int downloaderLibErrorCode, Throwable throwable) { 157 try { 158 callback.onFailure(downloaderLibErrorCode, throwable.getMessage()); 159 } catch (RemoteException e) { 160 TcLog.e(TAG, "Unable to notify failures in download", e); 161 } 162 } 163 } 164