• 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.collect.ImmutableMultimap;
21 import com.google.common.collect.ImmutableSet;
22 import com.google.common.util.concurrent.Futures;
23 import com.google.common.util.concurrent.ListenableFuture;
24 import com.google.common.util.concurrent.ListeningExecutorService;
25 import com.google.common.util.concurrent.SettableFuture;
26 import java.io.IOException;
27 import java.io.OutputStream;
28 import java.nio.channels.Channels;
29 import java.nio.channels.WritableByteChannel;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Set;
33 import okhttp3.Call;
34 import okhttp3.Callback;
35 import okhttp3.OkHttpClient;
36 import okhttp3.Request;
37 import okhttp3.Response;
38 import okhttp3.ResponseBody;
39 import okio.Okio;
40 import okio.Sink;
41 
42 /** {@link UrlEngine} implementation that uses OkHttp3 for network connectivity. */
43 public class OkHttp3UrlEngine implements UrlEngine {
44   private static final ImmutableSet<String> HTTP_SCHEMES = ImmutableSet.of("http", "https");
45 
46   private final OkHttpClient client;
47   private final ListeningExecutorService transferExecutorService;
48 
49   /**
50    * Constructs an instance of the OkHttp URL engine, for the given OkHttpClient instance.
51    *
52    * <p>Note that due to how OkHttp is implemented, reads from the network are blocking operations,
53    * and thus threads in the provided {@link ListeningExecutorService} can be tied up for long
54    * periods of time waiting on network responses. To mitigate, set {@link
55    * OkHttpClient.Builder#readTimeout(long, java.util.concurrent.TimeUnit)} to a value that is
56    * reasonable for your use case.
57    *
58    * @param transferExecutorService Executor on which the requests are synchronously executed.
59    */
OkHttp3UrlEngine(OkHttpClient client, ListeningExecutorService transferExecutorService)60   public OkHttp3UrlEngine(OkHttpClient client, ListeningExecutorService transferExecutorService) {
61     checkNotNull(client.dispatcher());
62     this.client = client;
63     this.transferExecutorService = transferExecutorService;
64   }
65 
66   @Override
createRequest(String url)67   public UrlRequest.Builder createRequest(String url) {
68     return new OkHttpUrlRequestBuilder(url);
69   }
70 
71   @Override
supportedSchemes()72   public Set<String> supportedSchemes() {
73     return HTTP_SCHEMES;
74   }
75 
76   class OkHttpUrlRequestBuilder implements UrlRequest.Builder {
77     private final String url;
78     private final ImmutableMultimap.Builder<String, String> headers = ImmutableMultimap.builder();
79 
OkHttpUrlRequestBuilder(String url)80     OkHttpUrlRequestBuilder(String url) {
81       this.url = url;
82     }
83 
84     @Override
addHeader(String key, String value)85     public UrlRequest.Builder addHeader(String key, String value) {
86       headers.put(key, value);
87       return this;
88     }
89 
90     @Override
build()91     public UrlRequest build() {
92       return new OkHttpUrlRequest(url, headers.build());
93     }
94   }
95 
96   /**
97    * Implementation of {@link UrlRequest} for OkHttp. Wraps OkHttp's {@link Call} to make network
98    * requests.
99    */
100   class OkHttpUrlRequest implements UrlRequest {
101     private final String url;
102     private final ImmutableMultimap<String, String> headers;
103 
OkHttpUrlRequest(String url, ImmutableMultimap<String, String> headers)104     OkHttpUrlRequest(String url, ImmutableMultimap<String, String> headers) {
105       this.url = url;
106       this.headers = headers;
107     }
108 
109     @Override
send()110     public ListenableFuture<UrlResponse> send() {
111       Request.Builder requestBuilder = new Request.Builder();
112 
113       try {
114         requestBuilder.url(url);
115       } catch (IllegalArgumentException e) {
116         return Futures.immediateFailedFuture(new RequestException(e));
117       }
118 
119       for (String key : headers.keys()) {
120         for (String value : headers.get(key)) {
121           requestBuilder.header(key, value);
122         }
123       }
124 
125       SettableFuture<UrlResponse> responseFuture = SettableFuture.create();
126       Call call = client.newCall(requestBuilder.build());
127       call.enqueue(
128           new Callback() {
129             @Override
130             public void onResponse(Call call, Response response) {
131               if (response.isSuccessful()) {
132                 responseFuture.set(new OkHttpUrlResponse(response));
133               } else {
134                 responseFuture.setException(
135                     new RequestException(
136                         ErrorDetails.createFromHttpErrorResponse(
137                             response.code(), response.headers().toMultimap(), response.message())));
138                 response.close();
139               }
140             }
141 
142             @Override
143             public void onFailure(Call call, IOException exception) {
144               responseFuture.setException(new RequestException(exception));
145             }
146           });
147       responseFuture.addListener(
148           () -> {
149             if (responseFuture.isCancelled()) {
150               call.cancel();
151             }
152           },
153           directExecutor());
154       return responseFuture;
155     }
156   }
157 
158   /**
159    * Implementation of {@link UrlResponse} for OkHttp. Wraps OkHttp's {@link Response} to complete
160    * its operations.
161    */
162   class OkHttpUrlResponse implements UrlResponse {
163     private final Response response;
164 
OkHttpUrlResponse(Response response)165     OkHttpUrlResponse(Response response) {
166       this.response = response;
167     }
168 
169     @Override
getResponseCode()170     public int getResponseCode() {
171       return response.code();
172     }
173 
174     @Override
getResponseHeaders()175     public Map<String, List<String>> getResponseHeaders() {
176       return response.headers().toMultimap();
177     }
178 
179     @Override
readResponseBody(WritableByteChannel destinationChannel)180     public ListenableFuture<Long> readResponseBody(WritableByteChannel destinationChannel) {
181       IOUtil.validateChannel(destinationChannel);
182       return transferExecutorService.submit(
183           () -> {
184             try (ResponseBody body = checkNotNull(response.body())) {
185               // Transfer the response body to the destination channel via OkHttp's Okio API.
186               // Sadly this needs to operate on OutputStream instead of Channels, but at least
187               // Okio manages buffers efficiently internally.
188               OutputStream outputStream = Channels.newOutputStream(destinationChannel);
189               Sink sink = Okio.sink(outputStream);
190               return body.source().readAll(sink);
191             } catch (IllegalStateException e) {
192               // OkHttp throws an IllegalStateException if the stream is closed while
193               // trying to write. Catch and rethrow.
194               throw new RequestException(e);
195             } catch (IOException e) {
196               if (e instanceof RequestException) {
197                 throw e;
198               } else {
199                 throw new RequestException(e);
200               }
201             } finally {
202               response.close();
203             }
204           });
205     }
206 
207     @Override
close()208     public void close() {
209       response.close();
210     }
211   }
212 }
213