Skip to content

Commit

Permalink
[APT-9577] Add DRM for downloads for offline usage : start downloadin…
Browse files Browse the repository at this point in the history
…g DRM license when download an MPEG-Dash audio

Create new helper DrmLicenseDownloader that is responsible for downloading the DRM license (and persist it on the device) for a DRM-protected content.
Once the DRM license is downloaded, we also persist separately its key ID, which is the ID used to retrieve the downloaded license from storage for playback.

When we start a new download, use this helper to download the DRM license to local storage. This currently only supports MPEG-Dash audio format
  • Loading branch information
rubeus90 committed Feb 6, 2024
1 parent 214dc60 commit 73539d9
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.scribd.armadillo.Constants
import com.scribd.armadillo.HeadersStore
import com.scribd.armadillo.StateStore
import com.scribd.armadillo.actions.ErrorAction
import com.scribd.armadillo.download.drm.OfflineDrmManager
import com.scribd.armadillo.error.DownloadServiceLaunchedInBackground
import com.scribd.armadillo.extensions.encodeInByteArray
import com.scribd.armadillo.extensions.toUri
Expand All @@ -39,15 +40,21 @@ internal interface DownloadEngine {
* Starts the [DownloadService] when necessary
*/
@Singleton
internal class ExoplayerDownloadEngine @Inject constructor(private val context: Context,
private val downloadHeadersStore: HeadersStore,
private val downloadService: Class<out DownloadService>,
private val downloadManager: DownloadManager,
private val downloadTracker: DownloadTracker,
private val stateModifier: StateStore.Modifier) : DownloadEngine {
internal class ExoplayerDownloadEngine @Inject constructor(
private val context: Context,
private val downloadHeadersStore: HeadersStore,
private val downloadService: Class<out DownloadService>,
private val downloadManager: DownloadManager,
private val downloadTracker: DownloadTracker,
private val stateModifier: StateStore.Modifier,
private val offlineDrmManager: OfflineDrmManager,
) : DownloadEngine {
override fun init() = downloadTracker.init()

override fun download(audiobook: AudioPlayable) {
// Download DRM license for offline use
offlineDrmManager.downloadDrmForOffline(audiobook)

val downloadHelper = downloadHelper(context, audiobook.request)

downloadHelper.prepare(object : DownloadHelper.Callback {
Expand Down Expand Up @@ -96,6 +103,7 @@ internal class ExoplayerDownloadEngine @Inject constructor(private val context:
C.TYPE_HLS,
C.TYPE_DASH ->
DownloadHelper.forMediaItem(context, mediaItem, renderersFactory, DefaultDataSource.Factory(context, dataSourceFactory))

C.TYPE_OTHER -> DownloadHelper.forMediaItem(context, mediaItem)
else -> throw IllegalStateException("Unsupported type: $type")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.scribd.armadillo.download.drm

import android.content.Context
import android.net.Uri
import com.google.android.exoplayer2.drm.DrmSessionEventListener
import com.google.android.exoplayer2.drm.OfflineLicenseHelper
import com.google.android.exoplayer2.source.dash.DashUtil
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
import com.google.android.exoplayer2.util.Log
import com.scribd.armadillo.Constants
import com.scribd.armadillo.encryption.SecureStorage
import com.scribd.armadillo.error.DrmDownloadException
import com.scribd.armadillo.models.AudioPlayable
import com.scribd.armadillo.models.DrmType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton

@Singleton
internal class DashDrmLicenseDownloader @Inject constructor(private val context: Context,
@Named(Constants.DI.GLOBAL_SCOPE) private val globalScope: CoroutineScope,
private val secureStorage: SecureStorage) : DrmLicenseDownloader {

private val drmDataSourceFactory: DefaultHttpDataSource.Factory = DefaultHttpDataSource.Factory().setUserAgent(Constants.getUserAgent(context))
private val audioDataSourceFactory = DefaultHttpDataSource.Factory().setUserAgent(Constants.getUserAgent(context))
private val drmEventDispatcher = DrmSessionEventListener.EventDispatcher()

override fun downloadDrmLicense(audiobook: AudioPlayable) {
globalScope.launch(Dispatchers.IO) {
// Fetch DRM license if the request contains a DRM
val drmKeyId = audiobook.request.drmInfo?.let { drmInfo ->
// Update data source for DRM license to add any DRM-specific request headers
drmDataSourceFactory.apply {
audiobook.request.drmInfo.drmHeaders.takeIf { it.isNotEmpty() }?.let { drmHeaders ->
setDefaultRequestProperties(drmHeaders)
}
}
// Update data source for audio to add custom headers
audioDataSourceFactory.apply {
audiobook.request.headers.takeIf { it.isNotEmpty() }?.let { audioHeaders ->
setDefaultRequestProperties(audioHeaders)
}
}
val audioDataSource = audioDataSourceFactory.createDataSource()

// Create helper to download DRM license
val offlineHelper = when (drmInfo.drmType) {
DrmType.WIDEVINE -> OfflineLicenseHelper.newWidevineInstance(drmInfo.licenseServer, drmDataSourceFactory, drmEventDispatcher)
}
try {
val manifest = DashUtil.loadManifest(audioDataSource, Uri.parse(audiobook.request.url))
val format = DashUtil.loadFormatWithDrmInitData(audioDataSource, manifest.getPeriod(0))
format?.let {
offlineHelper.downloadLicense(format)
}
} catch (e: Exception) {
Log.e(DrmLicenseDownloader.TAG, "Failure to download DRM license for offline usage", e)
throw DrmDownloadException(e)
}
}
// Persist DRM license key ID. This ID will be used to retrieve the DRM key that is needed to decrypt DRM-protected content
drmKeyId?.also {
secureStorage.saveDrmKeyId(context, audiobook.request.url, DrmType.WIDEVINE.name, it)
Log.i(DrmLicenseDownloader.TAG, "DRM license downloaded and ready for offline usage")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.scribd.armadillo.download.drm

import com.scribd.armadillo.models.AudioPlayable

/**
* This is a helper class responsible for downloading the DRM license (and persist it on the device) for a DRM-protected content.
* Aside from the DRM license itself, it's also responsible to persist the DRM key ID, which is the ID used to retrieve the downloaded
* license from storage for playback.
*/
internal interface DrmLicenseDownloader {
companion object {
const val TAG = "DrmLicenseDownloader"
}

fun downloadDrmLicense(audiobook: AudioPlayable)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.scribd.armadillo.download.drm

import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.util.Util
import com.scribd.armadillo.error.DrmContentTypeUnsupportedException
import com.scribd.armadillo.extensions.toUri
import com.scribd.armadillo.models.AudioPlayable
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
internal class OfflineDrmManager @Inject constructor(
private val dashDrmLicenseDownloader: DashDrmLicenseDownloader,
) {
fun downloadDrmForOffline(audiobook: AudioPlayable) {
when (@C.ContentType val type = Util.inferContentType(audiobook.request.url.toUri(), null)) {
C.TYPE_DASH -> dashDrmLicenseDownloader
else -> throw DrmContentTypeUnsupportedException(type)
}.downloadDrmLicense(audiobook)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,15 @@ data class UnknownRendererException(val exception: Exception) : ArmadilloExcepti
override val errorCode: Int = 604
}

/**
* DRM errors
*/

data class DrmContentTypeUnsupportedException(val contentType: Int) : ArmadilloException(exception = Exception()) {
override val errorCode = 700
}

data class DrmDownloadException(val exception: Exception) : ArmadilloException(exception) {
override val errorCode = 701
}

0 comments on commit 73539d9

Please sign in to comment.