1 /* 2 * Copyright 2022 Google LLC 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 package com.google.android.libraries.mobiledatadownload.downloader.offroad; 17 18 import android.net.Uri; 19 import android.util.Pair; 20 21 import com.google.android.downloader.DownloadConstraints; 22 import com.google.android.downloader.DownloadConstraints.NetworkType; 23 import com.google.android.downloader.DownloadDestination; 24 import com.google.android.downloader.DownloadRequest; 25 import com.google.android.downloader.DownloadResult; 26 import com.google.android.downloader.Downloader; 27 import com.google.android.downloader.OAuthTokenProvider; 28 import com.google.android.libraries.mobiledatadownload.DownloadException; 29 import com.google.android.libraries.mobiledatadownload.DownloadException.DownloadResultCode; 30 import com.google.android.libraries.mobiledatadownload.downloader.CheckContentChangeRequest; 31 import com.google.android.libraries.mobiledatadownload.downloader.CheckContentChangeResponse; 32 import com.google.android.libraries.mobiledatadownload.downloader.FileDownloader; 33 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage; 34 import com.google.android.libraries.mobiledatadownload.file.common.MalformedUriException; 35 import com.google.android.libraries.mobiledatadownload.file.integration.downloader.DownloadDestinationOpener; 36 import com.google.android.libraries.mobiledatadownload.file.integration.downloader.DownloadMetadataStore; 37 import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil; 38 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFluentFuture; 39 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures; 40 import com.google.common.base.Optional; 41 import com.google.common.base.Strings; 42 import com.google.common.util.concurrent.FluentFuture; 43 import com.google.common.util.concurrent.Futures; 44 import com.google.common.util.concurrent.ListenableFuture; 45 46 import java.io.IOException; 47 import java.net.URI; 48 import java.util.concurrent.Executor; 49 50 import javax.annotation.Nullable; 51 52 /** 53 * An implementation of the {@link 54 * com.google.android.libraries.mobiledatadownload.downloader.FileDownloader} using <internal> 55 */ 56 public final class Offroad2FileDownloader implements FileDownloader { 57 private static final String TAG = "Offroad2FileDownloader"; 58 59 private final Downloader downloader; 60 private final SynchronousFileStorage fileStorage; 61 private final Executor downloadExecutor; 62 private final DownloadMetadataStore downloadMetadataStore; 63 private final ExceptionHandler exceptionHandler; 64 // private final Optional<Supplier<CookieJar>> cookieJarSupplierOptional; 65 private final Optional<Integer> defaultTrafficTag; 66 @Nullable 67 private final OAuthTokenProvider authTokenProvider; 68 Offroad2FileDownloader( Downloader downloader, SynchronousFileStorage fileStorage, Executor downloadExecutor, @Nullable OAuthTokenProvider authTokenProvider, DownloadMetadataStore downloadMetadataStore, ExceptionHandler exceptionHandler, Optional<Integer> defaultTrafficTag)69 public Offroad2FileDownloader( 70 Downloader downloader, 71 SynchronousFileStorage fileStorage, 72 Executor downloadExecutor, 73 @Nullable OAuthTokenProvider authTokenProvider, 74 DownloadMetadataStore downloadMetadataStore, 75 ExceptionHandler exceptionHandler, 76 // Optional<Supplier<CookieJar>> cookieJarSupplierOptional, 77 Optional<Integer> defaultTrafficTag) { 78 this.downloader = downloader; 79 this.fileStorage = fileStorage; 80 this.downloadExecutor = downloadExecutor; 81 this.authTokenProvider = authTokenProvider; 82 this.downloadMetadataStore = downloadMetadataStore; 83 this.exceptionHandler = exceptionHandler; 84 // this.cookieJarSupplierOptional = cookieJarSupplierOptional; 85 this.defaultTrafficTag = defaultTrafficTag; 86 } 87 88 @Override startDownloading( com.google.android.libraries.mobiledatadownload.downloader.DownloadRequest fileDownloaderRequest)89 public ListenableFuture<Void> startDownloading( 90 com.google.android.libraries.mobiledatadownload.downloader.DownloadRequest 91 fileDownloaderRequest) { 92 String fileName = Strings.nullToEmpty(fileDownloaderRequest.fileUri().getLastPathSegment()); 93 94 DownloadDestination downloadDestination; 95 try { 96 downloadDestination = buildDownloadDestination(fileDownloaderRequest.fileUri()); 97 } catch (DownloadException e) { 98 return Futures.immediateFailedFuture(e); 99 } 100 101 DownloadRequest offroad2DownloadRequest = 102 buildDownloadRequest(fileDownloaderRequest, downloadDestination); 103 104 FluentFuture<DownloadResult> resultFuture = downloader.execute(offroad2DownloadRequest); 105 106 LogUtil.d( 107 "%s: Data download scheduled for file: %s", TAG, 108 fileDownloaderRequest.urlToDownload()); 109 110 return PropagatedFluentFuture.from(resultFuture) 111 .catchingAsync( 112 Exception.class, 113 cause -> { 114 LogUtil.d( 115 cause, 116 "%s: Failed to download file %s due to: %s", 117 TAG, 118 fileName, 119 Strings.nullToEmpty(cause.getMessage())); 120 121 DownloadException exception = 122 exceptionHandler.mapToDownloadException("failure in download!", 123 cause); 124 125 return Futures.immediateFailedFuture(exception); 126 }, 127 downloadExecutor) 128 .transformAsync( 129 (DownloadResult result) -> { 130 LogUtil.d( 131 "%s: Downloaded file %s, bytes written: %d", 132 TAG, fileName, result.bytesWritten()); 133 return PropagatedFutures.catchingAsync( 134 downloadMetadataStore.delete(fileDownloaderRequest.fileUri()), 135 Exception.class, 136 e -> { 137 // Failing to clean up metadata shouldn't cause a failure 138 // in the future, log and 139 // return void. 140 LogUtil.d(e, "%s: Failed to cleanup metadata", TAG); 141 return Futures.immediateVoidFuture(); 142 }, 143 downloadExecutor); 144 }, 145 downloadExecutor); 146 } 147 148 @Override 149 public ListenableFuture<CheckContentChangeResponse> isContentChanged( 150 CheckContentChangeRequest checkContentChangeRequest) { 151 return Futures.immediateFailedFuture( 152 new UnsupportedOperationException( 153 "Checking for content changes is currently unsupported for Downloader2")); 154 } 155 156 private DownloadDestination buildDownloadDestination(Uri destinationUri) 157 throws DownloadException { 158 try { 159 // Create DownloadDestination using mobstore 160 // NOTE: the use of DirectExecutor here should be fine since all async operations 161 // of DownloadDestination happen within Downloader2 IOExecutor. Consider replacing 162 // this with 163 // lightweight executor. 164 return fileStorage.open( 165 destinationUri, 166 DownloadDestinationOpener.create(downloadMetadataStore)); 167 } catch (IOException e) { 168 if (e instanceof MalformedUriException 169 || e.getCause() instanceof IllegalArgumentException) { 170 LogUtil.e("%s: The file uri is invalid, uri = %s", TAG, destinationUri); 171 throw DownloadException.builder() 172 .setDownloadResultCode(DownloadResultCode.MALFORMED_FILE_URI_ERROR) 173 .setCause(e) 174 .build(); 175 } else { 176 LogUtil.e(e, "%s: Unable to create DownloadDestination for file %s", TAG, 177 destinationUri); 178 // TODO: the result code is the most equivalent to downloader1 -- consider 179 // creating a separate result code that's more appropriate for downloader2. 180 throw DownloadException.builder() 181 .setDownloadResultCode( 182 DownloadResultCode.UNABLE_TO_CREATE_MOBSTORE_RESPONSE_WRITER_ERROR) 183 .setCause(e) 184 .build(); 185 } 186 } 187 } 188 189 private DownloadRequest buildDownloadRequest( 190 com.google.android.libraries.mobiledatadownload.downloader.DownloadRequest 191 fileDownloaderRequest, 192 DownloadDestination downloadDestination) { 193 DownloadRequest.Builder requestBuilder = 194 downloader.newRequestBuilder( 195 URI.create(fileDownloaderRequest.urlToDownload()), downloadDestination); 196 197 // if (cookieJarSupplierOptional.isPresent()) { 198 // requestBuilder.setCookieJar(cookieJarSupplierOptional.get().get()); 199 // } 200 201 requestBuilder.setOAuthTokenProvider(authTokenProvider); 202 203 if (com.google.android.libraries.mobiledatadownload.downloader.DownloadConstraints 204 .NETWORK_CONNECTED 205 == fileDownloaderRequest.downloadConstraints()) { 206 requestBuilder.setDownloadConstraints(DownloadConstraints.NETWORK_CONNECTED); 207 } else { 208 // Use all network types except cellular and require unmetered network. 209 requestBuilder.setDownloadConstraints( 210 DownloadConstraints.builder() 211 .addRequiredNetworkType(NetworkType.WIFI) 212 .addRequiredNetworkType(NetworkType.ETHERNET) 213 .addRequiredNetworkType(NetworkType.BLUETOOTH) 214 .setRequireUnmeteredNetwork(true) 215 .build()); 216 } 217 218 // TODO(b/237653774): Enable traffic tagging. 219 /*if (fileDownloaderRequest.trafficTag() > 0) { 220 // Prefer traffic tag from request. 221 requestBuilder.setTrafficStatsTag(fileDownloaderRequest.trafficTag()); 222 } else if (defaultTrafficTag.isPresent() && defaultTrafficTag.get() > 0) { 223 // Use default traffic tag as a fallback if present. 224 requestBuilder.setTrafficStatsTag(defaultTrafficTag.get()); 225 }*/ 226 227 for (Pair<String, String> header : fileDownloaderRequest.extraHttpHeaders()) { 228 requestBuilder.addHeader(header.first, header.second); 229 } 230 231 return requestBuilder.build(); 232 } 233 } 234