4343import androidx .media3 .exoplayer .offline .Downloader ;
4444import androidx .media3 .exoplayer .offline .DownloaderFactory ;
4545import androidx .media3 .exoplayer .source .MediaSource ;
46+ import androidx .media3 .exoplayer .util .ReleasableExecutor ;
4647import com .google .common .base .Supplier ;
4748import com .google .errorprone .annotations .CanIgnoreReturnValue ;
4849import java .io .IOException ;
4950import java .util .concurrent .CancellationException ;
5051import java .util .concurrent .Executor ;
52+ import java .util .concurrent .ExecutorService ;
53+ import org .checkerframework .checker .nullness .qual .MonotonicNonNull ;
5154
5255/** A helper for pre-caching a single media. */
5356@ UnstableApi
@@ -260,7 +263,7 @@ public PreCacheHelper create(MediaItem mediaItem) {
260263 new DefaultDownloaderFactory (cacheDataSourceFactory , downloadExecutor );
261264 return new PreCacheHelper (
262265 mediaItem ,
263- /* mediaSourceFactory = */ null ,
266+ /* testMediaSourceFactory = */ null ,
264267 downloadHelperFactory ,
265268 downloaderFactory ,
266269 preCacheLooper ,
@@ -271,7 +274,9 @@ public PreCacheHelper create(MediaItem mediaItem) {
271274 @ VisibleForTesting /* package */ static final int DEFAULT_MIN_RETRY_COUNT = 5 ;
272275
273276 private final MediaItem mediaItem ;
274- private final Supplier <DownloadHelper > downloadHelperSupplier ;
277+
278+ @ Nullable private final MediaSource .Factory testMediaSourceFactory ;
279+ private final DownloadHelper .Factory downloadHelperFactory ;
275280 private final DownloaderFactory downloaderFactory ;
276281 @ Nullable private final Listener listener ;
277282 private final Handler preCacheHandler ;
@@ -280,17 +285,14 @@ public PreCacheHelper create(MediaItem mediaItem) {
280285
281286 /* package */ PreCacheHelper (
282287 MediaItem mediaItem ,
283- @ Nullable MediaSource .Factory mediaSourceFactory ,
288+ @ Nullable MediaSource .Factory testMediaSourceFactory ,
284289 DownloadHelper .Factory downloadHelperFactory ,
285290 DownloaderFactory downloaderFactory ,
286291 Looper preCacheLooper ,
287292 @ Nullable Listener listener ) {
288293 this .mediaItem = mediaItem ;
289- this .downloadHelperSupplier =
290- () ->
291- mediaSourceFactory != null
292- ? downloadHelperFactory .create (mediaSourceFactory .createMediaSource (mediaItem ))
293- : downloadHelperFactory .create (mediaItem );
294+ this .testMediaSourceFactory = testMediaSourceFactory ;
295+ this .downloadHelperFactory = downloadHelperFactory ;
294296 this .downloaderFactory = downloaderFactory ;
295297 this .listener = listener ;
296298 this .preCacheHandler = Util .createHandler (preCacheLooper , /* callback= */ null );
@@ -352,14 +354,84 @@ public void release(boolean removeCachedContent) {
352354 });
353355 }
354356
357+ private static final class ReleasableSingleThreadExecutor implements ReleasableExecutor {
358+
359+ private final ExecutorService executor ;
360+ private final Runnable releaseRunnable ;
361+
362+ private ReleasableSingleThreadExecutor (Runnable releaseRunnable ) {
363+ this .executor = Util .newSingleThreadExecutor ("PreCacheHelper:Loader" );
364+ this .releaseRunnable = releaseRunnable ;
365+ }
366+
367+ @ Override
368+ public void release () {
369+ execute (releaseRunnable );
370+ executor .shutdown ();
371+ }
372+
373+ @ Override
374+ public void execute (Runnable command ) {
375+ executor .execute (command );
376+ }
377+ }
378+
379+ private static final class ReleasableExecutorSupplier implements Supplier <ReleasableExecutor > {
380+ private final Handler preCacheHandler ;
381+ private @ MonotonicNonNull DownloadCallback downloadCallback ;
382+
383+ @ GuardedBy ("this" )
384+ private int executorCount ;
385+
386+ private ReleasableExecutorSupplier (Handler preCacheHandler ) {
387+ this .preCacheHandler = preCacheHandler ;
388+ }
389+
390+ public void setDownloadCallback (DownloadCallback downloadCallback ) {
391+ this .downloadCallback = downloadCallback ;
392+ }
393+
394+ @ Override
395+ public ReleasableSingleThreadExecutor get () {
396+ synchronized (ReleasableExecutorSupplier .this ) {
397+ executorCount ++;
398+ }
399+ return new ReleasableSingleThreadExecutor (this ::onExecutorReleased );
400+ }
401+
402+ private void onExecutorReleased () {
403+ synchronized (ReleasableExecutorSupplier .this ) {
404+ checkState (executorCount > 0 );
405+ executorCount --;
406+ if (wereExecutorsReleased ()) {
407+ preCacheHandler .post (
408+ () -> {
409+ checkState (wereExecutorsReleased ());
410+ if (downloadCallback != null ) {
411+ downloadCallback .maybeSubmitPendingDownloadRequest ();
412+ }
413+ });
414+ }
415+ }
416+ }
417+
418+ public boolean wereExecutorsReleased () {
419+ synchronized (ReleasableExecutorSupplier .this ) {
420+ return executorCount == 0 ;
421+ }
422+ }
423+ }
424+
355425 private final class DownloadCallback implements DownloadHelper .Callback {
356426
357427 private final Object lock ;
358428 private final long startPositionMs ;
359429 private final long durationMs ;
430+ @ Nullable private final ReleasableExecutorSupplier releasableExecutorSupplier ;
360431 private final DownloadHelper downloadHelper ;
361432
362433 private boolean isPreparationOngoing ;
434+ @ Nullable private DownloadRequest pendingDownloadRequest ;
363435 @ Nullable private Downloader downloader ;
364436 @ Nullable private Task downloaderTask ;
365437
@@ -371,7 +443,16 @@ public DownloadCallback(long startPositionMs, long durationMs) {
371443 this .lock = new Object ();
372444 this .startPositionMs = startPositionMs ;
373445 this .durationMs = durationMs ;
374- this .downloadHelper = downloadHelperSupplier .get ();
446+ if (testMediaSourceFactory != null ) {
447+ this .releasableExecutorSupplier = null ;
448+ this .downloadHelper =
449+ downloadHelperFactory .create (testMediaSourceFactory .createMediaSource (mediaItem ));
450+ } else {
451+ this .releasableExecutorSupplier = new ReleasableExecutorSupplier (preCacheHandler );
452+ downloadHelperFactory .setLoadExecutor (releasableExecutorSupplier );
453+ this .downloadHelper = downloadHelperFactory .create (mediaItem );
454+ this .releasableExecutorSupplier .setDownloadCallback (this );
455+ }
375456 this .isPreparationOngoing = true ;
376457 this .downloadHelper .prepare (this );
377458 }
@@ -386,14 +467,11 @@ public void onPrepared(DownloadHelper helper, boolean tracksInfoAvailable) {
386467 downloadHelper .release ();
387468 MediaItem updatedMediaItem = downloadRequest .toMediaItem (mediaItem .buildUpon ());
388469 notifyListeners (listener -> listener .onPrepared (mediaItem , updatedMediaItem ));
389- downloader = downloaderFactory .createDownloader (downloadRequest );
390- downloaderTask =
391- new Task (
392- downloader ,
393- /* isRemove= */ false ,
394- DEFAULT_MIN_RETRY_COUNT ,
395- /* downloadCallback= */ this );
396- downloaderTask .start ();
470+ pendingDownloadRequest = downloadRequest ;
471+ if (releasableExecutorSupplier == null
472+ || releasableExecutorSupplier .wereExecutorsReleased ()) {
473+ maybeSubmitPendingDownloadRequest ();
474+ }
397475 }
398476
399477 @ Override
@@ -405,6 +483,21 @@ public void onPrepareError(DownloadHelper helper, IOException e) {
405483 notifyListeners (listener -> listener .onPrepareError (mediaItem , e ));
406484 }
407485
486+ public void maybeSubmitPendingDownloadRequest () {
487+ checkState (Looper .myLooper () == preCacheHandler .getLooper ());
488+ if (pendingDownloadRequest != null ) {
489+ downloader = downloaderFactory .createDownloader (pendingDownloadRequest );
490+ downloaderTask =
491+ new Task (
492+ downloader ,
493+ /* isRemove= */ false ,
494+ DEFAULT_MIN_RETRY_COUNT ,
495+ /* downloadCallback= */ this );
496+ downloaderTask .start ();
497+ pendingDownloadRequest = null ;
498+ }
499+ }
500+
408501 public void onDownloadStopped (Task task ) {
409502 preCacheHandler .post (
410503 () -> {
@@ -440,6 +533,7 @@ public void cancel(boolean removeCachedContent) {
440533 synchronized (lock ) {
441534 isCanceled = true ;
442535 }
536+ pendingDownloadRequest = null ;
443537 downloadHelper .release ();
444538 if (downloaderTask != null && downloaderTask .isRemove ) {
445539 return ;
0 commit comments