Media Data library¶
This library contains the implementation of the repositories defined in the media domain library, using Media3 and an internal database as data sources.
It also exposes its data sources classes so they can be used by your custom repositories.
MediaDownloadService¶
An implementation of Media3’s DownloadService
that, in conjunction with auxiliary
classes, will monitor the state and progress of media downloads, and update the information in the
internal database.
Usage¶
-
Add your own implementation of the service, extending
MediaDownloadService
; -
Add your service implementation to your app’s
AndroidManifest.xml
:<service android:name="MediaDownloadServiceImpl" android:exported="false"> <intent-filter> <action android:name="com.google.android.exoplayer.downloadService.action.RESTART"/> <category android:name="android.intent.category.DEFAULT"/> </intent-filter> </service>
Media Toolkit implementation¶
DownloadManagerListener¶
The DownloadManagerListener
is an implementation of listener for DownloadManager events which can also get notified of
DownloadService creation and destruction events.
This class persists information such as the id and the download status in the local database MediaDownloadLocalDataSource
.
override fun onDownloadChanged(
downloadManager: DownloadManager,
download: Download,
finalException: Exception?
) {
coroutineScope.launch {
val mediaId = download.request.id
val status = MediaDownloadEntityStatusMapper.map(download.state)
if (status == MediaDownloadEntityStatus.Downloaded) {
mediaDownloadLocalDataSource.setDownloaded(mediaId)
} else {
mediaDownloadLocalDataSource.updateStatus(mediaId, status)
}
}
downloadProgressMonitor.start(downloadManager)
}
override fun onDownloadRemoved(downloadManager: DownloadManager, download: Download) {
coroutineScope.launch {
val mediaId = download.request.id
mediaDownloadLocalDataSource.delete(mediaId)
}
}
DownloadProgressMonitor¶
The DownloadProgressMonitor
monitors the status of the download by polling the DownloadManager
and persists the progress in the local database MediaDownloadLocalDataSource
.
private fun update(downloadManager: DownloadManager) {
coroutineScope.launch {
val downloads = mediaDownloadLocalDataSource.getAllDownloading()
if (downloads.isNotEmpty()) {
for (it in downloads) {
downloadManager.downloadIndex.getDownload(it.mediaId)?.let { download ->
mediaDownloadLocalDataSource.updateProgress(
mediaId = download.request.id,
progress = download.percentDownloaded
.coerceAtLeast(DOWNLOAD_PROGRESS_START),
size = download.contentLength
)
}
}
} else {
stop()
}
}
if (running) {
handler.removeCallbacksAndMessages(null)
handler.postDelayed({ update(downloadManager) }, UPDATE_INTERVAL_MILLIS)
}
}
UI Implementation¶
The PlaylistsDownloadScreen is composed by the MediaContent
and the ButtonContent
.
The MediaContent
displays the content that is being downloaded or already downloaded. For content that is being downloaded,
we display the download progress for each track in place of the artist name in the secondaryLabel
val secondaryLabel = when (downloadMediaUiModel) {
is DownloadMediaUiModel.Downloading -> {
when (downloadMediaUiModel.progress) {
is DownloadMediaUiModel.Progress.Waiting -> stringResource(
id = R.string.horologist_playlist_download_download_progress_waiting
)
is DownloadMediaUiModel.Progress.InProgress -> when (downloadMediaUiModel.size) {
is DownloadMediaUiModel.Size.Known -> {
val size = Formatter.formatShortFileSize(
LocalContext.current,
downloadMediaUiModel.size.sizeInBytes
)
stringResource(
id = R.string.horologist_playlist_download_download_progress_known_size,
downloadMediaUiModel.progress.progress,
size
)
}
DownloadMediaUiModel.Size.Unknown -> stringResource(
id = R.string.horologist_playlist_download_download_progress_unknown_size,
downloadMediaUiModel.progress.progress
)
}
}
}
is DownloadMediaUiModel.Downloaded -> downloadMediaUiModel.artist
is DownloadMediaUiModel.NotDownloaded -> downloadMediaUiModel.artist
}
The ButtonContent
displays either a download chip or three buttons to delete, shuffle and play the content. While content is being downloaded,
we show an animation on the first button on the left to track progress .
if (state.downloadMediaListState == PlaylistDownloadScreenState.Loaded.DownloadMediaListState.None) {
if (state.downloadsProgress is DownloadsProgress.InProgress) {
StandardChip(
label = stringResource(id = R.string.horologist_playlist_download_button_cancel),
onClick = { onCancelDownloadButtonClick(state.collectionModel) },
modifier = Modifier.padding(bottom = 16.dp),
icon = Icons.Default.Close
)
} else {
StandardChip(
label = stringResource(id = R.string.horologist_playlist_download_button_download),
onClick = { onDownloadButtonClick(state.collectionModel) },
modifier = Modifier.padding(bottom = 16.dp),
icon = Icons.Default.Download
)
}
} else {
Row(
modifier = Modifier
.padding(bottom = 16.dp)
.height(52.dp),
verticalAlignment = CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp, CenterHorizontally)
) {
FirstButton(
downloadMediaListState = state.downloadMediaListState,
downloadsProgress = state.downloadsProgress,
collectionModel = state.collectionModel,
onDownloadButtonClick = onDownloadButtonClick,
onCancelDownloadButtonClick = onCancelDownloadButtonClick,
onDownloadCompletedButtonClick = onDownloadCompletedButtonClick,
modifier = Modifier
.weight(weight = 0.3F, fill = false)
)
StandardButton(
imageVector = Icons.Default.Shuffle,
contentDescription = stringResource(id = R.string.horologist_playlist_download_button_shuffle_content_description),
onClick = { onShuffleButtonClick(state.collectionModel) },
modifier = Modifier
.weight(weight = 0.3F, fill = false)
)
StandardButton(
imageVector = Icons.Filled.PlayArrow,
contentDescription = stringResource(id = R.string.horologist_playlist_download_button_play_content_description),
onClick = { onPlayButtonClick(state.collectionModel) },
modifier = Modifier
.weight(weight = 0.3F, fill = false)
)
}
}
Result¶
Download screens: