1 /* 2 * Copyright (C) 2016 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 package com.google.android.exoplayer2.drm; 17 18 import android.annotation.SuppressLint; 19 import android.media.NotProvisionedException; 20 import android.os.Handler; 21 import android.os.HandlerThread; 22 import android.os.Looper; 23 import android.os.Message; 24 import android.os.SystemClock; 25 import android.util.Pair; 26 import androidx.annotation.Nullable; 27 import androidx.annotation.RequiresApi; 28 import com.google.android.exoplayer2.C; 29 import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; 30 import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; 31 import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; 32 import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; 33 import com.google.android.exoplayer2.util.Assertions; 34 import com.google.android.exoplayer2.util.CopyOnWriteMultiset; 35 import com.google.android.exoplayer2.util.Log; 36 import com.google.android.exoplayer2.util.MediaSourceEventDispatcher; 37 import com.google.android.exoplayer2.util.Util; 38 import java.io.IOException; 39 import java.util.Arrays; 40 import java.util.Collections; 41 import java.util.HashMap; 42 import java.util.List; 43 import java.util.Map; 44 import java.util.UUID; 45 import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; 46 import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 47 import org.checkerframework.checker.nullness.qual.RequiresNonNull; 48 49 /** A {@link DrmSession} that supports playbacks using {@link ExoMediaDrm}. */ 50 @RequiresApi(18) 51 /* package */ class DefaultDrmSession implements DrmSession { 52 53 /** Thrown when an unexpected exception or error is thrown during provisioning or key requests. */ 54 public static final class UnexpectedDrmSessionException extends IOException { 55 UnexpectedDrmSessionException(Throwable cause)56 public UnexpectedDrmSessionException(Throwable cause) { 57 super("Unexpected " + cause.getClass().getSimpleName() + ": " + cause.getMessage(), cause); 58 } 59 } 60 61 /** Manages provisioning requests. */ 62 public interface ProvisioningManager { 63 64 /** 65 * Called when a session requires provisioning. The manager <em>may</em> call {@link 66 * #provision()} to have this session perform the provisioning operation. The manager 67 * <em>will</em> call {@link DefaultDrmSession#onProvisionCompleted()} when provisioning has 68 * completed, or {@link DefaultDrmSession#onProvisionError} if provisioning fails. 69 * 70 * @param session The session. 71 */ provisionRequired(DefaultDrmSession session)72 void provisionRequired(DefaultDrmSession session); 73 74 /** 75 * Called by a session when it fails to perform a provisioning operation. 76 * 77 * @param error The error that occurred. 78 */ onProvisionError(Exception error)79 void onProvisionError(Exception error); 80 81 /** Called by a session when it successfully completes a provisioning operation. */ onProvisionCompleted()82 void onProvisionCompleted(); 83 } 84 85 /** Callback to be notified when the session is released. */ 86 public interface ReleaseCallback { 87 88 /** 89 * Called immediately after releasing session resources. 90 * 91 * @param session The session. 92 */ onSessionReleased(DefaultDrmSession session)93 void onSessionReleased(DefaultDrmSession session); 94 } 95 96 private static final String TAG = "DefaultDrmSession"; 97 98 private static final int MSG_PROVISION = 0; 99 private static final int MSG_KEYS = 1; 100 private static final int MAX_LICENSE_DURATION_TO_RENEW_SECONDS = 60; 101 102 /** The DRM scheme datas, or null if this session uses offline keys. */ 103 @Nullable public final List<SchemeData> schemeDatas; 104 105 private final ExoMediaDrm mediaDrm; 106 private final ProvisioningManager provisioningManager; 107 private final ReleaseCallback releaseCallback; 108 private final @DefaultDrmSessionManager.Mode int mode; 109 private final boolean playClearSamplesWithoutKeys; 110 private final boolean isPlaceholderSession; 111 private final HashMap<String, String> keyRequestParameters; 112 private final CopyOnWriteMultiset<MediaSourceEventDispatcher> eventDispatchers; 113 private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; 114 115 /* package */ final MediaDrmCallback callback; 116 /* package */ final UUID uuid; 117 /* package */ final ResponseHandler responseHandler; 118 119 private @DrmSession.State int state; 120 private int referenceCount; 121 @Nullable private HandlerThread requestHandlerThread; 122 @Nullable private RequestHandler requestHandler; 123 @Nullable private ExoMediaCrypto mediaCrypto; 124 @Nullable private DrmSessionException lastException; 125 @Nullable private byte[] sessionId; 126 private byte @MonotonicNonNull [] offlineLicenseKeySetId; 127 128 @Nullable private KeyRequest currentKeyRequest; 129 @Nullable private ProvisionRequest currentProvisionRequest; 130 131 /** 132 * Instantiates a new DRM session. 133 * 134 * @param uuid The UUID of the drm scheme. 135 * @param mediaDrm The media DRM. 136 * @param provisioningManager The manager for provisioning. 137 * @param releaseCallback The {@link ReleaseCallback}. 138 * @param schemeDatas DRM scheme datas for this session, or null if an {@code 139 * offlineLicenseKeySetId} is provided or if {@code isPlaceholderSession} is true. 140 * @param mode The DRM mode. Ignored if {@code isPlaceholderSession} is true. 141 * @param isPlaceholderSession Whether this session is not expected to acquire any keys. 142 * @param offlineLicenseKeySetId The offline license key set identifier, or null when not using 143 * offline keys. 144 * @param keyRequestParameters Key request parameters. 145 * @param callback The media DRM callback. 146 * @param playbackLooper The playback looper. 147 * @param loadErrorHandlingPolicy The {@link LoadErrorHandlingPolicy} for key and provisioning 148 * requests. 149 */ DefaultDrmSession( UUID uuid, ExoMediaDrm mediaDrm, ProvisioningManager provisioningManager, ReleaseCallback releaseCallback, @Nullable List<SchemeData> schemeDatas, @DefaultDrmSessionManager.Mode int mode, boolean playClearSamplesWithoutKeys, boolean isPlaceholderSession, @Nullable byte[] offlineLicenseKeySetId, HashMap<String, String> keyRequestParameters, MediaDrmCallback callback, Looper playbackLooper, LoadErrorHandlingPolicy loadErrorHandlingPolicy)150 public DefaultDrmSession( 151 UUID uuid, 152 ExoMediaDrm mediaDrm, 153 ProvisioningManager provisioningManager, 154 ReleaseCallback releaseCallback, 155 @Nullable List<SchemeData> schemeDatas, 156 @DefaultDrmSessionManager.Mode int mode, 157 boolean playClearSamplesWithoutKeys, 158 boolean isPlaceholderSession, 159 @Nullable byte[] offlineLicenseKeySetId, 160 HashMap<String, String> keyRequestParameters, 161 MediaDrmCallback callback, 162 Looper playbackLooper, 163 LoadErrorHandlingPolicy loadErrorHandlingPolicy) { 164 if (mode == DefaultDrmSessionManager.MODE_QUERY 165 || mode == DefaultDrmSessionManager.MODE_RELEASE) { 166 Assertions.checkNotNull(offlineLicenseKeySetId); 167 } 168 this.uuid = uuid; 169 this.provisioningManager = provisioningManager; 170 this.releaseCallback = releaseCallback; 171 this.mediaDrm = mediaDrm; 172 this.mode = mode; 173 this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; 174 this.isPlaceholderSession = isPlaceholderSession; 175 if (offlineLicenseKeySetId != null) { 176 this.offlineLicenseKeySetId = offlineLicenseKeySetId; 177 this.schemeDatas = null; 178 } else { 179 this.schemeDatas = Collections.unmodifiableList(Assertions.checkNotNull(schemeDatas)); 180 } 181 this.keyRequestParameters = keyRequestParameters; 182 this.callback = callback; 183 this.eventDispatchers = new CopyOnWriteMultiset<>(); 184 this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; 185 state = STATE_OPENING; 186 responseHandler = new ResponseHandler(playbackLooper); 187 } 188 hasSessionId(byte[] sessionId)189 public boolean hasSessionId(byte[] sessionId) { 190 return Arrays.equals(this.sessionId, sessionId); 191 } 192 onMediaDrmEvent(int what)193 public void onMediaDrmEvent(int what) { 194 switch (what) { 195 case ExoMediaDrm.EVENT_KEY_REQUIRED: 196 onKeysRequired(); 197 break; 198 default: 199 break; 200 } 201 } 202 203 // Provisioning implementation. 204 provision()205 public void provision() { 206 currentProvisionRequest = mediaDrm.getProvisionRequest(); 207 Util.castNonNull(requestHandler) 208 .post( 209 MSG_PROVISION, 210 Assertions.checkNotNull(currentProvisionRequest), 211 /* allowRetry= */ true); 212 } 213 onProvisionCompleted()214 public void onProvisionCompleted() { 215 if (openInternal(false)) { 216 doLicense(true); 217 } 218 } 219 onProvisionError(Exception error)220 public void onProvisionError(Exception error) { 221 onError(error); 222 } 223 224 // DrmSession implementation. 225 226 @Override 227 @DrmSession.State getState()228 public final int getState() { 229 return state; 230 } 231 232 @Override playClearSamplesWithoutKeys()233 public boolean playClearSamplesWithoutKeys() { 234 return playClearSamplesWithoutKeys; 235 } 236 237 @Override getError()238 public final @Nullable DrmSessionException getError() { 239 return state == STATE_ERROR ? lastException : null; 240 } 241 242 @Override getMediaCrypto()243 public final @Nullable ExoMediaCrypto getMediaCrypto() { 244 return mediaCrypto; 245 } 246 247 @Override 248 @Nullable queryKeyStatus()249 public Map<String, String> queryKeyStatus() { 250 return sessionId == null ? null : mediaDrm.queryKeyStatus(sessionId); 251 } 252 253 @Override 254 @Nullable getOfflineLicenseKeySetId()255 public byte[] getOfflineLicenseKeySetId() { 256 return offlineLicenseKeySetId; 257 } 258 259 @Override acquire(@ullable MediaSourceEventDispatcher eventDispatcher)260 public void acquire(@Nullable MediaSourceEventDispatcher eventDispatcher) { 261 Assertions.checkState(referenceCount >= 0); 262 if (eventDispatcher != null) { 263 eventDispatchers.add(eventDispatcher); 264 } 265 if (++referenceCount == 1) { 266 Assertions.checkState(state == STATE_OPENING); 267 requestHandlerThread = new HandlerThread("ExoPlayer:DrmRequestHandler"); 268 requestHandlerThread.start(); 269 requestHandler = new RequestHandler(requestHandlerThread.getLooper()); 270 if (openInternal(true)) { 271 doLicense(true); 272 } 273 } else { 274 // TODO: Add a parameter to onDrmSessionAcquired to indicate whether the session is being 275 // re-used or not. 276 if (eventDispatcher != null) { 277 eventDispatcher.dispatch( 278 (listener, windowIndex, mediaPeriodId) -> listener.onDrmSessionAcquired(), 279 DrmSessionEventListener.class); 280 } 281 } 282 } 283 284 @Override release(@ullable MediaSourceEventDispatcher eventDispatcher)285 public void release(@Nullable MediaSourceEventDispatcher eventDispatcher) { 286 if (--referenceCount == 0) { 287 // Assigning null to various non-null variables for clean-up. 288 state = STATE_RELEASED; 289 Util.castNonNull(responseHandler).removeCallbacksAndMessages(null); 290 Util.castNonNull(requestHandler).removeCallbacksAndMessages(null); 291 requestHandler = null; 292 Util.castNonNull(requestHandlerThread).quit(); 293 requestHandlerThread = null; 294 mediaCrypto = null; 295 lastException = null; 296 currentKeyRequest = null; 297 currentProvisionRequest = null; 298 if (sessionId != null) { 299 mediaDrm.closeSession(sessionId); 300 sessionId = null; 301 } 302 releaseCallback.onSessionReleased(this); 303 } 304 dispatchEvent((listener, windowIndex, mediaPeriodId) -> listener.onDrmSessionReleased()); 305 if (eventDispatcher != null) { 306 eventDispatchers.remove(eventDispatcher); 307 } 308 } 309 310 // Internal methods. 311 312 /** 313 * Try to open a session, do provisioning if necessary. 314 * 315 * @param allowProvisioning if provisioning is allowed, set this to false when calling from 316 * processing provision response. 317 * @return true on success, false otherwise. 318 */ 319 @EnsuresNonNullIf(result = true, expression = "sessionId") openInternal(boolean allowProvisioning)320 private boolean openInternal(boolean allowProvisioning) { 321 if (isOpen()) { 322 // Already opened 323 return true; 324 } 325 326 try { 327 sessionId = mediaDrm.openSession(); 328 mediaCrypto = mediaDrm.createMediaCrypto(sessionId); 329 dispatchEvent((listener, windowIndex, mediaPeriodId) -> listener.onDrmSessionAcquired()); 330 state = STATE_OPENED; 331 Assertions.checkNotNull(sessionId); 332 return true; 333 } catch (NotProvisionedException e) { 334 if (allowProvisioning) { 335 provisioningManager.provisionRequired(this); 336 } else { 337 onError(e); 338 } 339 } catch (Exception e) { 340 onError(e); 341 } 342 343 return false; 344 } 345 onProvisionResponse(Object request, Object response)346 private void onProvisionResponse(Object request, Object response) { 347 if (request != currentProvisionRequest || (state != STATE_OPENING && !isOpen())) { 348 // This event is stale. 349 return; 350 } 351 currentProvisionRequest = null; 352 353 if (response instanceof Exception) { 354 provisioningManager.onProvisionError((Exception) response); 355 return; 356 } 357 358 try { 359 mediaDrm.provideProvisionResponse((byte[]) response); 360 } catch (Exception e) { 361 provisioningManager.onProvisionError(e); 362 return; 363 } 364 365 provisioningManager.onProvisionCompleted(); 366 } 367 368 @RequiresNonNull("sessionId") doLicense(boolean allowRetry)369 private void doLicense(boolean allowRetry) { 370 if (isPlaceholderSession) { 371 return; 372 } 373 byte[] sessionId = Util.castNonNull(this.sessionId); 374 switch (mode) { 375 case DefaultDrmSessionManager.MODE_PLAYBACK: 376 case DefaultDrmSessionManager.MODE_QUERY: 377 if (offlineLicenseKeySetId == null) { 378 postKeyRequest(sessionId, ExoMediaDrm.KEY_TYPE_STREAMING, allowRetry); 379 } else if (state == STATE_OPENED_WITH_KEYS || restoreKeys()) { 380 long licenseDurationRemainingSec = getLicenseDurationRemainingSec(); 381 if (mode == DefaultDrmSessionManager.MODE_PLAYBACK 382 && licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW_SECONDS) { 383 Log.d( 384 TAG, 385 "Offline license has expired or will expire soon. " 386 + "Remaining seconds: " 387 + licenseDurationRemainingSec); 388 postKeyRequest(sessionId, ExoMediaDrm.KEY_TYPE_OFFLINE, allowRetry); 389 } else if (licenseDurationRemainingSec <= 0) { 390 onError(new KeysExpiredException()); 391 } else { 392 state = STATE_OPENED_WITH_KEYS; 393 dispatchEvent((listener, windowIndex, mediaPeriodId) -> listener.onDrmKeysRestored()); 394 } 395 } 396 break; 397 case DefaultDrmSessionManager.MODE_DOWNLOAD: 398 if (offlineLicenseKeySetId == null || restoreKeys()) { 399 postKeyRequest(sessionId, ExoMediaDrm.KEY_TYPE_OFFLINE, allowRetry); 400 } 401 break; 402 case DefaultDrmSessionManager.MODE_RELEASE: 403 Assertions.checkNotNull(offlineLicenseKeySetId); 404 Assertions.checkNotNull(this.sessionId); 405 // It's not necessary to restore the key (and open a session to do that) before releasing it 406 // but this serves as a good sanity/fast-failure check. 407 if (restoreKeys()) { 408 postKeyRequest(offlineLicenseKeySetId, ExoMediaDrm.KEY_TYPE_RELEASE, allowRetry); 409 } 410 break; 411 default: 412 break; 413 } 414 } 415 416 @RequiresNonNull({"sessionId", "offlineLicenseKeySetId"}) restoreKeys()417 private boolean restoreKeys() { 418 try { 419 mediaDrm.restoreKeys(sessionId, offlineLicenseKeySetId); 420 return true; 421 } catch (Exception e) { 422 Log.e(TAG, "Error trying to restore keys.", e); 423 onError(e); 424 } 425 return false; 426 } 427 getLicenseDurationRemainingSec()428 private long getLicenseDurationRemainingSec() { 429 if (!C.WIDEVINE_UUID.equals(uuid)) { 430 return Long.MAX_VALUE; 431 } 432 Pair<Long, Long> pair = 433 Assertions.checkNotNull(WidevineUtil.getLicenseDurationRemainingSec(this)); 434 return Math.min(pair.first, pair.second); 435 } 436 postKeyRequest(byte[] scope, int type, boolean allowRetry)437 private void postKeyRequest(byte[] scope, int type, boolean allowRetry) { 438 try { 439 currentKeyRequest = mediaDrm.getKeyRequest(scope, schemeDatas, type, keyRequestParameters); 440 Util.castNonNull(requestHandler) 441 .post(MSG_KEYS, Assertions.checkNotNull(currentKeyRequest), allowRetry); 442 } catch (Exception e) { 443 onKeysError(e); 444 } 445 } 446 onKeyResponse(Object request, Object response)447 private void onKeyResponse(Object request, Object response) { 448 if (request != currentKeyRequest || !isOpen()) { 449 // This event is stale. 450 return; 451 } 452 currentKeyRequest = null; 453 454 if (response instanceof Exception) { 455 onKeysError((Exception) response); 456 return; 457 } 458 459 try { 460 byte[] responseData = (byte[]) response; 461 if (mode == DefaultDrmSessionManager.MODE_RELEASE) { 462 mediaDrm.provideKeyResponse(Util.castNonNull(offlineLicenseKeySetId), responseData); 463 dispatchEvent((listener, windowIndex, mediaPeriodId) -> listener.onDrmKeysRestored()); 464 } else { 465 byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, responseData); 466 if ((mode == DefaultDrmSessionManager.MODE_DOWNLOAD 467 || (mode == DefaultDrmSessionManager.MODE_PLAYBACK 468 && offlineLicenseKeySetId != null)) 469 && keySetId != null 470 && keySetId.length != 0) { 471 offlineLicenseKeySetId = keySetId; 472 } 473 state = STATE_OPENED_WITH_KEYS; 474 dispatchEvent((listener, windowIndex, mediaPeriodId) -> listener.onDrmKeysLoaded()); 475 } 476 } catch (Exception e) { 477 onKeysError(e); 478 } 479 } 480 onKeysRequired()481 private void onKeysRequired() { 482 if (mode == DefaultDrmSessionManager.MODE_PLAYBACK && state == STATE_OPENED_WITH_KEYS) { 483 Util.castNonNull(sessionId); 484 doLicense(/* allowRetry= */ false); 485 } 486 } 487 onKeysError(Exception e)488 private void onKeysError(Exception e) { 489 if (e instanceof NotProvisionedException) { 490 provisioningManager.provisionRequired(this); 491 } else { 492 onError(e); 493 } 494 } 495 onError(final Exception e)496 private void onError(final Exception e) { 497 lastException = new DrmSessionException(e); 498 dispatchEvent((listener, windowIndex, mediaPeriodId) -> listener.onDrmSessionManagerError(e)); 499 if (state != STATE_OPENED_WITH_KEYS) { 500 state = STATE_ERROR; 501 } 502 } 503 504 @EnsuresNonNullIf(result = true, expression = "sessionId") 505 @SuppressWarnings("contracts.conditional.postcondition.not.satisfied") isOpen()506 private boolean isOpen() { 507 return state == STATE_OPENED || state == STATE_OPENED_WITH_KEYS; 508 } 509 dispatchEvent( MediaSourceEventDispatcher.EventWithPeriodId<DrmSessionEventListener> event)510 private void dispatchEvent( 511 MediaSourceEventDispatcher.EventWithPeriodId<DrmSessionEventListener> event) { 512 for (MediaSourceEventDispatcher eventDispatcher : eventDispatchers.elementSet()) { 513 eventDispatcher.dispatch(event, DrmSessionEventListener.class); 514 } 515 } 516 517 // Internal classes. 518 519 @SuppressLint("HandlerLeak") 520 private class ResponseHandler extends Handler { 521 ResponseHandler(Looper looper)522 public ResponseHandler(Looper looper) { 523 super(looper); 524 } 525 526 @Override 527 @SuppressWarnings("unchecked") handleMessage(Message msg)528 public void handleMessage(Message msg) { 529 Pair<Object, Object> requestAndResponse = (Pair<Object, Object>) msg.obj; 530 Object request = requestAndResponse.first; 531 Object response = requestAndResponse.second; 532 switch (msg.what) { 533 case MSG_PROVISION: 534 onProvisionResponse(request, response); 535 break; 536 case MSG_KEYS: 537 onKeyResponse(request, response); 538 break; 539 default: 540 break; 541 } 542 } 543 } 544 545 @SuppressLint("HandlerLeak") 546 private class RequestHandler extends Handler { 547 RequestHandler(Looper backgroundLooper)548 public RequestHandler(Looper backgroundLooper) { 549 super(backgroundLooper); 550 } 551 post(int what, Object request, boolean allowRetry)552 void post(int what, Object request, boolean allowRetry) { 553 RequestTask requestTask = 554 new RequestTask(allowRetry, /* startTimeMs= */ SystemClock.elapsedRealtime(), request); 555 obtainMessage(what, requestTask).sendToTarget(); 556 } 557 558 @Override handleMessage(Message msg)559 public void handleMessage(Message msg) { 560 RequestTask requestTask = (RequestTask) msg.obj; 561 Object response; 562 try { 563 switch (msg.what) { 564 case MSG_PROVISION: 565 response = 566 callback.executeProvisionRequest(uuid, (ProvisionRequest) requestTask.request); 567 break; 568 case MSG_KEYS: 569 response = callback.executeKeyRequest(uuid, (KeyRequest) requestTask.request); 570 break; 571 default: 572 throw new RuntimeException(); 573 } 574 } catch (Exception e) { 575 if (maybeRetryRequest(msg, e)) { 576 return; 577 } 578 response = e; 579 } 580 responseHandler 581 .obtainMessage(msg.what, Pair.create(requestTask.request, response)) 582 .sendToTarget(); 583 } 584 maybeRetryRequest(Message originalMsg, Exception e)585 private boolean maybeRetryRequest(Message originalMsg, Exception e) { 586 RequestTask requestTask = (RequestTask) originalMsg.obj; 587 if (!requestTask.allowRetry) { 588 return false; 589 } 590 requestTask.errorCount++; 591 if (requestTask.errorCount 592 > loadErrorHandlingPolicy.getMinimumLoadableRetryCount(C.DATA_TYPE_DRM)) { 593 return false; 594 } 595 IOException ioException = 596 e instanceof IOException ? (IOException) e : new UnexpectedDrmSessionException(e); 597 long retryDelayMs = 598 loadErrorHandlingPolicy.getRetryDelayMsFor( 599 C.DATA_TYPE_DRM, 600 /* loadDurationMs= */ SystemClock.elapsedRealtime() - requestTask.startTimeMs, 601 ioException, 602 requestTask.errorCount); 603 if (retryDelayMs == C.TIME_UNSET) { 604 // The error is fatal. 605 return false; 606 } 607 sendMessageDelayed(Message.obtain(originalMsg), retryDelayMs); 608 return true; 609 } 610 } 611 612 private static final class RequestTask { 613 614 public final boolean allowRetry; 615 public final long startTimeMs; 616 public final Object request; 617 public int errorCount; 618 RequestTask(boolean allowRetry, long startTimeMs, Object request)619 public RequestTask(boolean allowRetry, long startTimeMs, Object request) { 620 this.allowRetry = allowRetry; 621 this.startTimeMs = startTimeMs; 622 this.request = request; 623 } 624 } 625 } 626