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