From 9feaeec7ccb99468c893dfe5b7fbd12c5ef1c004 Mon Sep 17 00:00:00 2001 From: CappielloAntonio Date: Fri, 31 Dec 2021 21:36:50 +0100 Subject: [PATCH] Initialization of the download logic --- app/src/main/AndroidManifest.xml | 8 + .../play/service/DownloaderService.java | 80 ++++ .../play/service/DownloaderTracker.java | 372 ++++++++++++++++++ .../play/util/DownloadUtil.java | 87 +++- 4 files changed, 526 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/com/cappielloantonio/play/service/DownloaderService.java create mode 100644 app/src/main/java/com/cappielloantonio/play/service/DownloaderTracker.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 87a59a7f..a282a05c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/play/service/DownloaderService.java b/app/src/main/java/com/cappielloantonio/play/service/DownloaderService.java new file mode 100644 index 00000000..a8f4105e --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/service/DownloaderService.java @@ -0,0 +1,80 @@ +package com.cappielloantonio.play.service; + +import android.annotation.SuppressLint; +import android.app.Notification; +import android.content.Context; + +import androidx.annotation.Nullable; +import androidx.media3.common.util.NotificationUtil; +import androidx.media3.common.util.Util; +import androidx.media3.exoplayer.offline.Download; +import androidx.media3.exoplayer.offline.DownloadManager; +import androidx.media3.exoplayer.offline.DownloadNotificationHelper; +import androidx.media3.exoplayer.scheduler.PlatformScheduler; +import androidx.media3.exoplayer.scheduler.Requirements; +import androidx.media3.exoplayer.scheduler.Scheduler; + +import com.cappielloantonio.play.R; +import com.cappielloantonio.play.util.DownloadUtil; + +import java.util.List; + +@SuppressLint("UnsafeOptInUsageError") +public class DownloaderService extends androidx.media3.exoplayer.offline.DownloadService { + + private static final int JOB_ID = 1; + private static final int FOREGROUND_NOTIFICATION_ID = 1; + + @SuppressLint("UnsafeOptInUsageError") + public DownloaderService() { + super(FOREGROUND_NOTIFICATION_ID, DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID, R.string.exo_download_notification_channel_name, 0); + } + + @Override + protected DownloadManager getDownloadManager() { + DownloadManager downloadManager = DownloadUtil.getDownloadManager(this); + DownloadNotificationHelper downloadNotificationHelper = DownloadUtil.getDownloadNotificationHelper(this); + downloadManager.addListener(new TerminalStateNotificationHelper(this, downloadNotificationHelper, FOREGROUND_NOTIFICATION_ID + 1)); + return downloadManager; + } + + @Override + protected Scheduler getScheduler() { + return new PlatformScheduler(this, JOB_ID); + } + + @Override + protected Notification getForegroundNotification(List downloads, @Requirements.RequirementFlags int notMetRequirements) { + return DownloadUtil.getDownloadNotificationHelper(this).buildProgressNotification(this, R.drawable.ic_download, null, null, downloads, notMetRequirements); + } + + + private static final class TerminalStateNotificationHelper implements DownloadManager.Listener { + private final Context context; + private final DownloadNotificationHelper notificationHelper; + + private int nextNotificationId; + + public TerminalStateNotificationHelper(Context context, DownloadNotificationHelper notificationHelper, int firstNotificationId) { + this.context = context.getApplicationContext(); + this.notificationHelper = notificationHelper; + nextNotificationId = firstNotificationId; + } + + @SuppressLint("UnsafeOptInUsageError") + @Override + public void onDownloadChanged(DownloadManager downloadManager, Download download, @Nullable Exception finalException) { + Notification notification; + + if (download.state == Download.STATE_COMPLETED) { + notification = notificationHelper.buildDownloadCompletedNotification(context, R.drawable.ic_check_circle, null, Util.fromUtf8Bytes(download.request.data)); + } else if (download.state == Download.STATE_FAILED) { + notification = notificationHelper.buildDownloadFailedNotification(context, R.drawable.ic_error, null, Util.fromUtf8Bytes(download.request.data)); + } else { + return; + } + + NotificationUtil.setNotification(context, nextNotificationId++, notification); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/play/service/DownloaderTracker.java b/app/src/main/java/com/cappielloantonio/play/service/DownloaderTracker.java new file mode 100644 index 00000000..d05223c8 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/service/DownloaderTracker.java @@ -0,0 +1,372 @@ +package com.cappielloantonio.play.service; + +import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Assertions.checkStateNotNull; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.DialogInterface; +import android.net.Uri; +import android.os.AsyncTask; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentManager; +import androidx.media3.common.DrmInitData; +import androidx.media3.common.Format; +import androidx.media3.common.MediaItem; +import androidx.media3.common.TrackGroup; +import androidx.media3.common.TrackGroupArray; +import androidx.media3.common.util.Log; +import androidx.media3.common.util.Util; +import androidx.media3.datasource.HttpDataSource; +import androidx.media3.exoplayer.RenderersFactory; +import androidx.media3.exoplayer.drm.DrmSession; +import androidx.media3.exoplayer.drm.DrmSessionEventListener; +import androidx.media3.exoplayer.drm.OfflineLicenseHelper; +import androidx.media3.exoplayer.offline.Download; +import androidx.media3.exoplayer.offline.DownloadCursor; +import androidx.media3.exoplayer.offline.DownloadHelper; +import androidx.media3.exoplayer.offline.DownloadIndex; +import androidx.media3.exoplayer.offline.DownloadManager; +import androidx.media3.exoplayer.offline.DownloadRequest; +import androidx.media3.exoplayer.offline.DownloadService; +import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; +import androidx.media3.exoplayer.trackselection.MappingTrackSelector; + +import java.io.IOException; +import java.util.HashMap; +import java.util.concurrent.CopyOnWriteArraySet; + +public class DownloaderTracker { + public interface Listener { + void onDownloadsChanged(); + } + + private static final String TAG = "DownloadTracker"; + + private final Context context; + private final HttpDataSource.Factory httpDataSourceFactory; + private final CopyOnWriteArraySet listeners; + private final HashMap downloads; + private final DownloadIndex downloadIndex; + private final DefaultTrackSelector.Parameters trackSelectorParameters; + + @Nullable + private StartDownloadDialogHelper startDownloadDialogHelper; + + @SuppressLint("UnsafeOptInUsageError") + public DownloaderTracker(Context context, HttpDataSource.Factory httpDataSourceFactory, DownloadManager downloadManager) { + this.context = context.getApplicationContext(); + this.httpDataSourceFactory = httpDataSourceFactory; + + listeners = new CopyOnWriteArraySet<>(); + downloads = new HashMap<>(); + downloadIndex = downloadManager.getDownloadIndex(); + trackSelectorParameters = DownloadHelper.getDefaultTrackSelectorParameters(context); + + downloadManager.addListener(new DownloadManagerListener()); + loadDownloads(); + } + + @SuppressLint("UnsafeOptInUsageError") + public void addListener(Listener listener) { + checkNotNull(listener); + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + @SuppressLint("UnsafeOptInUsageError") + public boolean isDownloaded(MediaItem mediaItem) { + @Nullable Download download = downloads.get(checkNotNull(mediaItem.localConfiguration).uri); + return download != null && download.state != Download.STATE_FAILED; + } + + @Nullable + public DownloadRequest getDownloadRequest(Uri uri) { + @Nullable Download download = downloads.get(uri); + return download != null && download.state != Download.STATE_FAILED ? download.request : null; + } + + @SuppressLint("UnsafeOptInUsageError") + public void toggleDownload(FragmentManager fragmentManager, MediaItem mediaItem, RenderersFactory renderersFactory) { + @Nullable Download download = downloads.get(checkNotNull(mediaItem.localConfiguration).uri); + if (download != null && download.state != Download.STATE_FAILED) { + androidx.media3.exoplayer.offline.DownloadService.sendRemoveDownload(context, DownloaderService.class, download.request.id, false); + } else { + if (startDownloadDialogHelper != null) { + startDownloadDialogHelper.release(); + } + startDownloadDialogHelper = new StartDownloadDialogHelper(fragmentManager, DownloadHelper.forMediaItem(context, mediaItem, renderersFactory, httpDataSourceFactory), mediaItem); + } + } + + @SuppressLint("UnsafeOptInUsageError") + private void loadDownloads() { + try (DownloadCursor loadedDownloads = downloadIndex.getDownloads()) { + while (loadedDownloads.moveToNext()) { + Download download = loadedDownloads.getDownload(); + downloads.put(download.request.uri, download); + } + } catch (IOException e) { + Log.w(TAG, "Failed to query downloads", e); + } + } + + private class DownloadManagerListener implements DownloadManager.Listener { + @Override + public void onDownloadChanged(DownloadManager downloadManager, Download download, @Nullable Exception finalException) { + downloads.put(download.request.uri, download); + for (Listener listener : listeners) { + listener.onDownloadsChanged(); + } + } + + @Override + public void onDownloadRemoved(DownloadManager downloadManager, Download download) { + downloads.remove(download.request.uri); + for (Listener listener : listeners) { + listener.onDownloadsChanged(); + } + } + } + + private final class StartDownloadDialogHelper implements DownloadHelper.Callback, DialogInterface.OnClickListener, DialogInterface.OnDismissListener { + private final FragmentManager fragmentManager; + private final DownloadHelper downloadHelper; + private final MediaItem mediaItem; + + private TrackSelectionDialog trackSelectionDialog; + private MappingTrackSelector.MappedTrackInfo mappedTrackInfo; + private WidevineOfflineLicenseFetchTask widevineOfflineLicenseFetchTask; + @Nullable + private byte[] keySetId; + + @SuppressLint("UnsafeOptInUsageError") + public StartDownloadDialogHelper(FragmentManager fragmentManager, DownloadHelper downloadHelper, MediaItem mediaItem) { + this.fragmentManager = fragmentManager; + this.downloadHelper = downloadHelper; + this.mediaItem = mediaItem; + downloadHelper.prepare(this); + } + + public void release() { + downloadHelper.release(); + if (trackSelectionDialog != null) { + trackSelectionDialog.dismiss(); + } + if (widevineOfflineLicenseFetchTask != null) { + widevineOfflineLicenseFetchTask.cancel(false); + } + } + + @SuppressLint("UnsafeOptInUsageError") + @Override + public void onPrepared(DownloadHelper helper) { + @Nullable Format format = getFirstFormatWithDrmInitData(helper); + if (format == null) { + onDownloadPrepared(helper); + return; + } + + // The content is DRM protected. We need to acquire an offline license. + if (Util.SDK_INT < 18) { + Toast.makeText(context, R.string.error_drm_unsupported_before_api_18, Toast.LENGTH_LONG).show(); + Log.e(TAG, "Downloading DRM protected content is not supported on API versions below 18"); + return; + } + + if (!hasSchemaData(format.drmInitData)) { + Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG).show(); + Log.e(TAG, "Downloading content where DRM scheme data is not located in the manifest is not supported"); + return; + } + + widevineOfflineLicenseFetchTask = new WidevineOfflineLicenseFetchTask(format, mediaItem.localConfiguration.drmConfiguration, httpDataSourceFactory, this, helper); + widevineOfflineLicenseFetchTask.execute(); + } + + @SuppressLint("UnsafeOptInUsageError") + @Override + public void onPrepareError(DownloadHelper helper, IOException e) { + boolean isLiveContent = e instanceof DownloadHelper.LiveContentUnsupportedException; + int toastStringId = isLiveContent ? R.string.download_live_unsupported : R.string.download_start_error; + String logMessage = isLiveContent ? "Downloading live content unsupported" : "Failed to start download"; + Toast.makeText(context, toastStringId, Toast.LENGTH_LONG).show(); + Log.e(TAG, logMessage, e); + } + + // DialogInterface.OnClickListener implementation. + + @SuppressLint("UnsafeOptInUsageError") + @Override + public void onClick(DialogInterface dialog, int which) { + for (int periodIndex = 0; periodIndex < downloadHelper.getPeriodCount(); periodIndex++) { + downloadHelper.clearTrackSelections(periodIndex); + for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { + if (!trackSelectionDialog.getIsDisabled(i)) { + downloadHelper.addTrackSelectionForSingleRenderer(periodIndex, i, trackSelectorParameters, trackSelectionDialog.getOverrides(i)); + } + } + } + + DownloadRequest downloadRequest = buildDownloadRequest(); + + if (downloadRequest.streamKeys.isEmpty()) { + // All tracks were deselected in the dialog. Don't start the download. + return; + } + startDownload(downloadRequest); + } + + // DialogInterface.OnDismissListener implementation. + + @SuppressLint("UnsafeOptInUsageError") + @Override + public void onDismiss(DialogInterface dialogInterface) { + trackSelectionDialog = null; + downloadHelper.release(); + } + + // Internal methods. + + /** + * Returns the first {@link Format} with a non-null {@link Format#drmInitData} found in the + * content's tracks, or null if none is found. + */ + @SuppressLint("UnsafeOptInUsageError") + @Nullable + private Format getFirstFormatWithDrmInitData(DownloadHelper helper) { + for (int periodIndex = 0; periodIndex < helper.getPeriodCount(); periodIndex++) { + MappingTrackSelector.MappedTrackInfo mappedTrackInfo = helper.getMappedTrackInfo(periodIndex); + for (int rendererIndex = 0; + rendererIndex < mappedTrackInfo.getRendererCount(); + rendererIndex++) { + TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); + for (int trackGroupIndex = 0; trackGroupIndex < trackGroups.length; trackGroupIndex++) { + TrackGroup trackGroup = trackGroups.get(trackGroupIndex); + for (int formatIndex = 0; formatIndex < trackGroup.length; formatIndex++) { + Format format = trackGroup.getFormat(formatIndex); + if (format.drmInitData != null) { + return format; + } + } + } + } + } + return null; + } + + private void onOfflineLicenseFetched(DownloadHelper helper, byte[] keySetId) { + this.keySetId = keySetId; + onDownloadPrepared(helper); + } + + private void onOfflineLicenseFetchedError(DrmSession.DrmSessionException e) { + Toast.makeText(context, R.string.download_start_error_offline_license, Toast.LENGTH_LONG) + .show(); + Log.e(TAG, "Failed to fetch offline DRM license", e); + } + + @SuppressLint("UnsafeOptInUsageError") + private void onDownloadPrepared(DownloadHelper helper) { + if (helper.getPeriodCount() == 0) { + Log.d(TAG, "No periods found. Downloading entire stream."); + startDownload(); + downloadHelper.release(); + return; + } + + mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0); + + if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) { + Log.d(TAG, "No dialog content. Downloading entire stream."); + startDownload(); + downloadHelper.release(); + return; + } + + trackSelectionDialog = TrackSelectionDialog.createForMappedTrackInfoAndParameters(R.string.exo_download_description, mappedTrackInfo, trackSelectorParameters, false, true, this, this); + + trackSelectionDialog.show(fragmentManager, null); + } + + /** + * Returns whether any the {@link DrmInitData.SchemeData} contained in {@code drmInitData} has + * non-null {@link DrmInitData.SchemeData#data}. + */ + @SuppressLint("UnsafeOptInUsageError") + private boolean hasSchemaData(DrmInitData drmInitData) { + for (int i = 0; i < drmInitData.schemeDataCount; i++) { + if (drmInitData.get(i).hasData()) { + return true; + } + } + return false; + } + + private void startDownload() { + startDownload(buildDownloadRequest()); + } + + @SuppressLint("UnsafeOptInUsageError") + private void startDownload(DownloadRequest downloadRequest) { + DownloadService.sendAddDownload(context, DownloaderService.class, downloadRequest, false); + } + + @SuppressLint("UnsafeOptInUsageError") + private DownloadRequest buildDownloadRequest() { + return downloadHelper.getDownloadRequest(Util.getUtf8Bytes(checkNotNull(mediaItem.mediaMetadata.title.toString()))).copyWithKeySetId(keySetId); + } + } + + private static final class WidevineOfflineLicenseFetchTask extends AsyncTask { + private final Format format; + private final MediaItem.DrmConfiguration drmConfiguration; + private final HttpDataSource.Factory httpDataSourceFactory; + private final StartDownloadDialogHelper dialogHelper; + private final DownloadHelper downloadHelper; + + @Nullable + private byte[] keySetId; + @Nullable + private DrmSession.DrmSessionException drmSessionException; + + public WidevineOfflineLicenseFetchTask(Format format, MediaItem.DrmConfiguration drmConfiguration, HttpDataSource.Factory httpDataSourceFactory, StartDownloadDialogHelper dialogHelper, DownloadHelper downloadHelper) { + this.format = format; + this.drmConfiguration = drmConfiguration; + this.httpDataSourceFactory = httpDataSourceFactory; + this.dialogHelper = dialogHelper; + this.downloadHelper = downloadHelper; + } + + @SuppressLint("UnsafeOptInUsageError") + @Override + protected Void doInBackground(Void... voids) { + OfflineLicenseHelper offlineLicenseHelper = OfflineLicenseHelper.newWidevineInstance(drmConfiguration.licenseUri.toString(), drmConfiguration.forceDefaultLicenseUri, httpDataSourceFactory, drmConfiguration.licenseRequestHeaders, new DrmSessionEventListener.EventDispatcher()); + + try { + keySetId = offlineLicenseHelper.downloadLicense(format); + } catch (DrmSession.DrmSessionException e) { + drmSessionException = e; + } finally { + offlineLicenseHelper.release(); + } + + return null; + } + + @SuppressLint("UnsafeOptInUsageError") + @Override + protected void onPostExecute(Void aVoid) { + if (drmSessionException != null) { + dialogHelper.onOfflineLicenseFetchedError(drmSessionException); + } else { + dialogHelper.onOfflineLicenseFetched(downloadHelper, checkStateNotNull(keySetId)); + } + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/play/util/DownloadUtil.java b/app/src/main/java/com/cappielloantonio/play/util/DownloadUtil.java index e7c532aa..9e8bae81 100644 --- a/app/src/main/java/com/cappielloantonio/play/util/DownloadUtil.java +++ b/app/src/main/java/com/cappielloantonio/play/util/DownloadUtil.java @@ -2,67 +2,98 @@ package com.cappielloantonio.play.util; import android.annotation.SuppressLint; import android.content.Context; -import android.util.Log; +import androidx.media3.common.util.Log; import androidx.media3.database.DatabaseProvider; -import androidx.media3.database.ExoDatabaseProvider; +import androidx.media3.database.StandaloneDatabaseProvider; +import androidx.media3.datasource.DataSource; +import androidx.media3.datasource.DefaultDataSource; +import androidx.media3.datasource.DefaultHttpDataSource; import androidx.media3.datasource.HttpDataSource; import androidx.media3.datasource.cache.Cache; +import androidx.media3.datasource.cache.CacheDataSource; import androidx.media3.datasource.cache.NoOpCacheEvictor; import androidx.media3.datasource.cache.SimpleCache; +import androidx.media3.exoplayer.DefaultRenderersFactory; +import androidx.media3.exoplayer.RenderersFactory; import androidx.media3.exoplayer.offline.ActionFileUpgradeUtil; import androidx.media3.exoplayer.offline.DefaultDownloadIndex; import androidx.media3.exoplayer.offline.DownloadManager; import androidx.media3.exoplayer.offline.DownloadNotificationHelper; +import com.cappielloantonio.play.service.DownloaderTracker; + import java.io.File; import java.io.IOException; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; -import java.util.Map; import java.util.concurrent.Executors; public final class DownloadUtil { public static final String DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel"; + private static final String TAG = "DemoUtil"; private static final String DOWNLOAD_ACTION_FILE = "actions"; private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions"; private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads"; + private static DataSource.Factory dataSourceFactory; private static HttpDataSource.Factory httpDataSourceFactory; private static DatabaseProvider databaseProvider; private static File downloadDirectory; private static Cache downloadCache; private static DownloadManager downloadManager; + private static DownloaderTracker downloaderTracker; private static DownloadNotificationHelper downloadNotificationHelper; - public static synchronized HttpDataSource.Factory getHttpDataSourceFactory() { + /** + * Returns whether extension renderers should be used. + */ + public static boolean useExtensionRenderers() { + return false; + } + + @SuppressLint("UnsafeOptInUsageError") + public static RenderersFactory buildRenderersFactory(Context context, boolean preferExtensionRenderer) { + @DefaultRenderersFactory.ExtensionRendererMode + int extensionRendererMode = useExtensionRenderers() + ? (preferExtensionRenderer + ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER + : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON) + : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF; + + return new DefaultRenderersFactory(context.getApplicationContext()).setExtensionRendererMode(extensionRendererMode); + } + + public static synchronized HttpDataSource.Factory getHttpDataSourceFactory(Context context) { if (httpDataSourceFactory == null) { CookieManager cookieManager = new CookieManager(); cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); CookieHandler.setDefault(cookieManager); - httpDataSourceFactory = new HttpDataSource.Factory() { - @Override - public HttpDataSource createDataSource() { - return null; - } - - @Override - public HttpDataSource.Factory setDefaultRequestProperties(Map defaultRequestProperties) { - return null; - } - }; + httpDataSourceFactory = new DefaultHttpDataSource.Factory(); } + return httpDataSourceFactory; } + public static synchronized DataSource.Factory getDataSourceFactory(Context context) { + if (dataSourceFactory == null) { + context = context.getApplicationContext(); + DefaultDataSource.Factory upstreamFactory = new DefaultDataSource.Factory(context, getHttpDataSourceFactory(context)); + dataSourceFactory = buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context)); + } + + return dataSourceFactory; + } + @SuppressLint("UnsafeOptInUsageError") public static synchronized DownloadNotificationHelper getDownloadNotificationHelper(Context context) { if (downloadNotificationHelper == null) { downloadNotificationHelper = new DownloadNotificationHelper(context, DOWNLOAD_NOTIFICATION_CHANNEL_ID); } + return downloadNotificationHelper; } @@ -71,13 +102,17 @@ public final class DownloadUtil { return downloadManager; } + public static synchronized DownloaderTracker getDownloadTracker(Context context) { + ensureDownloadManagerInitialized(context); + return downloaderTracker; + } + @SuppressLint("UnsafeOptInUsageError") - public static synchronized Cache getDownloadCache(Context context) { + private static synchronized Cache getDownloadCache(Context context) { if (downloadCache == null) { File downloadContentDirectory = new File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY); downloadCache = new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider(context)); } - return downloadCache; } @@ -85,9 +120,10 @@ public final class DownloadUtil { private static synchronized void ensureDownloadManagerInitialized(Context context) { if (downloadManager == null) { DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(getDatabaseProvider(context)); - upgradeActionFile(context, DOWNLOAD_ACTION_FILE, downloadIndex, false); + upgradeActionFile(context, DOWNLOAD_ACTION_FILE, downloadIndex, /* addNewDownloadsAsCompleted= */ false); upgradeActionFile(context, DOWNLOAD_TRACKER_ACTION_FILE, downloadIndex, true); - downloadManager = new DownloadManager(context, getDatabaseProvider(context), getDownloadCache(context), getHttpDataSourceFactory(), Executors.newFixedThreadPool(6)); + downloadManager = new DownloadManager(context, getDatabaseProvider(context), getDownloadCache(context), getHttpDataSourceFactory(context), Executors.newFixedThreadPool(/* nThreads= */ 6)); + downloaderTracker = new DownloaderTracker(context, getHttpDataSourceFactory(context), downloadManager); } } @@ -103,18 +139,27 @@ public final class DownloadUtil { @SuppressLint("UnsafeOptInUsageError") private static synchronized DatabaseProvider getDatabaseProvider(Context context) { if (databaseProvider == null) { - databaseProvider = new ExoDatabaseProvider(context); + databaseProvider = new StandaloneDatabaseProvider(context); } return databaseProvider; } private static synchronized File getDownloadDirectory(Context context) { if (downloadDirectory == null) { - downloadDirectory = context.getExternalFilesDir(null); + downloadDirectory = context.getExternalFilesDir(/* type= */ null); if (downloadDirectory == null) { downloadDirectory = context.getFilesDir(); } } + return downloadDirectory; } + + private static CacheDataSource.Factory buildReadOnlyCacheDataSource(DataSource.Factory upstreamFactory, Cache cache) { + return new CacheDataSource.Factory() + .setCache(cache) + .setUpstreamDataSourceFactory(upstreamFactory) + .setCacheWriteDataSinkFactory(null) + .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR); + } }