1 // Copyright 2021 The Android Open Source Project 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package com.google.android.downloader; 16 17 import static com.google.common.base.Preconditions.checkNotNull; 18 import static com.google.common.util.concurrent.MoreExecutors.directExecutor; 19 20 import com.google.common.annotations.VisibleForTesting; 21 import com.google.common.base.Strings; 22 import com.google.common.collect.ImmutableSet; 23 import com.google.common.util.concurrent.ListenableFuture; 24 import com.google.common.util.concurrent.SettableFuture; 25 import java.net.HttpURLConnection; 26 import java.nio.ByteBuffer; 27 import java.nio.channels.WritableByteChannel; 28 import java.util.List; 29 import java.util.Map; 30 import java.util.Set; 31 import java.util.concurrent.Executor; 32 import javax.annotation.Nullable; 33 import org.chromium.net.CallbackException; 34 import org.chromium.net.CronetEngine; 35 import org.chromium.net.CronetException; 36 import org.chromium.net.NetworkException; 37 import org.chromium.net.UrlResponseInfo; 38 39 /** 40 * {@link UrlEngine} implementation that uses Cronet for network connectivity. 41 * 42 * <p>Note: Internally this implementation allocates a 128kb direct byte buffer per request to 43 * transfer bytes around. If memory use is sensitive, then the number of concurrent requests should 44 * be limited. 45 */ 46 public final class CronetUrlEngine implements UrlEngine { 47 private static final ImmutableSet<String> SCHEMES = ImmutableSet.of("http", "https"); 48 @VisibleForTesting static final int BUFFER_SIZE_BYTES = 128 * 1024; // 128kb 49 50 private final CronetEngine cronetEngine; 51 private final Executor callbackExecutor; 52 53 /** 54 * Creates a new Cronet-based {@link UrlEngine}. 55 * 56 * @param cronetEngine The pre-configured {@link CronetEngine} that will be used to implement HTTP 57 * connections. 58 * @param callbackExecutor The {@link Executor} on which Cronet's callbacks will be executed. Note 59 * that this request factory implementation will perform I/O in the callbacks, so make sure 60 * the threads backing the executor can block safely (i.e. do not run on the UI thread!) 61 */ CronetUrlEngine(CronetEngine cronetEngine, Executor callbackExecutor)62 public CronetUrlEngine(CronetEngine cronetEngine, Executor callbackExecutor) { 63 this.cronetEngine = cronetEngine; 64 this.callbackExecutor = callbackExecutor; 65 } 66 67 @Override createRequest(String url)68 public UrlRequest.Builder createRequest(String url) { 69 SettableFuture<UrlResponse> responseFuture = SettableFuture.create(); 70 CronetCallback callback = new CronetCallback(responseFuture); 71 org.chromium.net.UrlRequest.Builder builder = 72 cronetEngine.newUrlRequestBuilder(url, callback, callbackExecutor); 73 return new CronetUrlRequestBuilder(builder, responseFuture); 74 } 75 76 @Override supportedSchemes()77 public Set<String> supportedSchemes() { 78 return SCHEMES; 79 } 80 81 /** Cronet-specific implementation of {@link UrlRequest} */ 82 static class CronetUrlRequest implements UrlRequest { 83 private final org.chromium.net.UrlRequest urlRequest; 84 private final ListenableFuture<UrlResponse> responseFuture; 85 CronetUrlRequest(CronetUrlRequestBuilder builder)86 CronetUrlRequest(CronetUrlRequestBuilder builder) { 87 urlRequest = builder.requestBuilder.build(); 88 responseFuture = builder.responseFuture; 89 90 responseFuture.addListener( 91 () -> { 92 if (responseFuture.isCancelled()) { 93 urlRequest.cancel(); 94 } 95 }, 96 directExecutor()); 97 } 98 99 @Override send()100 public ListenableFuture<UrlResponse> send() { 101 urlRequest.start(); 102 return responseFuture; 103 } 104 } 105 106 /** Cronet-specific implementation of {@link UrlRequest.Builder} */ 107 static class CronetUrlRequestBuilder implements UrlRequest.Builder { 108 private final org.chromium.net.UrlRequest.Builder requestBuilder; 109 private final ListenableFuture<UrlResponse> responseFuture; 110 CronetUrlRequestBuilder( org.chromium.net.UrlRequest.Builder requestBuilder, ListenableFuture<UrlResponse> responseFuture)111 CronetUrlRequestBuilder( 112 org.chromium.net.UrlRequest.Builder requestBuilder, 113 ListenableFuture<UrlResponse> responseFuture) { 114 this.requestBuilder = requestBuilder; 115 this.responseFuture = responseFuture; 116 } 117 118 @Override addHeader(String key, String value)119 public UrlRequest.Builder addHeader(String key, String value) { 120 requestBuilder.addHeader(key, value); 121 return this; 122 } 123 124 @Override build()125 public UrlRequest build() { 126 return new CronetUrlRequest(this); 127 } 128 } 129 130 /** 131 * Cronet-specific implementation of {@link UrlResponse}. Implements its functionality by using 132 * Cronet's {@link org.chromium.net.UrlRequest} and {@link UrlResponseInfo} objects. 133 */ 134 static class CronetResponse implements UrlResponse { 135 private final org.chromium.net.UrlRequest urlRequest; 136 private final UrlResponseInfo urlResponseInfo; 137 private final SettableFuture<Long> completionFuture; 138 private final CronetCallback callback; 139 CronetResponse( org.chromium.net.UrlRequest urlRequest, UrlResponseInfo urlResponseInfo, SettableFuture<Long> completionFuture, CronetCallback callback)140 CronetResponse( 141 org.chromium.net.UrlRequest urlRequest, 142 UrlResponseInfo urlResponseInfo, 143 SettableFuture<Long> completionFuture, 144 CronetCallback callback) { 145 this.urlRequest = urlRequest; 146 this.urlResponseInfo = urlResponseInfo; 147 this.completionFuture = completionFuture; 148 this.callback = callback; 149 } 150 151 @Override getResponseCode()152 public int getResponseCode() { 153 return urlResponseInfo.getHttpStatusCode(); 154 } 155 156 @Override getResponseHeaders()157 public Map<String, List<String>> getResponseHeaders() { 158 return urlResponseInfo.getAllHeaders(); 159 } 160 161 @Override readResponseBody(WritableByteChannel destinationChannel)162 public ListenableFuture<Long> readResponseBody(WritableByteChannel destinationChannel) { 163 IOUtil.validateChannel(destinationChannel); 164 callback.destinationChannel = destinationChannel; 165 urlRequest.read(ByteBuffer.allocateDirect(BUFFER_SIZE_BYTES)); 166 return completionFuture; 167 } 168 169 @Override close()170 public void close() { 171 urlRequest.cancel(); 172 } 173 } 174 175 /** 176 * Implementation of {@link org.chromium.net.UrlRequest.Callback} to handle the lifecycle of a 177 * Cronet url request. The operations of handling the response metadata returned by the server as 178 * well as actually reading the response body happen here. 179 */ 180 static class CronetCallback extends org.chromium.net.UrlRequest.Callback { 181 private final SettableFuture<UrlResponse> responseFuture; 182 private final SettableFuture<Long> completionFuture = SettableFuture.create(); 183 184 @Nullable private CronetResponse cronetResponse; 185 @Nullable private WritableByteChannel destinationChannel; 186 private long numBytesWritten; 187 CronetCallback(SettableFuture<UrlResponse> responseFuture)188 CronetCallback(SettableFuture<UrlResponse> responseFuture) { 189 this.responseFuture = responseFuture; 190 } 191 192 @Override onRedirectReceived( org.chromium.net.UrlRequest urlRequest, UrlResponseInfo urlResponseInfo, String newLocationUrl)193 public void onRedirectReceived( 194 org.chromium.net.UrlRequest urlRequest, 195 UrlResponseInfo urlResponseInfo, 196 String newLocationUrl) { 197 // Just blindly follow redirects; that's pretty much always what you want to do. 198 urlRequest.followRedirect(); 199 } 200 201 @Override onResponseStarted( org.chromium.net.UrlRequest urlRequest, UrlResponseInfo urlResponseInfo)202 public void onResponseStarted( 203 org.chromium.net.UrlRequest urlRequest, UrlResponseInfo urlResponseInfo) { 204 // We've received the response metadata from the server, so we have a status code and 205 // response headers to examine. At this point we can create a response object and complete 206 // the response future. If necessary, the body itself will be downloaded via a subsequent 207 // call urlRequest.read inside the CronetResponse.writeResponseBody, which will trigger the 208 // other lifecycle callbacks. 209 int httpCode = urlResponseInfo.getHttpStatusCode(); 210 if (httpCode >= HttpURLConnection.HTTP_BAD_REQUEST) { 211 responseFuture.setException( 212 new RequestException( 213 ErrorDetails.createFromHttpErrorResponse( 214 httpCode, 215 urlResponseInfo.getAllHeaders(), 216 urlResponseInfo.getHttpStatusText()))); 217 urlRequest.cancel(); 218 } else { 219 cronetResponse = new CronetResponse(urlRequest, urlResponseInfo, completionFuture, this); 220 responseFuture.set(cronetResponse); 221 } 222 } 223 224 @Override onReadCompleted( org.chromium.net.UrlRequest urlRequest, UrlResponseInfo urlResponseInfo, ByteBuffer byteBuffer)225 public void onReadCompleted( 226 org.chromium.net.UrlRequest urlRequest, 227 UrlResponseInfo urlResponseInfo, 228 ByteBuffer byteBuffer) 229 throws Exception { 230 // If we're already done, just bail out. 231 if (urlRequest.isDone()) { 232 return; 233 } 234 235 // If the underlying future has been cancelled, cancel the request and abort. 236 if (completionFuture.isCancelled()) { 237 urlRequest.cancel(); 238 return; 239 } 240 241 // Flip the buffer to prepare for reading from it. 242 byteBuffer.flip(); 243 244 // Write however many bytes are in our buffer to the underlying channel. 245 numBytesWritten += IOUtil.blockingWrite(byteBuffer, checkNotNull(destinationChannel)); 246 247 // Reset the buffer to be reused on the next iteration. 248 byteBuffer.clear(); 249 250 // Finally, request more bytes. This is necessary per the Cronet API. 251 urlRequest.read(byteBuffer); 252 } 253 254 @Override onSucceeded( org.chromium.net.UrlRequest urlRequest, UrlResponseInfo urlResponseInfo)255 public void onSucceeded( 256 org.chromium.net.UrlRequest urlRequest, UrlResponseInfo urlResponseInfo) { 257 // The body has been successfully streamed. Close the underlying response object to free 258 // up resources it holds, and resolve the pending future with the number of bytes written. 259 closeResponse(); 260 completionFuture.set(numBytesWritten); 261 } 262 263 @Override onFailed( org.chromium.net.UrlRequest urlRequest, UrlResponseInfo urlResponseInfo, CronetException exception)264 public void onFailed( 265 org.chromium.net.UrlRequest urlRequest, 266 UrlResponseInfo urlResponseInfo, 267 CronetException exception) { 268 // There was some sort of error with the connection. Clean up and resolve the pending future 269 // with the exception we encountered. 270 closeResponse(); 271 272 ErrorDetails errorDetails; 273 if (urlResponseInfo != null 274 && urlResponseInfo.getHttpStatusCode() >= HttpURLConnection.HTTP_BAD_REQUEST) { 275 errorDetails = 276 ErrorDetails.createFromHttpErrorResponse( 277 urlResponseInfo.getHttpStatusCode(), 278 urlResponseInfo.getAllHeaders(), 279 urlResponseInfo.getHttpStatusText()); 280 } else if (exception instanceof NetworkException) { 281 NetworkException networkException = (NetworkException) exception; 282 errorDetails = 283 ErrorDetails.builder() 284 .setInternalErrorCode(networkException.getCronetInternalErrorCode()) 285 .setErrorMessage(Strings.nullToEmpty(networkException.getMessage())) 286 .build(); 287 } else { 288 errorDetails = 289 ErrorDetails.builder() 290 .setErrorMessage(Strings.nullToEmpty(exception.getMessage())) 291 .build(); 292 } 293 294 RequestException requestException = 295 new RequestException(errorDetails, unwrapException(exception)); 296 297 if (!responseFuture.isDone()) { 298 responseFuture.setException(requestException); 299 } else { 300 // N.B: The completion future is available iff the response future is resolved, so 301 // we don't need to resolve it with an exception here unless the response future is done. 302 completionFuture.setException(requestException); 303 } 304 } 305 306 @Override onCanceled( org.chromium.net.UrlRequest urlRequest, UrlResponseInfo urlResponseInfo)307 public void onCanceled( 308 org.chromium.net.UrlRequest urlRequest, UrlResponseInfo urlResponseInfo) { 309 // The request was cancelled. This only occurs when UrlRequest.cancel is called, which 310 // in turn only happens when UrlResponse.close is called. Clean up internal state 311 // and resolve the future with an error. 312 closeResponse(); 313 completionFuture.setException(new RequestException("UrlRequest cancelled")); 314 } 315 316 /** Safely closes the current response object, if any. */ closeResponse()317 private void closeResponse() { 318 CronetResponse cronetResponse = this.cronetResponse; 319 if (cronetResponse == null) { 320 return; 321 } 322 cronetResponse.close(); 323 } 324 } 325 unwrapException(CronetException exception)326 private static Throwable unwrapException(CronetException exception) { 327 // CallbackExceptions aren't interesting, so unwrap them. 328 if (exception instanceof CallbackException) { 329 Throwable cause = exception.getCause(); 330 return cause == null ? exception : cause; 331 } 332 return exception; 333 } 334 } 335