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