• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.volley.cronet;
18 
19 import android.content.Context;
20 import android.text.TextUtils;
21 import android.util.Base64;
22 import androidx.annotation.Nullable;
23 import androidx.annotation.VisibleForTesting;
24 import com.android.volley.AuthFailureError;
25 import com.android.volley.Header;
26 import com.android.volley.Request;
27 import com.android.volley.RequestTask;
28 import com.android.volley.VolleyLog;
29 import com.android.volley.toolbox.AsyncHttpStack;
30 import com.android.volley.toolbox.ByteArrayPool;
31 import com.android.volley.toolbox.HttpHeaderParser;
32 import com.android.volley.toolbox.HttpResponse;
33 import com.android.volley.toolbox.PoolingByteArrayOutputStream;
34 import com.android.volley.toolbox.UrlRewriter;
35 import java.io.IOException;
36 import java.io.UnsupportedEncodingException;
37 import java.nio.ByteBuffer;
38 import java.nio.channels.Channels;
39 import java.nio.channels.WritableByteChannel;
40 import java.util.ArrayList;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.TreeMap;
44 import java.util.concurrent.Executor;
45 import java.util.concurrent.ExecutorService;
46 import org.chromium.net.CronetEngine;
47 import org.chromium.net.CronetException;
48 import org.chromium.net.UploadDataProvider;
49 import org.chromium.net.UploadDataProviders;
50 import org.chromium.net.UrlRequest;
51 import org.chromium.net.UrlRequest.Callback;
52 import org.chromium.net.UrlResponseInfo;
53 
54 /**
55  * A {@link AsyncHttpStack} that's based on Cronet's fully asynchronous API for network requests.
56  *
57  * <p><b>WARNING</b>: This API is experimental and subject to breaking changes. Please see
58  * https://github.com/google/volley/wiki/Asynchronous-Volley for more details.
59  */
60 public class CronetHttpStack extends AsyncHttpStack {
61 
62     private final CronetEngine mCronetEngine;
63     private final ByteArrayPool mPool;
64     private final UrlRewriter mUrlRewriter;
65     private final RequestListener mRequestListener;
66 
67     // cURL logging support
68     private final boolean mCurlLoggingEnabled;
69     private final CurlCommandLogger mCurlCommandLogger;
70     private final boolean mLogAuthTokensInCurlCommands;
71 
CronetHttpStack( CronetEngine cronetEngine, ByteArrayPool pool, UrlRewriter urlRewriter, RequestListener requestListener, boolean curlLoggingEnabled, CurlCommandLogger curlCommandLogger, boolean logAuthTokensInCurlCommands)72     private CronetHttpStack(
73             CronetEngine cronetEngine,
74             ByteArrayPool pool,
75             UrlRewriter urlRewriter,
76             RequestListener requestListener,
77             boolean curlLoggingEnabled,
78             CurlCommandLogger curlCommandLogger,
79             boolean logAuthTokensInCurlCommands) {
80         mCronetEngine = cronetEngine;
81         mPool = pool;
82         mUrlRewriter = urlRewriter;
83         mRequestListener = requestListener;
84         mCurlLoggingEnabled = curlLoggingEnabled;
85         mCurlCommandLogger = curlCommandLogger;
86         mLogAuthTokensInCurlCommands = logAuthTokensInCurlCommands;
87 
88         mRequestListener.initialize(this);
89     }
90 
91     @Override
executeRequest( final Request<?> request, final Map<String, String> additionalHeaders, final OnRequestComplete callback)92     public void executeRequest(
93             final Request<?> request,
94             final Map<String, String> additionalHeaders,
95             final OnRequestComplete callback) {
96         if (getBlockingExecutor() == null || getNonBlockingExecutor() == null) {
97             throw new IllegalStateException("Must set blocking and non-blocking executors");
98         }
99         final Callback urlCallback =
100                 new Callback() {
101                     PoolingByteArrayOutputStream bytesReceived = null;
102                     WritableByteChannel receiveChannel = null;
103 
104                     @Override
105                     public void onRedirectReceived(
106                             UrlRequest urlRequest,
107                             UrlResponseInfo urlResponseInfo,
108                             String newLocationUrl) {
109                         urlRequest.followRedirect();
110                     }
111 
112                     @Override
113                     public void onResponseStarted(
114                             UrlRequest urlRequest, UrlResponseInfo urlResponseInfo) {
115                         bytesReceived =
116                                 new PoolingByteArrayOutputStream(
117                                         mPool, getContentLength(urlResponseInfo));
118                         receiveChannel = Channels.newChannel(bytesReceived);
119                         urlRequest.read(ByteBuffer.allocateDirect(1024));
120                     }
121 
122                     @Override
123                     public void onReadCompleted(
124                             UrlRequest urlRequest,
125                             UrlResponseInfo urlResponseInfo,
126                             ByteBuffer byteBuffer) {
127                         byteBuffer.flip();
128                         try {
129                             receiveChannel.write(byteBuffer);
130                             byteBuffer.clear();
131                             urlRequest.read(byteBuffer);
132                         } catch (IOException e) {
133                             urlRequest.cancel();
134                             callback.onError(e);
135                         }
136                     }
137 
138                     @Override
139                     public void onSucceeded(
140                             UrlRequest urlRequest, UrlResponseInfo urlResponseInfo) {
141                         List<Header> headers = getHeaders(urlResponseInfo.getAllHeadersAsList());
142                         HttpResponse response =
143                                 new HttpResponse(
144                                         urlResponseInfo.getHttpStatusCode(),
145                                         headers,
146                                         bytesReceived.toByteArray());
147                         callback.onSuccess(response);
148                     }
149 
150                     @Override
151                     public void onFailed(
152                             UrlRequest urlRequest,
153                             UrlResponseInfo urlResponseInfo,
154                             CronetException e) {
155                         callback.onError(e);
156                     }
157                 };
158 
159         String url = request.getUrl();
160         String rewritten = mUrlRewriter.rewriteUrl(url);
161         if (rewritten == null) {
162             callback.onError(new IOException("URL blocked by rewriter: " + url));
163             return;
164         }
165         url = rewritten;
166 
167         // We can call allowDirectExecutor here and run directly on the network thread, since all
168         // the callbacks are non-blocking.
169         final UrlRequest.Builder builder =
170                 mCronetEngine
171                         .newUrlRequestBuilder(url, urlCallback, getNonBlockingExecutor())
172                         .allowDirectExecutor()
173                         .disableCache()
174                         .setPriority(getPriority(request));
175         // request.getHeaders() may be blocking, so submit it to the blocking executor.
176         getBlockingExecutor()
177                 .execute(
178                         new SetUpRequestTask<>(request, url, builder, additionalHeaders, callback));
179     }
180 
181     private class SetUpRequestTask<T> extends RequestTask<T> {
182         UrlRequest.Builder builder;
183         String url;
184         Map<String, String> additionalHeaders;
185         OnRequestComplete callback;
186         Request<T> request;
187 
SetUpRequestTask( Request<T> request, String url, UrlRequest.Builder builder, Map<String, String> additionalHeaders, OnRequestComplete callback)188         SetUpRequestTask(
189                 Request<T> request,
190                 String url,
191                 UrlRequest.Builder builder,
192                 Map<String, String> additionalHeaders,
193                 OnRequestComplete callback) {
194             super(request);
195             // Note that this URL may be different from Request#getUrl() due to the UrlRewriter.
196             this.url = url;
197             this.builder = builder;
198             this.additionalHeaders = additionalHeaders;
199             this.callback = callback;
200             this.request = request;
201         }
202 
203         @Override
run()204         public void run() {
205             try {
206                 mRequestListener.onRequestPrepared(request, builder);
207                 CurlLoggedRequestParameters requestParameters = new CurlLoggedRequestParameters();
208                 setHttpMethod(requestParameters, request);
209                 setRequestHeaders(requestParameters, request, additionalHeaders);
210                 requestParameters.applyToRequest(builder, getNonBlockingExecutor());
211                 UrlRequest urlRequest = builder.build();
212                 if (mCurlLoggingEnabled) {
213                     mCurlCommandLogger.logCurlCommand(generateCurlCommand(url, requestParameters));
214                 }
215                 urlRequest.start();
216             } catch (AuthFailureError authFailureError) {
217                 callback.onAuthError(authFailureError);
218             }
219         }
220     }
221 
222     @VisibleForTesting
getHeaders(List<Map.Entry<String, String>> headersList)223     public static List<Header> getHeaders(List<Map.Entry<String, String>> headersList) {
224         List<Header> headers = new ArrayList<>();
225         for (Map.Entry<String, String> header : headersList) {
226             headers.add(new Header(header.getKey(), header.getValue()));
227         }
228         return headers;
229     }
230 
231     /** Sets the connection parameters for the UrlRequest */
setHttpMethod(CurlLoggedRequestParameters requestParameters, Request<?> request)232     private void setHttpMethod(CurlLoggedRequestParameters requestParameters, Request<?> request)
233             throws AuthFailureError {
234         switch (request.getMethod()) {
235             case Request.Method.DEPRECATED_GET_OR_POST:
236                 // This is the deprecated way that needs to be handled for backwards compatibility.
237                 // If the request's post body is null, then the assumption is that the request is
238                 // GET.  Otherwise, it is assumed that the request is a POST.
239                 byte[] postBody = request.getPostBody();
240                 if (postBody != null) {
241                     requestParameters.setHttpMethod("POST");
242                     addBodyIfExists(requestParameters, request.getPostBodyContentType(), postBody);
243                 } else {
244                     requestParameters.setHttpMethod("GET");
245                 }
246                 break;
247             case Request.Method.GET:
248                 // Not necessary to set the request method because connection defaults to GET but
249                 // being explicit here.
250                 requestParameters.setHttpMethod("GET");
251                 break;
252             case Request.Method.DELETE:
253                 requestParameters.setHttpMethod("DELETE");
254                 break;
255             case Request.Method.POST:
256                 requestParameters.setHttpMethod("POST");
257                 addBodyIfExists(requestParameters, request.getBodyContentType(), request.getBody());
258                 break;
259             case Request.Method.PUT:
260                 requestParameters.setHttpMethod("PUT");
261                 addBodyIfExists(requestParameters, request.getBodyContentType(), request.getBody());
262                 break;
263             case Request.Method.HEAD:
264                 requestParameters.setHttpMethod("HEAD");
265                 break;
266             case Request.Method.OPTIONS:
267                 requestParameters.setHttpMethod("OPTIONS");
268                 break;
269             case Request.Method.TRACE:
270                 requestParameters.setHttpMethod("TRACE");
271                 break;
272             case Request.Method.PATCH:
273                 requestParameters.setHttpMethod("PATCH");
274                 addBodyIfExists(requestParameters, request.getBodyContentType(), request.getBody());
275                 break;
276             default:
277                 throw new IllegalStateException("Unknown method type.");
278         }
279     }
280 
281     /**
282      * Sets the request headers for the UrlRequest.
283      *
284      * @param requestParameters parameters that we are adding the request headers to
285      * @param request to get the headers from
286      * @param additionalHeaders for the UrlRequest
287      * @throws AuthFailureError is thrown if Request#getHeaders throws ones
288      */
setRequestHeaders( CurlLoggedRequestParameters requestParameters, Request<?> request, Map<String, String> additionalHeaders)289     private void setRequestHeaders(
290             CurlLoggedRequestParameters requestParameters,
291             Request<?> request,
292             Map<String, String> additionalHeaders)
293             throws AuthFailureError {
294         requestParameters.putAllHeaders(additionalHeaders);
295         // Request.getHeaders() takes precedence over the given additional (cache) headers).
296         requestParameters.putAllHeaders(request.getHeaders());
297     }
298 
299     /** Sets the UploadDataProvider of the UrlRequest.Builder */
addBodyIfExists( CurlLoggedRequestParameters requestParameters, String contentType, @Nullable byte[] body)300     private void addBodyIfExists(
301             CurlLoggedRequestParameters requestParameters,
302             String contentType,
303             @Nullable byte[] body) {
304         requestParameters.setBody(contentType, body);
305     }
306 
307     /** Helper method that maps Volley's request priority to Cronet's */
getPriority(Request<?> request)308     private int getPriority(Request<?> request) {
309         switch (request.getPriority()) {
310             case LOW:
311                 return UrlRequest.Builder.REQUEST_PRIORITY_LOW;
312             case HIGH:
313             case IMMEDIATE:
314                 return UrlRequest.Builder.REQUEST_PRIORITY_HIGHEST;
315             case NORMAL:
316             default:
317                 return UrlRequest.Builder.REQUEST_PRIORITY_MEDIUM;
318         }
319     }
320 
getContentLength(UrlResponseInfo urlResponseInfo)321     private int getContentLength(UrlResponseInfo urlResponseInfo) {
322         List<String> content = urlResponseInfo.getAllHeaders().get("Content-Length");
323         if (content == null) {
324             return 1024;
325         } else {
326             return Integer.parseInt(content.get(0));
327         }
328     }
329 
generateCurlCommand(String url, CurlLoggedRequestParameters requestParameters)330     private String generateCurlCommand(String url, CurlLoggedRequestParameters requestParameters) {
331         StringBuilder builder = new StringBuilder("curl ");
332 
333         // HTTP method
334         builder.append("-X ").append(requestParameters.getHttpMethod()).append(" ");
335 
336         // Request headers
337         for (Map.Entry<String, String> header : requestParameters.getHeaders().entrySet()) {
338             builder.append("--header \"").append(header.getKey()).append(": ");
339             if (!mLogAuthTokensInCurlCommands
340                     && ("Authorization".equals(header.getKey())
341                             || "Cookie".equals(header.getKey()))) {
342                 builder.append("[REDACTED]");
343             } else {
344                 builder.append(header.getValue());
345             }
346             builder.append("\" ");
347         }
348 
349         // URL
350         builder.append("\"").append(url).append("\"");
351 
352         // Request body (if any)
353         if (requestParameters.getBody() != null) {
354             if (requestParameters.getBody().length >= 1024) {
355                 builder.append(" [REQUEST BODY TOO LARGE TO INCLUDE]");
356             } else if (isBinaryContentForLogging(requestParameters)) {
357                 String base64 = Base64.encodeToString(requestParameters.getBody(), Base64.NO_WRAP);
358                 builder.insert(0, "echo '" + base64 + "' | base64 -d > /tmp/$$.bin; ")
359                         .append(" --data-binary @/tmp/$$.bin");
360             } else {
361                 // Just assume the request body is UTF-8 since this is for debugging.
362                 try {
363                     builder.append(" --data-ascii \"")
364                             .append(new String(requestParameters.getBody(), "UTF-8"))
365                             .append("\"");
366                 } catch (UnsupportedEncodingException e) {
367                     throw new RuntimeException("Could not encode to UTF-8", e);
368                 }
369             }
370         }
371 
372         return builder.toString();
373     }
374 
375     /** Rough heuristic to determine whether the request body is binary, for logging purposes. */
isBinaryContentForLogging(CurlLoggedRequestParameters requestParameters)376     private boolean isBinaryContentForLogging(CurlLoggedRequestParameters requestParameters) {
377         // Check to see if the content is gzip compressed - this means it should be treated as
378         // binary content regardless of the content type.
379         String contentEncoding = requestParameters.getHeaders().get("Content-Encoding");
380         if (contentEncoding != null) {
381             String[] encodings = TextUtils.split(contentEncoding, ",");
382             for (String encoding : encodings) {
383                 if ("gzip".equals(encoding.trim())) {
384                     return true;
385                 }
386             }
387         }
388 
389         // If the content type is a known text type, treat it as text content.
390         String contentType = requestParameters.getHeaders().get("Content-Type");
391         if (contentType != null) {
392             return !contentType.startsWith("text/")
393                     && !contentType.startsWith("application/xml")
394                     && !contentType.startsWith("application/json");
395         }
396 
397         // Otherwise, assume it is binary content.
398         return true;
399     }
400 
401     /**
402      * Builder is used to build an instance of {@link CronetHttpStack} from values configured by the
403      * setters.
404      */
405     public static class Builder {
406         private static final int DEFAULT_POOL_SIZE = 4096;
407         private CronetEngine mCronetEngine;
408         private final Context context;
409         private ByteArrayPool mPool;
410         private UrlRewriter mUrlRewriter;
411         private RequestListener mRequestListener;
412         private boolean mCurlLoggingEnabled;
413         private CurlCommandLogger mCurlCommandLogger;
414         private boolean mLogAuthTokensInCurlCommands;
415 
Builder(Context context)416         public Builder(Context context) {
417             this.context = context;
418         }
419 
420         /** Sets the CronetEngine to be used. Defaults to a vanialla CronetEngine. */
setCronetEngine(CronetEngine engine)421         public Builder setCronetEngine(CronetEngine engine) {
422             mCronetEngine = engine;
423             return this;
424         }
425 
426         /** Sets the ByteArrayPool to be used. Defaults to a new pool with 4096 bytes. */
setPool(ByteArrayPool pool)427         public Builder setPool(ByteArrayPool pool) {
428             mPool = pool;
429             return this;
430         }
431 
432         /** Sets the UrlRewriter to be used. Default is to return the original string. */
setUrlRewriter(UrlRewriter urlRewriter)433         public Builder setUrlRewriter(UrlRewriter urlRewriter) {
434             mUrlRewriter = urlRewriter;
435             return this;
436         }
437 
438         /** Set the optional RequestListener to be used. */
setRequestListener(RequestListener requestListener)439         public Builder setRequestListener(RequestListener requestListener) {
440             mRequestListener = requestListener;
441             return this;
442         }
443 
444         /**
445          * Sets whether cURL logging should be enabled for debugging purposes.
446          *
447          * <p>When enabled, for each request dispatched to the network, a roughly-equivalent cURL
448          * command will be logged to logcat.
449          *
450          * <p>The command may be missing some headers that are added by Cronet automatically, and
451          * the full request body may not be included if it is too large. To inspect the full
452          * requests and responses, see {@code CronetEngine#startNetLogToFile}.
453          *
454          * <p>WARNING: This is only intended for debugging purposes and should never be enabled on
455          * production devices.
456          *
457          * @see #setCurlCommandLogger(CurlCommandLogger)
458          * @see #setLogAuthTokensInCurlCommands(boolean)
459          */
setCurlLoggingEnabled(boolean curlLoggingEnabled)460         public Builder setCurlLoggingEnabled(boolean curlLoggingEnabled) {
461             mCurlLoggingEnabled = curlLoggingEnabled;
462             return this;
463         }
464 
465         /**
466          * Sets the function used to log cURL commands.
467          *
468          * <p>Allows customization of the logging performed when cURL logging is enabled.
469          *
470          * <p>By default, when cURL logging is enabled, cURL commands are logged using {@link
471          * VolleyLog#v}, e.g. at the verbose log level with the same log tag used by the rest of
472          * Volley. This function may optionally be invoked to provide a custom logger.
473          *
474          * @see #setCurlLoggingEnabled(boolean)
475          */
setCurlCommandLogger(CurlCommandLogger curlCommandLogger)476         public Builder setCurlCommandLogger(CurlCommandLogger curlCommandLogger) {
477             mCurlCommandLogger = curlCommandLogger;
478             return this;
479         }
480 
481         /**
482          * Sets whether to log known auth tokens in cURL commands, or redact them.
483          *
484          * <p>By default, headers which may contain auth tokens (e.g. Authorization or Cookie) will
485          * have their values redacted. Passing true to this method will disable this redaction and
486          * log the values of these headers.
487          *
488          * <p>This heuristic is not perfect; tokens that are logged in unknown headers, or in the
489          * request body itself, will not be redacted as they cannot be detected generically.
490          *
491          * @see #setCurlLoggingEnabled(boolean)
492          */
setLogAuthTokensInCurlCommands(boolean logAuthTokensInCurlCommands)493         public Builder setLogAuthTokensInCurlCommands(boolean logAuthTokensInCurlCommands) {
494             mLogAuthTokensInCurlCommands = logAuthTokensInCurlCommands;
495             return this;
496         }
497 
build()498         public CronetHttpStack build() {
499             if (mCronetEngine == null) {
500                 mCronetEngine = new CronetEngine.Builder(context).build();
501             }
502             if (mUrlRewriter == null) {
503                 mUrlRewriter =
504                         new UrlRewriter() {
505                             @Override
506                             public String rewriteUrl(String originalUrl) {
507                                 return originalUrl;
508                             }
509                         };
510             }
511             if (mRequestListener == null) {
512                 mRequestListener = new RequestListener() {};
513             }
514             if (mPool == null) {
515                 mPool = new ByteArrayPool(DEFAULT_POOL_SIZE);
516             }
517             if (mCurlCommandLogger == null) {
518                 mCurlCommandLogger =
519                         new CurlCommandLogger() {
520                             @Override
521                             public void logCurlCommand(String curlCommand) {
522                                 VolleyLog.v(curlCommand);
523                             }
524                         };
525             }
526             return new CronetHttpStack(
527                     mCronetEngine,
528                     mPool,
529                     mUrlRewriter,
530                     mRequestListener,
531                     mCurlLoggingEnabled,
532                     mCurlCommandLogger,
533                     mLogAuthTokensInCurlCommands);
534         }
535     }
536 
537     /** Callback interface allowing clients to intercept different parts of the request flow. */
538     public abstract static class RequestListener {
539         private CronetHttpStack mStack;
540 
initialize(CronetHttpStack stack)541         void initialize(CronetHttpStack stack) {
542             mStack = stack;
543         }
544 
545         /**
546          * Called when a request is prepared and about to be sent over the network.
547          *
548          * <p>Clients may use this callback to customize UrlRequests before they are dispatched,
549          * e.g. to enable socket tagging or request finished listeners.
550          */
onRequestPrepared(Request<?> request, UrlRequest.Builder requestBuilder)551         public void onRequestPrepared(Request<?> request, UrlRequest.Builder requestBuilder) {}
552 
553         /** @see AsyncHttpStack#getNonBlockingExecutor() */
getNonBlockingExecutor()554         protected Executor getNonBlockingExecutor() {
555             return mStack.getNonBlockingExecutor();
556         }
557 
558         /** @see AsyncHttpStack#getBlockingExecutor() */
getBlockingExecutor()559         protected Executor getBlockingExecutor() {
560             return mStack.getBlockingExecutor();
561         }
562     }
563 
564     /**
565      * Interface for logging cURL commands for requests.
566      *
567      * @see Builder#setCurlCommandLogger(CurlCommandLogger)
568      */
569     public interface CurlCommandLogger {
570         /** Log the given cURL command. */
logCurlCommand(String curlCommand)571         void logCurlCommand(String curlCommand);
572     }
573 
574     /**
575      * Internal container class for request parameters that impact logged cURL commands.
576      *
577      * <p>When cURL logging is enabled, an equivalent cURL command to a given request must be
578      * generated and logged. However, the Cronet UrlRequest object is write-only. So, we write any
579      * relevant parameters into this read-write container so they can be referenced when generating
580      * the cURL command (if needed) and then merged into the UrlRequest.
581      */
582     private static class CurlLoggedRequestParameters {
583         private final TreeMap<String, String> mHeaders =
584                 new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
585         private String mHttpMethod;
586         @Nullable private byte[] mBody;
587 
588         /**
589          * Return the headers to be used for the request.
590          *
591          * <p>The returned map is case-insensitive.
592          */
getHeaders()593         TreeMap<String, String> getHeaders() {
594             return mHeaders;
595         }
596 
597         /** Apply all the headers in the given map to the request. */
putAllHeaders(Map<String, String> headers)598         void putAllHeaders(Map<String, String> headers) {
599             mHeaders.putAll(headers);
600         }
601 
getHttpMethod()602         String getHttpMethod() {
603             return mHttpMethod;
604         }
605 
setHttpMethod(String httpMethod)606         void setHttpMethod(String httpMethod) {
607             mHttpMethod = httpMethod;
608         }
609 
610         @Nullable
getBody()611         byte[] getBody() {
612             return mBody;
613         }
614 
setBody(String contentType, @Nullable byte[] body)615         void setBody(String contentType, @Nullable byte[] body) {
616             mBody = body;
617             if (body != null && !mHeaders.containsKey(HttpHeaderParser.HEADER_CONTENT_TYPE)) {
618                 // Set the content-type unless it was already set (by Request#getHeaders).
619                 mHeaders.put(HttpHeaderParser.HEADER_CONTENT_TYPE, contentType);
620             }
621         }
622 
applyToRequest(UrlRequest.Builder builder, ExecutorService nonBlockingExecutor)623         void applyToRequest(UrlRequest.Builder builder, ExecutorService nonBlockingExecutor) {
624             for (Map.Entry<String, String> header : mHeaders.entrySet()) {
625                 builder.addHeader(header.getKey(), header.getValue());
626             }
627             builder.setHttpMethod(mHttpMethod);
628             if (mBody != null) {
629                 UploadDataProvider dataProvider = UploadDataProviders.create(mBody);
630                 builder.setUploadDataProvider(dataProvider, nonBlockingExecutor);
631             }
632         }
633     }
634 }
635