• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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