1 // Copyright 2014 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.net.impl; 6 7 import static java.lang.Math.max; 8 import static org.chromium.net.UrlRequest.Builder.REQUEST_PRIORITY_IDLE; 9 import static org.chromium.net.UrlRequest.Builder.REQUEST_PRIORITY_LOWEST; 10 import static org.chromium.net.UrlRequest.Builder.REQUEST_PRIORITY_LOW; 11 import static org.chromium.net.UrlRequest.Builder.REQUEST_PRIORITY_MEDIUM; 12 import static org.chromium.net.UrlRequest.Builder.REQUEST_PRIORITY_HIGHEST; 13 14 import android.os.Build; 15 16 import androidx.annotation.RequiresApi; 17 import androidx.annotation.VisibleForTesting; 18 19 import org.jni_zero.CalledByNative; 20 import org.jni_zero.JNINamespace; 21 import org.jni_zero.NativeClassQualifiedName; 22 import org.jni_zero.NativeMethods; 23 24 import org.chromium.base.Log; 25 import org.chromium.net.CallbackException; 26 import org.chromium.net.CronetException; 27 import org.chromium.net.Idempotency; 28 import org.chromium.net.InlineExecutionProhibitedException; 29 import org.chromium.net.NetworkException; 30 import org.chromium.net.RequestFinishedInfo; 31 import org.chromium.net.RequestPriority; 32 import org.chromium.net.UploadDataProvider; 33 import org.chromium.net.UrlRequest; 34 import org.chromium.net.UrlResponseInfo.HeaderBlock; 35 import org.chromium.net.impl.CronetLogger.CronetTrafficInfo; 36 37 import java.nio.ByteBuffer; 38 import java.time.Duration; 39 import java.util.AbstractMap; 40 import java.util.ArrayList; 41 import java.util.Collection; 42 import java.util.Collections; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.Objects; 46 import java.util.concurrent.Executor; 47 import java.util.concurrent.RejectedExecutionException; 48 49 import javax.annotation.concurrent.GuardedBy; 50 51 /** 52 * UrlRequest using Chromium HTTP stack implementation. Could be accessed from 53 * any thread on Executor. Cancel can be called from any thread. 54 * All @CallByNative methods are called on native network thread 55 * and post tasks with listener calls onto Executor. Upon return from listener 56 * callback native request adapter is called on executive thread and posts 57 * native tasks to native network thread. Because Cancel could be called from 58 * any thread it is protected by mUrlRequestAdapterLock. 59 */ 60 @JNINamespace("cronet") 61 @VisibleForTesting 62 public final class CronetUrlRequest extends UrlRequestBase { 63 private final boolean mAllowDirectExecutor; 64 65 /* Native adapter object, owned by UrlRequest. */ 66 @GuardedBy("mUrlRequestAdapterLock") 67 private long mUrlRequestAdapter; 68 69 @GuardedBy("mUrlRequestAdapterLock") 70 private boolean mStarted; 71 72 @GuardedBy("mUrlRequestAdapterLock") 73 private boolean mWaitingOnRedirect; 74 75 @GuardedBy("mUrlRequestAdapterLock") 76 private boolean mWaitingOnRead; 77 78 /* 79 * Synchronize access to mUrlRequestAdapter, mStarted, mWaitingOnRedirect, 80 * and mWaitingOnRead. 81 */ 82 private final Object mUrlRequestAdapterLock = new Object(); 83 private final CronetUrlRequestContext mRequestContext; 84 private final Executor mExecutor; 85 86 /* 87 * URL chain contains the URL currently being requested, and 88 * all URLs previously requested. New URLs are added before 89 * mCallback.onRedirectReceived is called. 90 */ 91 private final List<String> mUrlChain = new ArrayList<String>(); 92 93 private final VersionSafeCallbacks.UrlRequestCallback mCallback; 94 private final String mInitialUrl; 95 private final int mPriority; 96 private final int mIdempotency; 97 private String mInitialMethod; 98 private final HeadersList mRequestHeaders = new HeadersList(); 99 private final Collection<Object> mRequestAnnotations; 100 private final boolean mDisableCache; 101 private final boolean mDisableConnectionMigration; 102 private final boolean mTrafficStatsTagSet; 103 private final int mTrafficStatsTag; 104 private final boolean mTrafficStatsUidSet; 105 private final int mTrafficStatsUid; 106 private final VersionSafeCallbacks.RequestFinishedInfoListener mRequestFinishedListener; 107 private final long mNetworkHandle; 108 private final int mCronetEngineId; 109 private final CronetLogger mLogger; 110 111 private CronetUploadDataStream mUploadDataStream; 112 113 private UrlResponseInfoImpl mResponseInfo; 114 115 // These three should only be updated once with mUrlRequestAdapterLock held. They are read on 116 // UrlRequest.Callback's and RequestFinishedInfo.Listener's executors after the last update. 117 @RequestFinishedInfoImpl.FinishedReason private int mFinishedReason; 118 private CronetException mException; 119 private CronetMetrics mMetrics; 120 private boolean mQuicConnectionMigrationAttempted; 121 private boolean mQuicConnectionMigrationSuccessful; 122 123 /* 124 * Listener callback is repeatedly invoked when each read is completed, so it 125 * is cached as a member variable. 126 */ 127 private OnReadCompletedRunnable mOnReadCompletedTask; 128 129 @GuardedBy("mUrlRequestAdapterLock") 130 private Runnable mOnDestroyedCallbackForTesting; 131 132 @VisibleForTesting 133 public static final class HeadersList extends ArrayList<Map.Entry<String, String>> {} 134 135 private final class OnReadCompletedRunnable implements Runnable { 136 // Buffer passed back from current invocation of onReadCompleted. 137 ByteBuffer mByteBuffer; 138 139 @Override run()140 public void run() { 141 checkCallingThread(); 142 // Null out mByteBuffer, to pass buffer ownership to callback or release if done. 143 ByteBuffer buffer = mByteBuffer; 144 mByteBuffer = null; 145 146 try { 147 synchronized (mUrlRequestAdapterLock) { 148 if (isDoneLocked()) { 149 return; 150 } 151 mWaitingOnRead = true; 152 } 153 mCallback.onReadCompleted(CronetUrlRequest.this, mResponseInfo, buffer); 154 } catch (Exception e) { 155 onCallbackException(e); 156 } 157 } 158 } 159 CronetUrlRequest( CronetUrlRequestContext requestContext, String url, int priority, UrlRequest.Callback callback, Executor executor, Collection<Object> requestAnnotations, boolean disableCache, boolean disableConnectionMigration, boolean allowDirectExecutor, boolean trafficStatsTagSet, int trafficStatsTag, boolean trafficStatsUidSet, int trafficStatsUid, RequestFinishedInfo.Listener requestFinishedListener, int idempotency, long networkHandle)160 CronetUrlRequest( 161 CronetUrlRequestContext requestContext, 162 String url, 163 int priority, 164 UrlRequest.Callback callback, 165 Executor executor, 166 Collection<Object> requestAnnotations, 167 boolean disableCache, 168 boolean disableConnectionMigration, 169 boolean allowDirectExecutor, 170 boolean trafficStatsTagSet, 171 int trafficStatsTag, 172 boolean trafficStatsUidSet, 173 int trafficStatsUid, 174 RequestFinishedInfo.Listener requestFinishedListener, 175 int idempotency, 176 long networkHandle) { 177 Objects.requireNonNull(url, "URL is required"); 178 Objects.requireNonNull(callback, "Listener is required"); 179 Objects.requireNonNull(executor, "Executor is required"); 180 181 mAllowDirectExecutor = allowDirectExecutor; 182 mRequestContext = requestContext; 183 mCronetEngineId = requestContext.getCronetEngineId(); 184 mLogger = requestContext.getCronetLogger(); 185 mInitialUrl = url; 186 mUrlChain.add(url); 187 mPriority = convertRequestPriority(priority); 188 mCallback = new VersionSafeCallbacks.UrlRequestCallback(callback); 189 mExecutor = executor; 190 mRequestAnnotations = requestAnnotations; 191 mDisableCache = disableCache; 192 mDisableConnectionMigration = disableConnectionMigration; 193 mTrafficStatsTagSet = trafficStatsTagSet; 194 mTrafficStatsTag = trafficStatsTag; 195 mTrafficStatsUidSet = trafficStatsUidSet; 196 mTrafficStatsUid = trafficStatsUid; 197 mRequestFinishedListener = 198 requestFinishedListener != null 199 ? new VersionSafeCallbacks.RequestFinishedInfoListener( 200 requestFinishedListener) 201 : null; 202 mIdempotency = convertIdempotency(idempotency); 203 mNetworkHandle = networkHandle; 204 } 205 206 @Override setHttpMethod(String method)207 public void setHttpMethod(String method) { 208 checkNotStarted(); 209 Objects.requireNonNull(method, "Method is required."); 210 mInitialMethod = method; 211 } 212 213 @Override addHeader(String header, String value)214 public void addHeader(String header, String value) { 215 checkNotStarted(); 216 Objects.requireNonNull(header, "Invalid header name."); 217 Objects.requireNonNull(value, "Invalid header value."); 218 mRequestHeaders.add(new AbstractMap.SimpleImmutableEntry<String, String>(header, value)); 219 } 220 221 @Override setUploadDataProvider(UploadDataProvider uploadDataProvider, Executor executor)222 public void setUploadDataProvider(UploadDataProvider uploadDataProvider, Executor executor) { 223 Objects.requireNonNull(uploadDataProvider, "Invalid UploadDataProvider."); 224 if (mInitialMethod == null) { 225 mInitialMethod = "POST"; 226 } 227 mUploadDataStream = new CronetUploadDataStream(uploadDataProvider, executor, this); 228 } 229 230 @Override getHttpMethod()231 public String getHttpMethod() { 232 return mInitialMethod; 233 } 234 235 @Override isDirectExecutorAllowed()236 public boolean isDirectExecutorAllowed() { 237 return mAllowDirectExecutor; 238 } 239 240 @Override isCacheDisabled()241 public boolean isCacheDisabled() { 242 return mDisableCache; 243 } 244 245 @Override hasTrafficStatsTag()246 public boolean hasTrafficStatsTag() { 247 return mTrafficStatsTagSet; 248 } 249 250 @Override getTrafficStatsTag()251 public int getTrafficStatsTag() { 252 if (!hasTrafficStatsTag()) { 253 throw new IllegalStateException("TrafficStatsTag is not set"); 254 } 255 return mTrafficStatsTag; 256 } 257 258 @Override hasTrafficStatsUid()259 public boolean hasTrafficStatsUid() { 260 return mTrafficStatsUidSet; 261 } 262 263 @Override getTrafficStatsUid()264 public int getTrafficStatsUid() { 265 if (!hasTrafficStatsUid()) { 266 throw new IllegalStateException("TrafficStatsUid is not set"); 267 } 268 return mTrafficStatsUid; 269 } 270 @Override getPriority()271 public int getPriority() { 272 switch (mPriority) { 273 case RequestPriority.IDLE: 274 return REQUEST_PRIORITY_IDLE; 275 case RequestPriority.LOWEST: 276 return REQUEST_PRIORITY_LOWEST; 277 case RequestPriority.LOW: 278 return REQUEST_PRIORITY_LOW; 279 case RequestPriority.MEDIUM: 280 return REQUEST_PRIORITY_MEDIUM; 281 case RequestPriority.HIGHEST: 282 return REQUEST_PRIORITY_HIGHEST; 283 default: 284 throw new IllegalStateException("Invalid request priority: " + mPriority); 285 } 286 } 287 288 @Override getHeaders()289 public HeaderBlock getHeaders() { 290 return new UrlResponseInfoImpl.HeaderBlockImpl(mRequestHeaders); 291 } 292 293 @Override start()294 public void start() { 295 synchronized (mUrlRequestAdapterLock) { 296 checkNotStarted(); 297 298 try { 299 mUrlRequestAdapter = 300 CronetUrlRequestJni.get() 301 .createRequestAdapter( 302 CronetUrlRequest.this, 303 mRequestContext.getUrlRequestContextAdapter(), 304 mInitialUrl, 305 mPriority, 306 mDisableCache, 307 mDisableConnectionMigration, 308 mTrafficStatsTagSet, 309 mTrafficStatsTag, 310 mTrafficStatsUidSet, 311 mTrafficStatsUid, 312 mIdempotency, 313 mNetworkHandle); 314 mRequestContext.onRequestStarted(); 315 if (mInitialMethod != null) { 316 if (!CronetUrlRequestJni.get() 317 .setHttpMethod( 318 mUrlRequestAdapter, CronetUrlRequest.this, mInitialMethod)) { 319 throw new IllegalArgumentException("Invalid http method " + mInitialMethod); 320 } 321 } 322 323 boolean hasContentType = false; 324 for (Map.Entry<String, String> header : mRequestHeaders) { 325 if (header.getKey().equalsIgnoreCase("Content-Type") 326 && !header.getValue().isEmpty()) { 327 hasContentType = true; 328 } 329 if (!CronetUrlRequestJni.get() 330 .addRequestHeader( 331 mUrlRequestAdapter, 332 CronetUrlRequest.this, 333 header.getKey(), 334 header.getValue())) { 335 throw new IllegalArgumentException( 336 "Invalid header with headername: " + header.getKey()); 337 } 338 } 339 if (mUploadDataStream != null) { 340 if (!hasContentType) { 341 throw new IllegalArgumentException( 342 "Requests with upload data must have a Content-Type."); 343 } 344 mStarted = true; 345 mUploadDataStream.postTaskToExecutor( 346 new Runnable() { 347 @Override 348 public void run() { 349 mUploadDataStream.initializeWithRequest(); 350 synchronized (mUrlRequestAdapterLock) { 351 if (isDoneLocked()) { 352 return; 353 } 354 mUploadDataStream.attachNativeAdapterToRequest( 355 mUrlRequestAdapter); 356 startInternalLocked(); 357 } 358 } 359 }); 360 return; 361 } 362 } catch (RuntimeException e) { 363 // If there's an exception, cleanup and then throw the exception to the caller. 364 // start() is synchronized so we do not acquire mUrlRequestAdapterLock here. 365 destroyRequestAdapterLocked(RequestFinishedInfo.FAILED); 366 mRequestContext.onRequestFinished(); 367 throw e; 368 } 369 mStarted = true; 370 startInternalLocked(); 371 } 372 } 373 374 /* 375 * Starts fully configured request. Could execute on UploadDataProvider executor. 376 * Caller is expected to ensure that request isn't canceled and mUrlRequestAdapter is valid. 377 */ 378 @GuardedBy("mUrlRequestAdapterLock") startInternalLocked()379 private void startInternalLocked() { 380 CronetUrlRequestJni.get().start(mUrlRequestAdapter, CronetUrlRequest.this); 381 } 382 383 @Override followRedirect()384 public void followRedirect() { 385 synchronized (mUrlRequestAdapterLock) { 386 if (!mWaitingOnRedirect) { 387 throw new IllegalStateException("No redirect to follow."); 388 } 389 mWaitingOnRedirect = false; 390 391 if (isDoneLocked()) { 392 return; 393 } 394 395 CronetUrlRequestJni.get() 396 .followDeferredRedirect(mUrlRequestAdapter, CronetUrlRequest.this); 397 } 398 } 399 400 @Override read(ByteBuffer buffer)401 public void read(ByteBuffer buffer) { 402 Preconditions.checkHasRemaining(buffer); 403 Preconditions.checkDirect(buffer); 404 synchronized (mUrlRequestAdapterLock) { 405 if (!mWaitingOnRead) { 406 throw new IllegalStateException("Unexpected read attempt."); 407 } 408 mWaitingOnRead = false; 409 410 if (isDoneLocked()) { 411 return; 412 } 413 414 if (!CronetUrlRequestJni.get() 415 .readData( 416 mUrlRequestAdapter, 417 CronetUrlRequest.this, 418 buffer, 419 buffer.position(), 420 buffer.limit())) { 421 // Still waiting on read. This is just to have consistent 422 // behavior with the other error cases. 423 mWaitingOnRead = true; 424 throw new IllegalArgumentException("Unable to call native read"); 425 } 426 } 427 } 428 429 @Override cancel()430 public void cancel() { 431 synchronized (mUrlRequestAdapterLock) { 432 if (isDoneLocked() || !mStarted) { 433 return; 434 } 435 destroyRequestAdapterLocked(RequestFinishedInfo.CANCELED); 436 } 437 } 438 439 @Override isDone()440 public boolean isDone() { 441 synchronized (mUrlRequestAdapterLock) { 442 return isDoneLocked(); 443 } 444 } 445 446 @GuardedBy("mUrlRequestAdapterLock") isDoneLocked()447 private boolean isDoneLocked() { 448 return mStarted && mUrlRequestAdapter == 0; 449 } 450 451 @Override getStatus(UrlRequest.StatusListener unsafeListener)452 public void getStatus(UrlRequest.StatusListener unsafeListener) { 453 final VersionSafeCallbacks.UrlRequestStatusListener listener = 454 new VersionSafeCallbacks.UrlRequestStatusListener(unsafeListener); 455 synchronized (mUrlRequestAdapterLock) { 456 if (mUrlRequestAdapter != 0) { 457 CronetUrlRequestJni.get() 458 .getStatus(mUrlRequestAdapter, CronetUrlRequest.this, listener); 459 return; 460 } 461 } 462 Runnable task = 463 new Runnable() { 464 @Override 465 public void run() { 466 listener.onStatus(UrlRequest.Status.INVALID); 467 } 468 }; 469 postTaskToExecutor(task); 470 } 471 setOnDestroyedCallbackForTesting(Runnable onDestroyedCallbackForTesting)472 public void setOnDestroyedCallbackForTesting(Runnable onDestroyedCallbackForTesting) { 473 synchronized (mUrlRequestAdapterLock) { 474 mOnDestroyedCallbackForTesting = onDestroyedCallbackForTesting; 475 } 476 } 477 setOnDestroyedUploadCallbackForTesting( Runnable onDestroyedUploadCallbackForTesting)478 public void setOnDestroyedUploadCallbackForTesting( 479 Runnable onDestroyedUploadCallbackForTesting) { 480 mUploadDataStream.setOnDestroyedCallbackForTesting(onDestroyedUploadCallbackForTesting); 481 } 482 getUrlRequestAdapterForTesting()483 public long getUrlRequestAdapterForTesting() { 484 synchronized (mUrlRequestAdapterLock) { 485 return mUrlRequestAdapter; 486 } 487 } 488 489 /** 490 * Posts task to application Executor. Used for Listener callbacks 491 * and other tasks that should not be executed on network thread. 492 */ postTaskToExecutor(Runnable task)493 private void postTaskToExecutor(Runnable task) { 494 try { 495 mExecutor.execute(task); 496 } catch (RejectedExecutionException failException) { 497 Log.e( 498 CronetUrlRequestContext.LOG_TAG, 499 "Exception posting task to executor", 500 failException); 501 // If posting a task throws an exception, then we fail the request. This exception could 502 // be permanent (executor shutdown), transient (AbortPolicy, or CallerRunsPolicy with 503 // direct execution not permitted), or caused by the runnables we submit if 504 // mUserExecutor is a direct executor and direct execution is not permitted. In the 505 // latter two cases, there is at least have a chance to inform the embedder of the 506 // request's failure, since failWithException does not enforce that onFailed() is not 507 // executed inline. 508 failWithException( 509 new CronetExceptionImpl("Exception posting task to executor", failException)); 510 } 511 } 512 convertRequestPriority(int priority)513 private static int convertRequestPriority(int priority) { 514 switch (priority) { 515 case Builder.REQUEST_PRIORITY_IDLE: 516 return RequestPriority.IDLE; 517 case Builder.REQUEST_PRIORITY_LOWEST: 518 return RequestPriority.LOWEST; 519 case Builder.REQUEST_PRIORITY_LOW: 520 return RequestPriority.LOW; 521 case Builder.REQUEST_PRIORITY_MEDIUM: 522 return RequestPriority.MEDIUM; 523 case Builder.REQUEST_PRIORITY_HIGHEST: 524 return RequestPriority.HIGHEST; 525 default: 526 return RequestPriority.MEDIUM; 527 } 528 } 529 convertIdempotency(int idempotency)530 private static int convertIdempotency(int idempotency) { 531 switch (idempotency) { 532 case Builder.DEFAULT_IDEMPOTENCY: 533 return Idempotency.DEFAULT_IDEMPOTENCY; 534 case Builder.IDEMPOTENT: 535 return Idempotency.IDEMPOTENT; 536 case Builder.NOT_IDEMPOTENT: 537 return Idempotency.NOT_IDEMPOTENT; 538 default: 539 return Idempotency.DEFAULT_IDEMPOTENCY; 540 } 541 } 542 543 /** 544 * Estimates the byte size of the headers in their on-wire format. 545 * We are not really interested in their specific size but something which is close enough. 546 */ 547 @VisibleForTesting estimateHeadersSizeInBytes(Map<String, List<String>> headers)548 public static long estimateHeadersSizeInBytes(Map<String, List<String>> headers) { 549 if (headers == null) return 0; 550 551 long responseHeaderSizeInBytes = 0; 552 for (Map.Entry<String, List<String>> entry : headers.entrySet()) { 553 String key = entry.getKey(); 554 if (key != null) responseHeaderSizeInBytes += key.length(); 555 if (entry.getValue() == null) continue; 556 557 for (String content : entry.getValue()) { 558 responseHeaderSizeInBytes += content.length(); 559 } 560 } 561 return responseHeaderSizeInBytes; 562 } 563 564 /** 565 * Estimates the byte size of the headers in their on-wire format. 566 * We are not really interested in their specific size but something which is close enough. 567 */ 568 @VisibleForTesting estimateHeadersSizeInBytes(HeadersList headers)569 public static long estimateHeadersSizeInBytes(HeadersList headers) { 570 if (headers == null) return 0; 571 long responseHeaderSizeInBytes = 0; 572 for (Map.Entry<String, String> entry : headers) { 573 String key = entry.getKey(); 574 if (key != null) responseHeaderSizeInBytes += key.length(); 575 String value = entry.getValue(); 576 if (value != null) responseHeaderSizeInBytes += entry.getValue().length(); 577 } 578 return responseHeaderSizeInBytes; 579 } 580 prepareResponseInfoOnNetworkThread( int httpStatusCode, String httpStatusText, String[] headers, boolean wasCached, String negotiatedProtocol, String proxyServer, long receivedByteCount)581 private UrlResponseInfoImpl prepareResponseInfoOnNetworkThread( 582 int httpStatusCode, 583 String httpStatusText, 584 String[] headers, 585 boolean wasCached, 586 String negotiatedProtocol, 587 String proxyServer, 588 long receivedByteCount) { 589 HeadersList headersList = new HeadersList(); 590 for (int i = 0; i < headers.length; i += 2) { 591 headersList.add( 592 new AbstractMap.SimpleImmutableEntry<String, String>( 593 headers[i], headers[i + 1])); 594 } 595 return new UrlResponseInfoImpl( 596 new ArrayList<String>(mUrlChain), 597 httpStatusCode, 598 httpStatusText, 599 headersList, 600 wasCached, 601 negotiatedProtocol, 602 proxyServer, 603 receivedByteCount); 604 } 605 checkNotStarted()606 private void checkNotStarted() { 607 synchronized (mUrlRequestAdapterLock) { 608 if (mStarted || isDoneLocked()) { 609 throw new IllegalStateException("Request is already started."); 610 } 611 } 612 } 613 614 /** 615 * Helper method to set final status of CronetUrlRequest and clean up the 616 * native request adapter. 617 */ 618 @GuardedBy("mUrlRequestAdapterLock") destroyRequestAdapterLocked( @equestFinishedInfoImpl.FinishedReason int finishedReason)619 private void destroyRequestAdapterLocked( 620 @RequestFinishedInfoImpl.FinishedReason int finishedReason) { 621 assert mException == null || finishedReason == RequestFinishedInfo.FAILED; 622 mFinishedReason = finishedReason; 623 if (mUrlRequestAdapter == 0) { 624 return; 625 } 626 mRequestContext.onRequestDestroyed(); 627 // Posts a task to destroy the native adapter. 628 CronetUrlRequestJni.get() 629 .destroy( 630 mUrlRequestAdapter, 631 CronetUrlRequest.this, 632 finishedReason == RequestFinishedInfo.CANCELED); 633 mUrlRequestAdapter = 0; 634 } 635 636 /** 637 * If callback method throws an exception, request gets canceled 638 * and exception is reported via onFailed listener callback. 639 * Only called on the Executor. 640 */ onCallbackException(Exception e)641 private void onCallbackException(Exception e) { 642 CallbackException requestError = 643 new CallbackExceptionImpl("Exception received from UrlRequest.Callback", e); 644 Log.e(CronetUrlRequestContext.LOG_TAG, "Exception in CalledByNative method", e); 645 failWithException(requestError); 646 } 647 648 /** Called when UploadDataProvider encounters an error. */ onUploadException(Throwable e)649 void onUploadException(Throwable e) { 650 CallbackException uploadError = 651 new CallbackExceptionImpl("Exception received from UploadDataProvider", e); 652 Log.e(CronetUrlRequestContext.LOG_TAG, "Exception in upload method", e); 653 failWithException(uploadError); 654 } 655 656 /** Fails the request with an exception on any thread. */ failWithException(final CronetException exception)657 private void failWithException(final CronetException exception) { 658 synchronized (mUrlRequestAdapterLock) { 659 if (isDoneLocked()) { 660 return; 661 } 662 assert mException == null; 663 mException = exception; 664 destroyRequestAdapterLocked(RequestFinishedInfo.FAILED); 665 } 666 // onFailed will be invoked from onNativeAdapterDestroyed() to ensure metrics collection. 667 } 668 669 //////////////////////////////////////////////// 670 // Private methods called by the native code. 671 // Always called on network thread. 672 //////////////////////////////////////////////// 673 674 /** 675 * Called before following redirects. The redirect will only be followed if 676 * {@link #followRedirect()} is called. If the redirect response has a body, it will be ignored. 677 * This will only be called between start and onResponseStarted. 678 * 679 * @param newLocation Location where request is redirected. 680 * @param httpStatusCode from redirect response 681 * @param receivedByteCount count of bytes received for redirect response 682 * @param headers an array of response headers with keys at the even indices 683 * followed by the corresponding values at the odd indices. 684 */ 685 @SuppressWarnings("unused") 686 @CalledByNative onRedirectReceived( final String newLocation, int httpStatusCode, String httpStatusText, String[] headers, boolean wasCached, String negotiatedProtocol, String proxyServer, long receivedByteCount)687 private void onRedirectReceived( 688 final String newLocation, 689 int httpStatusCode, 690 String httpStatusText, 691 String[] headers, 692 boolean wasCached, 693 String negotiatedProtocol, 694 String proxyServer, 695 long receivedByteCount) { 696 final UrlResponseInfoImpl responseInfo = 697 prepareResponseInfoOnNetworkThread( 698 httpStatusCode, 699 httpStatusText, 700 headers, 701 wasCached, 702 negotiatedProtocol, 703 proxyServer, 704 receivedByteCount); 705 706 // Have to do this after creating responseInfo. 707 mUrlChain.add(newLocation); 708 709 Runnable task = 710 new Runnable() { 711 @Override 712 public void run() { 713 checkCallingThread(); 714 synchronized (mUrlRequestAdapterLock) { 715 if (isDoneLocked()) { 716 return; 717 } 718 mWaitingOnRedirect = true; 719 } 720 721 try { 722 mCallback.onRedirectReceived( 723 CronetUrlRequest.this, responseInfo, newLocation); 724 } catch (Exception e) { 725 onCallbackException(e); 726 } 727 } 728 }; 729 postTaskToExecutor(task); 730 } 731 732 /** 733 * Called when the final set of headers, after all redirects, 734 * is received. Can only be called once for each request. 735 */ 736 @SuppressWarnings("unused") 737 @CalledByNative onResponseStarted( int httpStatusCode, String httpStatusText, String[] headers, boolean wasCached, String negotiatedProtocol, String proxyServer, long receivedByteCount)738 private void onResponseStarted( 739 int httpStatusCode, 740 String httpStatusText, 741 String[] headers, 742 boolean wasCached, 743 String negotiatedProtocol, 744 String proxyServer, 745 long receivedByteCount) { 746 mResponseInfo = 747 prepareResponseInfoOnNetworkThread( 748 httpStatusCode, 749 httpStatusText, 750 headers, 751 wasCached, 752 negotiatedProtocol, 753 proxyServer, 754 receivedByteCount); 755 Runnable task = 756 new Runnable() { 757 @Override 758 public void run() { 759 checkCallingThread(); 760 synchronized (mUrlRequestAdapterLock) { 761 if (isDoneLocked()) { 762 return; 763 } 764 mWaitingOnRead = true; 765 } 766 767 try { 768 mCallback.onResponseStarted(CronetUrlRequest.this, mResponseInfo); 769 } catch (Exception e) { 770 onCallbackException(e); 771 } 772 } 773 }; 774 postTaskToExecutor(task); 775 } 776 777 /** 778 * Called whenever data is received. The ByteBuffer remains 779 * valid only until listener callback. Or if the callback 780 * pauses the request, it remains valid until the request is resumed. 781 * Cancelling the request also invalidates the buffer. 782 * 783 * @param byteBuffer ByteBuffer containing received data, starting at 784 * initialPosition. Guaranteed to have at least one read byte. Its 785 * limit has not yet been updated to reflect the bytes read. 786 * @param bytesRead Number of bytes read. 787 * @param initialPosition Original position of byteBuffer when passed to 788 * read(). Used as a minimal check that the buffer hasn't been 789 * modified while reading from the network. 790 * @param initialLimit Original limit of byteBuffer when passed to 791 * read(). Used as a minimal check that the buffer hasn't been 792 * modified while reading from the network. 793 * @param receivedByteCount number of bytes received. 794 */ 795 @SuppressWarnings("unused") 796 @CalledByNative onReadCompleted( final ByteBuffer byteBuffer, int bytesRead, int initialPosition, int initialLimit, long receivedByteCount)797 private void onReadCompleted( 798 final ByteBuffer byteBuffer, 799 int bytesRead, 800 int initialPosition, 801 int initialLimit, 802 long receivedByteCount) { 803 mResponseInfo.setReceivedByteCount(receivedByteCount); 804 if (byteBuffer.position() != initialPosition || byteBuffer.limit() != initialLimit) { 805 failWithException( 806 new CronetExceptionImpl("ByteBuffer modified externally during read", null)); 807 return; 808 } 809 if (mOnReadCompletedTask == null) { 810 mOnReadCompletedTask = new OnReadCompletedRunnable(); 811 } 812 byteBuffer.position(initialPosition + bytesRead); 813 mOnReadCompletedTask.mByteBuffer = byteBuffer; 814 postTaskToExecutor(mOnReadCompletedTask); 815 } 816 817 /** 818 * Called when request is completed successfully, no callbacks will be 819 * called afterwards. 820 * 821 * @param receivedByteCount number of bytes received. 822 */ 823 @SuppressWarnings("unused") 824 @CalledByNative onSucceeded(long receivedByteCount)825 private void onSucceeded(long receivedByteCount) { 826 mResponseInfo.setReceivedByteCount(receivedByteCount); 827 Runnable task = 828 new Runnable() { 829 @Override 830 public void run() { 831 synchronized (mUrlRequestAdapterLock) { 832 if (isDoneLocked()) { 833 return; 834 } 835 // Destroy adapter first, so request context could be shut 836 // down from the listener. 837 destroyRequestAdapterLocked(RequestFinishedInfo.SUCCEEDED); 838 } 839 try { 840 mCallback.onSucceeded(CronetUrlRequest.this, mResponseInfo); 841 } catch (Exception e) { 842 Log.e( 843 CronetUrlRequestContext.LOG_TAG, 844 "Exception in onSucceeded method", 845 e); 846 } 847 maybeReportMetrics(); 848 } 849 }; 850 postTaskToExecutor(task); 851 } 852 853 /** 854 * Called when error has occurred, no callbacks will be called afterwards. 855 * 856 * @param errorCode Error code represented by {@code UrlRequestError} that should be mapped 857 * to one of {@link NetworkException#ERROR_HOSTNAME_NOT_RESOLVED 858 * NetworkException.ERROR_*}. 859 * @param nativeError native net error code. 860 * @param errorString textual representation of the error code. 861 * @param receivedByteCount number of bytes received. 862 */ 863 @SuppressWarnings("unused") 864 @CalledByNative onError( int errorCode, int nativeError, int nativeQuicError, String errorString, long receivedByteCount)865 private void onError( 866 int errorCode, 867 int nativeError, 868 int nativeQuicError, 869 String errorString, 870 long receivedByteCount) { 871 if (mResponseInfo != null) { 872 mResponseInfo.setReceivedByteCount(receivedByteCount); 873 } 874 if (errorCode == NetworkException.ERROR_QUIC_PROTOCOL_FAILED 875 || errorCode == NetworkException.ERROR_NETWORK_CHANGED) { 876 failWithException( 877 new QuicExceptionImpl( 878 "Exception in CronetUrlRequest: " + errorString, 879 errorCode, 880 nativeError, 881 nativeQuicError)); 882 } else { 883 int javaError = mapUrlRequestErrorToApiErrorCode(errorCode); 884 failWithException( 885 new NetworkExceptionImpl( 886 "Exception in CronetUrlRequest: " + errorString, 887 javaError, 888 nativeError)); 889 } 890 } 891 892 /** Called when request is canceled, no callbacks will be called afterwards. */ 893 @SuppressWarnings("unused") 894 @CalledByNative onCanceled()895 private void onCanceled() { 896 Runnable task = 897 new Runnable() { 898 @Override 899 public void run() { 900 try { 901 mCallback.onCanceled(CronetUrlRequest.this, mResponseInfo); 902 } catch (Exception e) { 903 Log.e( 904 CronetUrlRequestContext.LOG_TAG, 905 "Exception in onCanceled method", 906 e); 907 } 908 maybeReportMetrics(); 909 } 910 }; 911 postTaskToExecutor(task); 912 } 913 914 /** 915 * Called by the native code when request status is fetched from the 916 * native stack. 917 */ 918 @SuppressWarnings("unused") 919 @CalledByNative onStatus( final VersionSafeCallbacks.UrlRequestStatusListener listener, final int loadState)920 private void onStatus( 921 final VersionSafeCallbacks.UrlRequestStatusListener listener, final int loadState) { 922 Runnable task = 923 new Runnable() { 924 @Override 925 public void run() { 926 listener.onStatus(convertLoadState(loadState)); 927 } 928 }; 929 postTaskToExecutor(task); 930 } 931 932 /** 933 * Called by the native code on the network thread to report metrics. Happens before 934 * onSucceeded, onError and onCanceled. 935 */ 936 @SuppressWarnings("unused") 937 @CalledByNative onMetricsCollected( long requestStartMs, long dnsStartMs, long dnsEndMs, long connectStartMs, long connectEndMs, long sslStartMs, long sslEndMs, long sendingStartMs, long sendingEndMs, long pushStartMs, long pushEndMs, long responseStartMs, long requestEndMs, boolean socketReused, long sentByteCount, long receivedByteCount, boolean quicConnectionMigrationAttempted, boolean quicConnectionMigrationSuccessful)938 private void onMetricsCollected( 939 long requestStartMs, 940 long dnsStartMs, 941 long dnsEndMs, 942 long connectStartMs, 943 long connectEndMs, 944 long sslStartMs, 945 long sslEndMs, 946 long sendingStartMs, 947 long sendingEndMs, 948 long pushStartMs, 949 long pushEndMs, 950 long responseStartMs, 951 long requestEndMs, 952 boolean socketReused, 953 long sentByteCount, 954 long receivedByteCount, 955 boolean quicConnectionMigrationAttempted, 956 boolean quicConnectionMigrationSuccessful) { 957 synchronized (mUrlRequestAdapterLock) { 958 if (mMetrics != null) { 959 throw new IllegalStateException("Metrics collection should only happen once."); 960 } 961 mMetrics = 962 new CronetMetrics( 963 requestStartMs, 964 dnsStartMs, 965 dnsEndMs, 966 connectStartMs, 967 connectEndMs, 968 sslStartMs, 969 sslEndMs, 970 sendingStartMs, 971 sendingEndMs, 972 pushStartMs, 973 pushEndMs, 974 responseStartMs, 975 requestEndMs, 976 socketReused, 977 sentByteCount, 978 receivedByteCount); 979 mQuicConnectionMigrationAttempted = quicConnectionMigrationAttempted; 980 mQuicConnectionMigrationSuccessful = quicConnectionMigrationSuccessful; 981 } 982 // Metrics are reported to RequestFinishedListener when the final UrlRequest.Callback has 983 // been invoked. 984 } 985 986 /** Called when the native adapter is destroyed. */ 987 @SuppressWarnings("unused") 988 @CalledByNative onNativeAdapterDestroyed()989 private void onNativeAdapterDestroyed() { 990 synchronized (mUrlRequestAdapterLock) { 991 if (mOnDestroyedCallbackForTesting != null) { 992 mOnDestroyedCallbackForTesting.run(); 993 } 994 // mException is set when an error is encountered (in native code via onError or in 995 // Java code). If mException is not null, notify the mCallback and report metrics. 996 if (mException == null) { 997 return; 998 } 999 } 1000 Runnable task = 1001 new Runnable() { 1002 @Override 1003 public void run() { 1004 try { 1005 mCallback.onFailed(CronetUrlRequest.this, mResponseInfo, mException); 1006 } catch (Exception e) { 1007 Log.e( 1008 CronetUrlRequestContext.LOG_TAG, 1009 "Exception in onFailed method", 1010 e); 1011 } 1012 maybeReportMetrics(); 1013 } 1014 }; 1015 try { 1016 mExecutor.execute(task); 1017 } catch (RejectedExecutionException e) { 1018 Log.e(CronetUrlRequestContext.LOG_TAG, "Exception posting task to executor", e); 1019 } 1020 } 1021 1022 /** Enforces prohibition of direct execution. */ checkCallingThread()1023 void checkCallingThread() { 1024 if (!mAllowDirectExecutor && mRequestContext.isNetworkThread(Thread.currentThread())) { 1025 throw new InlineExecutionProhibitedException(); 1026 } 1027 } 1028 mapUrlRequestErrorToApiErrorCode(int errorCode)1029 private int mapUrlRequestErrorToApiErrorCode(int errorCode) { 1030 switch (errorCode) { 1031 case UrlRequestError.HOSTNAME_NOT_RESOLVED: 1032 return NetworkException.ERROR_HOSTNAME_NOT_RESOLVED; 1033 case UrlRequestError.INTERNET_DISCONNECTED: 1034 return NetworkException.ERROR_INTERNET_DISCONNECTED; 1035 case UrlRequestError.NETWORK_CHANGED: 1036 return NetworkException.ERROR_NETWORK_CHANGED; 1037 case UrlRequestError.TIMED_OUT: 1038 return NetworkException.ERROR_TIMED_OUT; 1039 case UrlRequestError.CONNECTION_CLOSED: 1040 return NetworkException.ERROR_CONNECTION_CLOSED; 1041 case UrlRequestError.CONNECTION_TIMED_OUT: 1042 return NetworkException.ERROR_CONNECTION_TIMED_OUT; 1043 case UrlRequestError.CONNECTION_REFUSED: 1044 return NetworkException.ERROR_CONNECTION_REFUSED; 1045 case UrlRequestError.CONNECTION_RESET: 1046 return NetworkException.ERROR_CONNECTION_RESET; 1047 case UrlRequestError.ADDRESS_UNREACHABLE: 1048 return NetworkException.ERROR_ADDRESS_UNREACHABLE; 1049 case UrlRequestError.QUIC_PROTOCOL_FAILED: 1050 return NetworkException.ERROR_QUIC_PROTOCOL_FAILED; 1051 case UrlRequestError.OTHER: 1052 return NetworkException.ERROR_OTHER; 1053 default: 1054 Log.e(CronetUrlRequestContext.LOG_TAG, "Unknown error code: " + errorCode); 1055 return errorCode; 1056 } 1057 } 1058 1059 /** 1060 * Builds the {@link CronetTrafficInfo} associated to this request internal state. 1061 * This helper methods makes strong assumptions about the state of the request. For this reason 1062 * it should only be called within {@link CronetUrlRequest#maybeReportMetrics} where these 1063 * assumptions are guaranteed to be true. 1064 * @return the {@link CronetTrafficInfo} associated to this request internal state 1065 */ 1066 @RequiresApi(Build.VERSION_CODES.O) buildCronetTrafficInfo()1067 private CronetTrafficInfo buildCronetTrafficInfo() { 1068 assert mMetrics != null; 1069 assert mRequestHeaders != null; 1070 1071 // Most of the CronetTrafficInfo fields have similar names/semantics. To avoid bugs due to 1072 // typos everything is final, this means that things have to initialized through an if/else. 1073 final Map<String, List<String>> responseHeaders; 1074 final String negotiatedProtocol; 1075 final int httpStatusCode; 1076 final boolean wasCached; 1077 if (mResponseInfo != null) { 1078 responseHeaders = mResponseInfo.getAllHeaders(); 1079 negotiatedProtocol = mResponseInfo.getNegotiatedProtocol(); 1080 httpStatusCode = mResponseInfo.getHttpStatusCode(); 1081 wasCached = mResponseInfo.wasCached(); 1082 } else { 1083 responseHeaders = Collections.emptyMap(); 1084 negotiatedProtocol = ""; 1085 httpStatusCode = 0; 1086 wasCached = false; 1087 } 1088 1089 // TODO(stefanoduo): A better approach might be keeping track of the total length of an 1090 // upload and use that value as the request body size instead. 1091 final long requestTotalSizeInBytes = mMetrics.getSentByteCount(); 1092 final long requestHeaderSizeInBytes; 1093 final long requestBodySizeInBytes; 1094 // Cached responses might still need to be revalidated over the network before being served 1095 // (from UrlResponseInfo#wasCached documentation). 1096 if (wasCached && requestTotalSizeInBytes == 0) { 1097 // Served from cache without the need to revalidate. 1098 requestHeaderSizeInBytes = 0; 1099 requestBodySizeInBytes = 0; 1100 } else { 1101 // Served from cache with the need to revalidate or served from the network directly. 1102 requestHeaderSizeInBytes = estimateHeadersSizeInBytes(mRequestHeaders); 1103 requestBodySizeInBytes = max(0, requestTotalSizeInBytes - requestHeaderSizeInBytes); 1104 } 1105 1106 final long responseTotalSizeInBytes = mMetrics.getReceivedByteCount(); 1107 final long responseBodySizeInBytes; 1108 final long responseHeaderSizeInBytes; 1109 // Cached responses might still need to be revalidated over the network before being served 1110 // (from UrlResponseInfo#wasCached documentation). 1111 if (wasCached && responseTotalSizeInBytes == 0) { 1112 // Served from cache without the need to revalidate. 1113 responseBodySizeInBytes = 0; 1114 responseHeaderSizeInBytes = 0; 1115 } else { 1116 // Served from cache with the need to revalidate or served from the network directly. 1117 responseHeaderSizeInBytes = estimateHeadersSizeInBytes(responseHeaders); 1118 responseBodySizeInBytes = max(0, responseTotalSizeInBytes - responseHeaderSizeInBytes); 1119 } 1120 1121 final Duration headersLatency; 1122 if (mMetrics.getRequestStart() != null && mMetrics.getResponseStart() != null) { 1123 headersLatency = 1124 Duration.ofMillis( 1125 mMetrics.getResponseStart().getTime() 1126 - mMetrics.getRequestStart().getTime()); 1127 } else { 1128 headersLatency = Duration.ofSeconds(0); 1129 } 1130 1131 final Duration totalLatency; 1132 if (mMetrics.getRequestStart() != null && mMetrics.getRequestEnd() != null) { 1133 totalLatency = 1134 Duration.ofMillis( 1135 mMetrics.getRequestEnd().getTime() 1136 - mMetrics.getRequestStart().getTime()); 1137 } else { 1138 totalLatency = Duration.ofSeconds(0); 1139 } 1140 1141 return new CronetTrafficInfo( 1142 requestHeaderSizeInBytes, 1143 requestBodySizeInBytes, 1144 responseHeaderSizeInBytes, 1145 responseBodySizeInBytes, 1146 httpStatusCode, 1147 headersLatency, 1148 totalLatency, 1149 negotiatedProtocol, 1150 mQuicConnectionMigrationAttempted, 1151 mQuicConnectionMigrationSuccessful); 1152 } 1153 1154 // Maybe report metrics. This method should only be called on Callback's executor thread and 1155 // after Callback's onSucceeded, onFailed and onCanceled. maybeReportMetrics()1156 private void maybeReportMetrics() { 1157 final RefCountDelegate inflightCallbackCount = 1158 new RefCountDelegate(() -> mRequestContext.onRequestFinished()); 1159 try { 1160 if (mMetrics == null) return; 1161 1162 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 1163 try { 1164 mLogger.logCronetTrafficInfo(mCronetEngineId, buildCronetTrafficInfo()); 1165 } catch (RuntimeException e) { 1166 // Handle any issue gracefully, we should never crash due failures while 1167 // logging. 1168 Log.e( 1169 CronetUrlRequestContext.LOG_TAG, 1170 "Error while trying to log CronetTrafficInfo: ", 1171 e); 1172 } 1173 } 1174 1175 final RequestFinishedInfo requestInfo = 1176 new RequestFinishedInfoImpl( 1177 mInitialUrl, 1178 mRequestAnnotations, 1179 mMetrics, 1180 mFinishedReason, 1181 mResponseInfo, 1182 mException); 1183 mRequestContext.reportRequestFinished(requestInfo, inflightCallbackCount); 1184 if (mRequestFinishedListener != null) { 1185 inflightCallbackCount.increment(); 1186 try { 1187 mRequestFinishedListener 1188 .getExecutor() 1189 .execute( 1190 new Runnable() { 1191 @Override 1192 public void run() { 1193 try { 1194 mRequestFinishedListener.onRequestFinished( 1195 requestInfo); 1196 } catch (Exception e) { 1197 Log.e( 1198 CronetUrlRequestContext.LOG_TAG, 1199 "Exception thrown from request" 1200 + " finishedlistener", 1201 e); 1202 } finally { 1203 inflightCallbackCount.decrement(); 1204 } 1205 } 1206 }); 1207 } catch (RejectedExecutionException failException) { 1208 Log.e( 1209 CronetUrlRequestContext.LOG_TAG, 1210 "Exception posting task to executor", 1211 failException); 1212 inflightCallbackCount.decrement(); 1213 } 1214 } 1215 } finally { 1216 inflightCallbackCount.decrement(); 1217 } 1218 } 1219 1220 // Native methods are implemented in cronet_url_request_adapter.cc. 1221 @NativeMethods 1222 interface Natives { createRequestAdapter( CronetUrlRequest caller, long urlRequestContextAdapter, String url, int priority, boolean disableCache, boolean disableConnectionMigration, boolean trafficStatsTagSet, int trafficStatsTag, boolean trafficStatsUidSet, int trafficStatsUid, int idempotency, long networkHandle)1223 long createRequestAdapter( 1224 CronetUrlRequest caller, 1225 long urlRequestContextAdapter, 1226 String url, 1227 int priority, 1228 boolean disableCache, 1229 boolean disableConnectionMigration, 1230 boolean trafficStatsTagSet, 1231 int trafficStatsTag, 1232 boolean trafficStatsUidSet, 1233 int trafficStatsUid, 1234 int idempotency, 1235 long networkHandle); 1236 1237 @NativeClassQualifiedName("CronetURLRequestAdapter") setHttpMethod(long nativePtr, CronetUrlRequest caller, String method)1238 boolean setHttpMethod(long nativePtr, CronetUrlRequest caller, String method); 1239 1240 @NativeClassQualifiedName("CronetURLRequestAdapter") addRequestHeader( long nativePtr, CronetUrlRequest caller, String name, String value)1241 boolean addRequestHeader( 1242 long nativePtr, CronetUrlRequest caller, String name, String value); 1243 1244 @NativeClassQualifiedName("CronetURLRequestAdapter") start(long nativePtr, CronetUrlRequest caller)1245 void start(long nativePtr, CronetUrlRequest caller); 1246 1247 @NativeClassQualifiedName("CronetURLRequestAdapter") followDeferredRedirect(long nativePtr, CronetUrlRequest caller)1248 void followDeferredRedirect(long nativePtr, CronetUrlRequest caller); 1249 1250 @NativeClassQualifiedName("CronetURLRequestAdapter") readData( long nativePtr, CronetUrlRequest caller, ByteBuffer byteBuffer, int position, int capacity)1251 boolean readData( 1252 long nativePtr, 1253 CronetUrlRequest caller, 1254 ByteBuffer byteBuffer, 1255 int position, 1256 int capacity); 1257 1258 @NativeClassQualifiedName("CronetURLRequestAdapter") destroy(long nativePtr, CronetUrlRequest caller, boolean sendOnCanceled)1259 void destroy(long nativePtr, CronetUrlRequest caller, boolean sendOnCanceled); 1260 1261 @NativeClassQualifiedName("CronetURLRequestAdapter") getStatus( long nativePtr, CronetUrlRequest caller, VersionSafeCallbacks.UrlRequestStatusListener listener)1262 void getStatus( 1263 long nativePtr, 1264 CronetUrlRequest caller, 1265 VersionSafeCallbacks.UrlRequestStatusListener listener); 1266 } 1267 } 1268