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