From ca6f5ed29982412229867d196f8db1857ec1973c Mon Sep 17 00:00:00 2001 From: Katherine Blizard <414924+kabliz@users.noreply.github.com> Date: Tue, 17 Sep 2024 15:38:06 -0700 Subject: [PATCH 1/4] [APT-10393] Protect EncryptedSharedPref Instability Throw an exception that will be caught when a device needs DRM but isn't able to safely store it. --- .../com/scribd/armadillo/di/DownloadModule.kt | 69 +++++++++++-------- .../armadillo/encryption/SecureStorage.kt | 36 ++++++---- 2 files changed, 60 insertions(+), 45 deletions(-) diff --git a/Armadillo/src/main/java/com/scribd/armadillo/di/DownloadModule.kt b/Armadillo/src/main/java/com/scribd/armadillo/di/DownloadModule.kt index 8c8270f..d303b30 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/di/DownloadModule.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/di/DownloadModule.kt @@ -121,41 +121,50 @@ internal class DownloadModule { @Singleton @Provides @Named(Constants.DI.STANDARD_SECURE_STORAGE) - fun standardSecureStorage(context: Context): SharedPreferences { - val keys = MasterKeys.getOrCreate( - KeyGenParameterSpec.Builder("armadilloStandard", PURPOSE_ENCRYPT or PURPOSE_DECRYPT) - .setKeySize(256) - .setBlockModes(BLOCK_MODE_GCM) - .setEncryptionPaddings(ENCRYPTION_PADDING_NONE) - .build() - ) - return EncryptedSharedPreferences.create( - "armadillo.standard.secure", - keys, - context, - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM - ) + fun standardSecureStorage(context: Context): SharedPreferences? { + return try { + val keys = MasterKeys.getOrCreate( + KeyGenParameterSpec.Builder("armadilloStandard", PURPOSE_ENCRYPT or PURPOSE_DECRYPT) + .setKeySize(256) + .setBlockModes(BLOCK_MODE_GCM) + .setEncryptionPaddings(ENCRYPTION_PADDING_NONE) + .build() + ) + EncryptedSharedPreferences.create( + "armadillo.standard.secure", + keys, + context, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } catch (ex: Exception) { + null + } } @Singleton @Provides @Named(Constants.DI.DRM_SECURE_STORAGE) - fun drmSecureStorage(context: Context): SharedPreferences { - val keys = MasterKeys.getOrCreate( - KeyGenParameterSpec.Builder("armadillo", PURPOSE_ENCRYPT or PURPOSE_DECRYPT) - .setKeySize(256) - .setBlockModes(BLOCK_MODE_GCM) - .setEncryptionPaddings(ENCRYPTION_PADDING_NONE) - .build() - ) - return EncryptedSharedPreferences.create( - "armadillo.download.secure", - keys, - context, - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM - ) + fun drmSecureStorage(context: Context): SharedPreferences? { + return try { + val keys = MasterKeys.getOrCreate( + KeyGenParameterSpec.Builder("armadillo", PURPOSE_ENCRYPT or PURPOSE_DECRYPT) + .setKeySize(256) + .setBlockModes(BLOCK_MODE_GCM) + .setEncryptionPaddings(ENCRYPTION_PADDING_NONE) + .build() + ) + EncryptedSharedPreferences.create( + "armadillo.download.secure", + keys, + context, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } + catch (ex: Exception) { + null + } } @Singleton diff --git a/Armadillo/src/main/java/com/scribd/armadillo/encryption/SecureStorage.kt b/Armadillo/src/main/java/com/scribd/armadillo/encryption/SecureStorage.kt index 140543d..23d4e81 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/encryption/SecureStorage.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/encryption/SecureStorage.kt @@ -5,6 +5,7 @@ import android.content.SharedPreferences import android.util.Base64 import android.util.Log import com.scribd.armadillo.Constants +import com.scribd.armadillo.error.DrmDownloadException import com.scribd.armadillo.models.DrmDownload import com.scribd.armadillo.models.DrmType import kotlinx.serialization.decodeFromString @@ -28,9 +29,9 @@ internal interface SecureStorage { @Singleton internal class ArmadilloSecureStorage @Inject constructor( @Named(Constants.DI.STANDARD_STORAGE) private val legacyStandardStorage: SharedPreferences, - @Named(Constants.DI.STANDARD_SECURE_STORAGE) private val secureStandardStorage: SharedPreferences, + @Named(Constants.DI.STANDARD_SECURE_STORAGE) private val secureStandardStorage: SharedPreferences?, @Named(Constants.DI.DRM_DOWNLOAD_STORAGE) private val legacyDrmStorage: SharedPreferences, - @Named(Constants.DI.DRM_SECURE_STORAGE) private val secureDrmStorage: SharedPreferences + @Named(Constants.DI.DRM_SECURE_STORAGE) private val secureDrmStorage: SharedPreferences? ) : SecureStorage { companion object { const val DOWNLOAD_KEY = "download_key" @@ -41,8 +42,8 @@ internal class ArmadilloSecureStorage @Inject constructor( } override fun downloadSecretKey(context: Context): ByteArray { - return if (secureStandardStorage.contains(DOWNLOAD_KEY)) { - val storedKey = secureDrmStorage.getString(DOWNLOAD_KEY, DEFAULT)!! + return if (secureStandardStorage?.contains(DOWNLOAD_KEY) == true) { + val storedKey = secureDrmStorage?.getString(DOWNLOAD_KEY, DEFAULT)!! if (storedKey == DEFAULT) { Log.e(TAG, "Storage Is Out of Alignment") } @@ -53,13 +54,13 @@ internal class ArmadilloSecureStorage @Inject constructor( if (storedKey == DEFAULT) { Log.e(TAG, "Storage Is Out of Alignment") } - secureStandardStorage.edit().putString(DOWNLOAD_KEY, storedKey).apply() + secureStandardStorage?.edit()?.putString(DOWNLOAD_KEY, storedKey)?.apply() legacyStandardStorage.edit().remove(DOWNLOAD_KEY).apply() storedKey.toSecretByteArray } else { //no key exists anywhere yet createRandomString().also { - secureStandardStorage.edit().putString(DOWNLOAD_KEY, it).apply() + secureStandardStorage?.edit()?.putString(DOWNLOAD_KEY, it)?.apply() }.toSecretByteArray } } @@ -71,48 +72,53 @@ internal class ArmadilloSecureStorage @Inject constructor( } override fun saveDrmDownload(context: Context, audioUrl: String, drmDownload: DrmDownload) { + if(secureDrmStorage == null){ + throw DrmDownloadException(UnsupportedOperationException("This device cannot encrypt downloads")) + } val alias = getDrmDownloadAlias(audioUrl, drmDownload.drmType) val value = Base64.encodeToString(Json.encodeToString(drmDownload).toByteArray(StandardCharsets.UTF_8), Base64.NO_WRAP) - secureDrmStorage.edit().putString(alias, value).apply() + secureDrmStorage.edit()?.putString(alias, value)?.apply() } override fun getDrmDownload(context: Context, audioUrl: String, drmType: DrmType): DrmDownload? { val alias = getDrmDownloadAlias(audioUrl, drmType) - var download = secureDrmStorage.getString(alias, null)?.decodeToDrmDownload() + var download = secureDrmStorage?.getString(alias, null)?.decodeToDrmDownload() if (download == null && legacyDrmStorage.contains(alias)) { //migrate old storage to secure storage val downloadValue = legacyDrmStorage.getString(alias, null) download = downloadValue?.decodeToDrmDownload() - secureDrmStorage.edit().putString(alias, downloadValue).apply() - legacyDrmStorage.edit().remove(alias).apply() + if(secureDrmStorage != null) { + secureDrmStorage.edit()?.putString(alias, downloadValue)?.apply() + legacyDrmStorage.edit().remove(alias).apply() + } } return download } override fun getAllDrmDownloads(context: Context): Map { - val drmDownloads = secureDrmStorage.all.keys.mapNotNull { alias -> + val drmDownloads = secureDrmStorage?.all?.keys?.mapNotNull { alias -> secureDrmStorage.getString(alias, null)?.let { drmResult -> alias to drmResult.decodeToDrmDownload() } - }.toMap() + }?.toMap() val legacyDownloads = legacyDrmStorage.all.keys.mapNotNull { alias -> legacyDrmStorage.getString(alias, null)?.let { drmResult -> alias to drmResult.decodeToDrmDownload() } }.toMap() - return drmDownloads.plus(legacyDownloads) + return legacyDownloads.plus(drmDownloads ?: emptyMap()) } override fun removeDrmDownload(context: Context, audioUrl: String, drmType: DrmType) { val alias = getDrmDownloadAlias(audioUrl, drmType) legacyDrmStorage.edit().remove(alias).apply() - secureDrmStorage.edit().remove(alias).apply() + secureDrmStorage?.edit()?.remove(alias)?.apply() } override fun removeDrmDownload(context: Context, key: String) { legacyDrmStorage.edit().remove(key).apply() - secureDrmStorage.edit().remove(key).apply() + secureDrmStorage?.edit()?.remove(key)?.apply() } private val String.toSecretByteArray: ByteArray From 33fd980f9c2420442f4fc3e0ba92fb3ed73f15f3 Mon Sep 17 00:00:00 2001 From: Katherine Blizard <414924+kabliz@users.noreply.github.com> Date: Tue, 17 Sep 2024 17:48:51 -0700 Subject: [PATCH 2/4] [APT-10393] Support Rotating Content URLs Change the internal download tracking to be based on the ID passed in from the client, not by the URL of the audio content. This gives the client full control over the content downloaded by Armadillo, regardless if the URL changes or not at a later time. Beforehand, a changed URL could fail to be found in Armadillo, even though it has been downloaded. These downloads should no longer be lost. Removed unused storage reference in the WidevineSessionEventListener. --- .../armadillo/download/DownloadEngine.kt | 24 +++++++----- .../download/ExoplayerDownloadTracker.kt | 38 +++++++++++-------- .../download/drm/OfflineDrmManager.kt | 10 ++--- .../events/WidevineSessionEventListener.kt | 4 -- .../armadillo/encryption/SecureStorage.kt | 26 ++++++------- .../armadillo/playback/PlaybackEngine.kt | 5 ++- .../mediasource/DashMediaSourceGenerator.kt | 13 +++++-- .../mediasource/DrmMediaSourceHelper.kt | 5 ++- .../mediasource/HlsMediaSourceGenerator.kt | 4 +- .../mediasource/MediaSourceGenerator.kt | 2 +- .../mediasource/MediaSourceRetriever.kt | 8 ++-- .../ProgressiveMediaSourceGenerator.kt | 2 +- RELEASE.md | 4 ++ gradle.properties | 2 +- 14 files changed, 85 insertions(+), 62 deletions(-) diff --git a/Armadillo/src/main/java/com/scribd/armadillo/download/DownloadEngine.kt b/Armadillo/src/main/java/com/scribd/armadillo/download/DownloadEngine.kt index 57a8c46..a1671b6 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/download/DownloadEngine.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/download/DownloadEngine.kt @@ -40,7 +40,7 @@ internal interface DownloadEngine { fun removeDownload(audioPlayable: AudioPlayable) fun removeAllDownloads() fun updateProgress() - fun redownloadDrmLicense(request: AudioPlayable.MediaRequest) + fun redownloadDrmLicense(id: String, request: AudioPlayable.MediaRequest) } /** @@ -70,15 +70,20 @@ internal class ExoplayerDownloadEngine @Inject constructor( scope.launch(errorHandler) { launch { // Download DRM license for offline use - offlineDrmManager.downloadDrmLicenseForOffline(audioPlayable.request) + offlineDrmManager.downloadDrmLicenseForOffline(id = audioPlayable.id.toString(), request = audioPlayable.request) } launch { - val downloadHelper = downloadHelper(context, audioPlayable.request) + val downloadHelper = downloadHelper( + id = audioPlayable.id.toString(), + context = context, + mediaRequest = audioPlayable.request + ) downloadHelper.prepare(object : DownloadHelper.Callback { override fun onPrepared(helper: DownloadHelper) { - val request = helper.getDownloadRequest(audioPlayable.id.encodeInByteArray()) + var request = helper.getDownloadRequest(audioPlayable.id.encodeInByteArray()) + request = request.copyWithId(audioPlayable.id.toString()) try { startDownload(context, request) } catch (e: Exception) { @@ -99,8 +104,8 @@ internal class ExoplayerDownloadEngine @Inject constructor( override fun removeDownload(audioPlayable: AudioPlayable) { scope.launch(errorHandler) { - launch { downloadManager.removeDownload(audioPlayable.request.url) } - launch { offlineDrmManager.removeDownloadedDrmLicense(audioPlayable.request) } + launch { downloadManager.removeDownload(audioPlayable.id.toString()) } + launch { offlineDrmManager.removeDownloadedDrmLicense(id = audioPlayable.id.toString(), request = audioPlayable.request) } } } @@ -113,10 +118,10 @@ internal class ExoplayerDownloadEngine @Inject constructor( override fun updateProgress() = downloadTracker.updateProgress() - override fun redownloadDrmLicense(request: AudioPlayable.MediaRequest) { + override fun redownloadDrmLicense(id: String, request: AudioPlayable.MediaRequest) { scope.launch(errorHandler) { try { - offlineDrmManager.downloadDrmLicenseForOffline(request) + offlineDrmManager.downloadDrmLicenseForOffline(id = id, request = request) } catch (ex: DrmDownloadException){ //continue to try and use old license - a playback error appears elsewhere } @@ -126,7 +131,7 @@ internal class ExoplayerDownloadEngine @Inject constructor( private fun startDownload(context: Context, downloadRequest: DownloadRequest) = DownloadService.sendAddDownload(context, downloadService, downloadRequest, true) - private fun downloadHelper(context: Context, mediaRequest: AudioPlayable.MediaRequest): DownloadHelper { + private fun downloadHelper(id: String, context: Context, mediaRequest: AudioPlayable.MediaRequest): DownloadHelper { val uri = mediaRequest.url.toUri() val renderersFactory = createRenderersFactory(context) val dataSourceFactory = DefaultHttpDataSource.Factory().setUserAgent(Constants.getUserAgent(context)) @@ -139,6 +144,7 @@ internal class ExoplayerDownloadEngine @Inject constructor( } val mediaItem = MediaItem.Builder() .setUri(uri) + .setMediaId(id) .build() return when (@C.ContentType val type = Util.inferContentType(uri)) { C.TYPE_HLS, diff --git a/Armadillo/src/main/java/com/scribd/armadillo/download/ExoplayerDownloadTracker.kt b/Armadillo/src/main/java/com/scribd/armadillo/download/ExoplayerDownloadTracker.kt index b200cd3..a15fb8d 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/download/ExoplayerDownloadTracker.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/download/ExoplayerDownloadTracker.kt @@ -1,6 +1,5 @@ package com.scribd.armadillo.download -import android.net.Uri import android.util.Log import androidx.annotation.VisibleForTesting import com.google.android.exoplayer2.offline.Download @@ -14,7 +13,6 @@ import com.scribd.armadillo.actions.StopTrackingDownloadAction import com.scribd.armadillo.actions.UpdateDownloadAction import com.scribd.armadillo.error.DownloadFailed import com.scribd.armadillo.error.UnableToLoadDownloadInfo -import com.scribd.armadillo.extensions.toUri import com.scribd.armadillo.models.DownloadProgressInfo import com.scribd.armadillo.models.DownloadState import kotlinx.coroutines.CoroutineScope @@ -35,8 +33,8 @@ import javax.inject.Singleton */ internal interface DownloadTracker { fun init() - fun trackDownload(download: ExoplayerDownload) - fun getDownload(uri: Uri): ExoplayerDownload? + fun trackDownload(id: String, download: ExoplayerDownload) + fun getDownload(id: String, uri: String): ExoplayerDownload? fun updateProgress() suspend fun loadDownloads() } @@ -58,7 +56,7 @@ internal class ExoplayerDownloadTracker @Inject constructor( private const val TAG = "DownloadTracker" } - private val downloads = HashMap() + private val downloads = HashMap() private val downloadIndex = downloadManager.downloadIndex private var isInitialized = false @@ -89,8 +87,8 @@ internal class ExoplayerDownloadTracker @Inject constructor( .use { loadedDownloads -> while (loadedDownloads.moveToNext()) { val download = loadedDownloads.download - downloads[download.request.uri] = download - // If we want to resume downloads we should make a call here to the download service to begin download for this uri + downloads[download.request.id] = download + //If we want to resume downloads we should make a call here to the download service to begin download for this id } } } catch (e: IOException) { @@ -102,18 +100,22 @@ internal class ExoplayerDownloadTracker @Inject constructor( } } - override fun trackDownload(download: ExoplayerDownload) { - if (downloads.containsKey(download.request.uri)) { + override fun trackDownload(id: String, download: ExoplayerDownload) { + if (downloads.containsKey(id) || downloads.containsKey(download.request.uri.toString())) { return } - downloads[download.request.uri] = download + downloads[id] = download } - override fun getDownload(uri: Uri): ExoplayerDownload? = downloads[uri] + override fun getDownload(id: String, uri: String): ExoplayerDownload? = downloads[id] ?: downloads[uri] //older usage override fun updateProgress() { downloadManager.currentDownloads.forEach { download -> - downloads[download.request.uri] = download + if(downloads.containsKey(download.request.uri.toString())){ + downloads[download.request.uri.toString()] = download //older usage + } else { + downloads[download.request.id] = download + } TestableDownloadState(download).toDownloadInfo()?.let { dispatchActionsForProgress(it) } @@ -127,7 +129,11 @@ internal class ExoplayerDownloadTracker @Inject constructor( override fun onDownloadChanged(downloadManager: DownloadManager, download: Download, finalException: Exception?) { Log.v(TAG, "onDownloadChanged") - downloads[download.request.uri] = download + if(downloads.containsKey(download.request.uri.toString())){ + downloads[download.request.uri.toString()] = download //older usage + } else { + downloads[download.request.id] = download + } TestableDownloadState(download).toDownloadInfo()?.let { dispatchActionsForProgress(it) } @@ -138,7 +144,8 @@ internal class ExoplayerDownloadTracker @Inject constructor( */ override fun onDownloadRemoved(downloadManager: DownloadManager, download: ExoplayerDownload) { Log.v(TAG, "onDownloadRemoved") - downloads.remove(download.request.uri) + downloads.remove(download.request.id) + downloads.remove(download.request.uri.toString()) //older usage TestableDownloadState(download).toDownloadInfo()?.let { dispatchActionsForProgress(it) } @@ -175,6 +182,7 @@ internal class ExoplayerDownloadTracker @Inject constructor( } private fun stopTracking(downloadInfo: DownloadProgressInfo) { - downloads.remove(downloadInfo.url.toUri()) + downloads.remove(downloadInfo.id.toString()) + downloads.remove(downloadInfo.url) //older usage } } diff --git a/Armadillo/src/main/java/com/scribd/armadillo/download/drm/OfflineDrmManager.kt b/Armadillo/src/main/java/com/scribd/armadillo/download/drm/OfflineDrmManager.kt index 9ade829..859e351 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/download/drm/OfflineDrmManager.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/download/drm/OfflineDrmManager.kt @@ -28,7 +28,7 @@ internal class OfflineDrmManager @Inject constructor( private const val TAG = "OfflineDrmManager" } - suspend fun downloadDrmLicenseForOffline(request: AudioPlayable.MediaRequest) { + suspend fun downloadDrmLicenseForOffline(id: String, request: AudioPlayable.MediaRequest) { withContext(Dispatchers.IO) { request.drmInfo?.let { drmInfo -> val drmResult = when (@C.ContentType val type = Util.inferContentType(request.url.toUri(), null)) { @@ -41,18 +41,18 @@ internal class OfflineDrmManager @Inject constructor( ) // Persist DRM result, which includes the key ID that can be used to retrieve the offline license - secureStorage.saveDrmDownload(context, request.url, drmResult) + secureStorage.saveDrmDownload(context, id, drmResult) Log.i(TAG, "DRM license ready for offline usage") } } } - suspend fun removeDownloadedDrmLicense(request: AudioPlayable.MediaRequest) { + suspend fun removeDownloadedDrmLicense(id: String, request: AudioPlayable.MediaRequest) { withContext(Dispatchers.IO) { request.drmInfo?.let { drmInfo -> - secureStorage.getDrmDownload(context, request.url, drmInfo.drmType)?.let { drmDownload -> + secureStorage.getDrmDownload(context = context, id = id, drmType = drmInfo.drmType)?.let { drmDownload -> // Remove the persisted download info immediately so audio playback would stop using the offline license - secureStorage.removeDrmDownload(context, request.url, drmInfo.drmType) + secureStorage.removeDrmDownload(context = context, id = id, drmType = drmInfo.drmType) // Release the DRM license when (val type = drmDownload.audioType) { diff --git a/Armadillo/src/main/java/com/scribd/armadillo/download/drm/events/WidevineSessionEventListener.kt b/Armadillo/src/main/java/com/scribd/armadillo/download/drm/events/WidevineSessionEventListener.kt index 2c19ee2..c871f37 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/download/drm/events/WidevineSessionEventListener.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/download/drm/events/WidevineSessionEventListener.kt @@ -7,7 +7,6 @@ import com.scribd.armadillo.StateStore import com.scribd.armadillo.actions.LicenseAcquiredAction import com.scribd.armadillo.actions.LicenseReleasedAction import com.scribd.armadillo.di.Injector -import com.scribd.armadillo.encryption.SecureStorage import com.scribd.armadillo.models.DrmType import javax.inject.Inject @@ -17,9 +16,6 @@ internal class WidevineSessionEventListener @Inject internal lateinit var stateStore: StateStore.Modifier - @Inject - internal lateinit var secureStorage: SecureStorage - @Inject internal lateinit var context: Context diff --git a/Armadillo/src/main/java/com/scribd/armadillo/encryption/SecureStorage.kt b/Armadillo/src/main/java/com/scribd/armadillo/encryption/SecureStorage.kt index 23d4e81..c5e68d5 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/encryption/SecureStorage.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/encryption/SecureStorage.kt @@ -19,10 +19,10 @@ import javax.inject.Singleton internal interface SecureStorage { fun downloadSecretKey(context: Context): ByteArray - fun saveDrmDownload(context: Context, audioUrl: String, drmDownload: DrmDownload) - fun getDrmDownload(context: Context, audioUrl: String, drmType: DrmType): DrmDownload? + fun saveDrmDownload(context: Context, id: String, drmDownload: DrmDownload) + fun getDrmDownload(context: Context, id: String, drmType: DrmType): DrmDownload? fun getAllDrmDownloads(context: Context): Map - fun removeDrmDownload(context: Context, audioUrl: String, drmType: DrmType) + fun removeDrmDownload(context: Context, id: String, drmType: DrmType) fun removeDrmDownload(context: Context, key: String) } @@ -43,14 +43,14 @@ internal class ArmadilloSecureStorage @Inject constructor( override fun downloadSecretKey(context: Context): ByteArray { return if (secureStandardStorage?.contains(DOWNLOAD_KEY) == true) { - val storedKey = secureDrmStorage?.getString(DOWNLOAD_KEY, DEFAULT)!! + val storedKey = secureDrmStorage?.getString(DOWNLOAD_KEY, DEFAULT) ?: DEFAULT if (storedKey == DEFAULT) { Log.e(TAG, "Storage Is Out of Alignment") } storedKey.toSecretByteArray } else if(legacyStandardStorage.contains(DOWNLOAD_KEY)) { //migrate to secured version - val storedKey = legacyStandardStorage.getString(DOWNLOAD_KEY, DEFAULT)!! + val storedKey = legacyStandardStorage.getString(DOWNLOAD_KEY, DEFAULT) ?: DEFAULT if (storedKey == DEFAULT) { Log.e(TAG, "Storage Is Out of Alignment") } @@ -71,17 +71,17 @@ internal class ArmadilloSecureStorage @Inject constructor( .joinToString("") } - override fun saveDrmDownload(context: Context, audioUrl: String, drmDownload: DrmDownload) { + override fun saveDrmDownload(context: Context, id: String, drmDownload: DrmDownload) { if(secureDrmStorage == null){ throw DrmDownloadException(UnsupportedOperationException("This device cannot encrypt downloads")) } - val alias = getDrmDownloadAlias(audioUrl, drmDownload.drmType) + val alias = getDrmDownloadAlias(id, drmDownload.drmType) val value = Base64.encodeToString(Json.encodeToString(drmDownload).toByteArray(StandardCharsets.UTF_8), Base64.NO_WRAP) secureDrmStorage.edit()?.putString(alias, value)?.apply() } - override fun getDrmDownload(context: Context, audioUrl: String, drmType: DrmType): DrmDownload? { - val alias = getDrmDownloadAlias(audioUrl, drmType) + override fun getDrmDownload(context: Context, id: String, drmType: DrmType): DrmDownload? { + val alias = getDrmDownloadAlias(id, drmType) var download = secureDrmStorage?.getString(alias, null)?.decodeToDrmDownload() if (download == null && legacyDrmStorage.contains(alias)) { //migrate old storage to secure storage @@ -110,8 +110,8 @@ internal class ArmadilloSecureStorage @Inject constructor( return legacyDownloads.plus(drmDownloads ?: emptyMap()) } - override fun removeDrmDownload(context: Context, audioUrl: String, drmType: DrmType) { - val alias = getDrmDownloadAlias(audioUrl, drmType) + override fun removeDrmDownload(context: Context, id: String, drmType: DrmType) { + val alias = getDrmDownloadAlias(id, drmType) legacyDrmStorage.edit().remove(alias).apply() secureDrmStorage?.edit()?.remove(alias)?.apply() } @@ -130,8 +130,8 @@ internal class ArmadilloSecureStorage @Inject constructor( return keyBytes } - private fun getDrmDownloadAlias(audioUrl: String, drmType: DrmType) = - Base64.encodeToString(audioUrl.toSecretByteArray + drmType.name.toSecretByteArray, Base64.NO_WRAP) + private fun getDrmDownloadAlias(id: String, drmType: DrmType) = + Base64.encodeToString(id.toSecretByteArray + drmType.name.toSecretByteArray, Base64.NO_WRAP) private fun String.decodeToDrmDownload(): DrmDownload = Base64.decode(this, Base64.NO_WRAP).let { resultByteArray -> diff --git a/Armadillo/src/main/java/com/scribd/armadillo/playback/PlaybackEngine.kt b/Armadillo/src/main/java/com/scribd/armadillo/playback/PlaybackEngine.kt index df37281..08d303d 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/playback/PlaybackEngine.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/playback/PlaybackEngine.kt @@ -141,7 +141,10 @@ internal class ExoplayerPlaybackEngine(private var audioPlayable: AudioPlayable) exoPlayer = createExoplayerInstance(context, audioAttributes.exoPlayerAttrs, loadControl) try { - val mediaSource = mediaSourceRetriever.generateMediaSource(audioPlayable.request, context) + val mediaSource = mediaSourceRetriever.generateMediaSource( + mediaId = audioPlayable.id.toString(), + request = audioPlayable.request, + context = context) exoPlayer.setMediaSource(mediaSource) exoPlayer.prepare() diff --git a/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/DashMediaSourceGenerator.kt b/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/DashMediaSourceGenerator.kt index 567dba7..4c41bb7 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/DashMediaSourceGenerator.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/DashMediaSourceGenerator.kt @@ -30,20 +30,25 @@ internal class DashMediaSourceGenerator @Inject constructor( private val drmHandler = Handler(context.mainLooper) - override fun generateMediaSource(context: Context, request: AudioPlayable.MediaRequest): MediaSource { + override fun generateMediaSource(mediaId: String, context: Context, request: AudioPlayable.MediaRequest): MediaSource { if (request.drmInfo != null) { stateStore.dispatch(OpeningLicenseAction(request.drmInfo.drmType)) } val dataSourceFactory = mediaSourceHelper.createDataSourceFactory(context, request) - val download = downloadTracker.getDownload(request.url.toUri()) + val download = downloadTracker.getDownload(id = mediaId, uri = request.url) val isDownloaded = download != null && download.state == Download.STATE_COMPLETED - val mediaItem = drmMediaSourceHelper.createMediaItem(context = context, request = request, isDownload = isDownloaded) + val mediaItem = drmMediaSourceHelper.createMediaItem( + context = context, + id = mediaId, + request = request, + isDownload = isDownloaded + ) return if (isDownloaded) { val drmManager = drmSessionManagerProvider.get(mediaItem) if(request.drmInfo?.drmType == DrmType.WIDEVINE) { - downloadEngine.redownloadDrmLicense(request) + downloadEngine.redownloadDrmLicense(id = mediaId, request = request) } DownloadHelper.createMediaSource(download!!.request, dataSourceFactory, drmManager) } else { diff --git a/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/DrmMediaSourceHelper.kt b/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/DrmMediaSourceHelper.kt index ef4841e..f500ff2 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/DrmMediaSourceHelper.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/DrmMediaSourceHelper.kt @@ -18,6 +18,7 @@ import javax.inject.Singleton internal interface DrmMediaSourceHelper { fun createMediaItem( context: Context, + id: String, request: AudioPlayable.MediaRequest, isDownload: Boolean, ): MediaItem @@ -26,7 +27,7 @@ internal interface DrmMediaSourceHelper { @Singleton internal class DrmMediaSourceHelperImpl @Inject constructor(private val secureStorage: SecureStorage) : DrmMediaSourceHelper { - override fun createMediaItem(context: Context, request: AudioPlayable.MediaRequest, isDownload: Boolean): MediaItem = + override fun createMediaItem(context: Context, id: String, request: AudioPlayable.MediaRequest, isDownload: Boolean): MediaItem = MediaItem.Builder() .setUri(request.url) .apply { @@ -39,7 +40,7 @@ internal class DrmMediaSourceHelperImpl @Inject constructor(private val secureSt // If the content is a download content, use the saved offline DRM key id. // This ID is needed to retrieve the local DRM license for content decryption. if (isDownload) { - secureStorage.getDrmDownload(context, request.url, drmInfo.drmType)?.let { drmDownload -> + secureStorage.getDrmDownload(context = context, id = id, drmType = drmInfo.drmType)?.let { drmDownload -> setKeySetId(drmDownload.drmKeyId) } ?: throw DrmPlaybackException(IllegalStateException("No DRM key id saved for download content")) } diff --git a/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/HlsMediaSourceGenerator.kt b/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/HlsMediaSourceGenerator.kt index 7d40f47..8893c95 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/HlsMediaSourceGenerator.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/HlsMediaSourceGenerator.kt @@ -21,10 +21,10 @@ internal class HlsMediaSourceGenerator @Inject constructor( private val downloadTracker: DownloadTracker) : MediaSourceGenerator { - override fun generateMediaSource(context: Context, request: AudioPlayable.MediaRequest): MediaSource { + override fun generateMediaSource(mediaId: String, context: Context, request: AudioPlayable.MediaRequest): MediaSource { val dataSourceFactory = mediaSourceHelper.createDataSourceFactory(context, request) - downloadTracker.getDownload(request.url.toUri())?.let { + downloadTracker.getDownload(id = mediaId, uri = request.url)?.let { if (it.state != Download.STATE_FAILED) { return DownloadHelper.createMediaSource(it.request, dataSourceFactory) } diff --git a/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/MediaSourceGenerator.kt b/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/MediaSourceGenerator.kt index 5cdaef0..f16b601 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/MediaSourceGenerator.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/MediaSourceGenerator.kt @@ -11,7 +11,7 @@ internal interface MediaSourceGenerator { const val TAG = "MediaSourceGenerator" } - fun generateMediaSource(context: Context, request: AudioPlayable.MediaRequest): MediaSource + fun generateMediaSource(mediaId: String, context: Context, request: AudioPlayable.MediaRequest): MediaSource fun updateMediaSourceHeaders(request: AudioPlayable.MediaRequest) } \ No newline at end of file diff --git a/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/MediaSourceRetriever.kt b/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/MediaSourceRetriever.kt index 51426dc..a312dc1 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/MediaSourceRetriever.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/MediaSourceRetriever.kt @@ -12,8 +12,7 @@ import javax.inject.Inject /** Creates a MediaSource for starting playback in Exoplayer based on what type * of audio content is passed into it. */ interface MediaSourceRetriever { - fun generateMediaSource(request: AudioPlayable.MediaRequest, - context: Context): MediaSource + fun generateMediaSource(mediaId: String, request: AudioPlayable.MediaRequest, context: Context): MediaSource fun updateMediaSourceHeaders(request: AudioPlayable.MediaRequest) } @@ -32,10 +31,11 @@ class MediaSourceRetrieverImpl @Inject constructor(): MediaSourceRetriever { Injector.mainComponent.inject(this) } - override fun generateMediaSource(request: AudioPlayable.MediaRequest, + override fun generateMediaSource(mediaId: String, + request: AudioPlayable.MediaRequest, context: Context): MediaSource { - return buildMediaGenerator(request).generateMediaSource(context, request) + return buildMediaGenerator(request).generateMediaSource(mediaId = mediaId, context = context, request = request) } override fun updateMediaSourceHeaders(request: AudioPlayable.MediaRequest) { diff --git a/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/ProgressiveMediaSourceGenerator.kt b/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/ProgressiveMediaSourceGenerator.kt index 375ad9a..6f4f0b5 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/ProgressiveMediaSourceGenerator.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/playback/mediasource/ProgressiveMediaSourceGenerator.kt @@ -16,7 +16,7 @@ import javax.inject.Inject internal class ProgressiveMediaSourceGenerator @Inject constructor( private val cacheManager: CacheManager) : MediaSourceGenerator { - override fun generateMediaSource(context: Context, request: AudioPlayable.MediaRequest): MediaSource = + override fun generateMediaSource(mediaId: String, context: Context, request: AudioPlayable.MediaRequest): MediaSource = ProgressiveMediaSource.Factory(buildDataSourceFactory(context)).createMediaSource(MediaItem.fromUri(request.url)).also { if (request.drmInfo != null) { Log.e(MediaSourceGenerator.TAG, "Progressive media does not currently support DRM") diff --git a/RELEASE.md b/RELEASE.md index 0d1f930..8d20b0a 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,5 +1,9 @@ # Project Armadillo Release Notes +## 1.6.3 +- Prevents downloaded content with rotating URLs from being lost in the download system after the URL moves. +- Prevents devices that fail to initialize encrypted storage from crashing during initialization. + ## 1.6.2 - Prevents fatal crashing for actions are being performed before the player is initialized diff --git a/gradle.properties b/gradle.properties index cd06b72..d28f589 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ org.gradle.jvmargs=-Xmx1536m # org.gradle.parallel=true PACKAGE_NAME=com.scribd.armadillo GRADLE_PLUGIN_VERSION=7.2.0 -LIBRARY_VERSION=1.6.2 +LIBRARY_VERSION=1.6.3 EXOPLAYER_VERSION=2.19.1 RXJAVA_VERSION=2.2.4 RXANDROID_VERSION=2.0.1 From e1a5348dcbd5d85f90664cb2da470d8cf0c72903 Mon Sep 17 00:00:00 2001 From: Katherine Blizard <414924+kabliz@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:23:40 -0700 Subject: [PATCH 3/4] [APT-10393] Fallback if EncyrSharedPref fails Clears the keystore if the EncryptedSharedPref key is lost, which can happen in some random OEMs. --- .../com/scribd/armadillo/di/DownloadModule.kt | 73 +++++++++---------- .../armadillo/encryption/SecureStorage.kt | 36 ++++----- 2 files changed, 50 insertions(+), 59 deletions(-) diff --git a/Armadillo/src/main/java/com/scribd/armadillo/di/DownloadModule.kt b/Armadillo/src/main/java/com/scribd/armadillo/di/DownloadModule.kt index d303b30..26c6dcb 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/di/DownloadModule.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/di/DownloadModule.kt @@ -38,6 +38,7 @@ import com.scribd.armadillo.exoplayerExternalDirectory import dagger.Module import dagger.Provides import java.io.File +import java.security.KeyStore import javax.inject.Named import javax.inject.Qualifier import javax.inject.Singleton @@ -121,50 +122,46 @@ internal class DownloadModule { @Singleton @Provides @Named(Constants.DI.STANDARD_SECURE_STORAGE) - fun standardSecureStorage(context: Context): SharedPreferences? { - return try { - val keys = MasterKeys.getOrCreate( - KeyGenParameterSpec.Builder("armadilloStandard", PURPOSE_ENCRYPT or PURPOSE_DECRYPT) - .setKeySize(256) - .setBlockModes(BLOCK_MODE_GCM) - .setEncryptionPaddings(ENCRYPTION_PADDING_NONE) - .build() - ) - EncryptedSharedPreferences.create( - "armadillo.standard.secure", - keys, - context, - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM - ) - } catch (ex: Exception) { - null - } + fun standardSecureStorage(context: Context): SharedPreferences { + val keystoreAlias = "armadilloStandard" + val fileName = "armadillo.standard.secure" + return createEncryptedSharedPrefsKeyStore(context = context, fileName = fileName, keystoreAlias = keystoreAlias) } @Singleton @Provides @Named(Constants.DI.DRM_SECURE_STORAGE) - fun drmSecureStorage(context: Context): SharedPreferences? { - return try { - val keys = MasterKeys.getOrCreate( - KeyGenParameterSpec.Builder("armadillo", PURPOSE_ENCRYPT or PURPOSE_DECRYPT) - .setKeySize(256) - .setBlockModes(BLOCK_MODE_GCM) - .setEncryptionPaddings(ENCRYPTION_PADDING_NONE) - .build() - ) - EncryptedSharedPreferences.create( - "armadillo.download.secure", - keys, - context, - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM - ) - } - catch (ex: Exception) { - null + fun drmSecureStorage(context: Context): SharedPreferences { + val keystoreAlias = "armadillo" + val fileName = "armadillo.download.secure" + return createEncryptedSharedPrefsKeyStore(context = context, fileName = fileName, keystoreAlias = keystoreAlias) + } + + private fun createEncryptedSharedPrefsKeyStore(context: Context, fileName: String, keystoreAlias: String) + : SharedPreferences { + val keySpec = KeyGenParameterSpec.Builder(keystoreAlias, PURPOSE_ENCRYPT or PURPOSE_DECRYPT) + .setKeySize(256) + .setBlockModes(BLOCK_MODE_GCM) + .setEncryptionPaddings(ENCRYPTION_PADDING_NONE) + .build() + + val keys = try { + MasterKeys.getOrCreate(keySpec) + } catch (ex: Exception) { + //clear corrupted store, contents will be lost + val keyStore = KeyStore.getInstance("AndroidKeyStore") + keyStore.load(null) + keyStore.deleteEntry(keystoreAlias) + context.getSharedPreferences(fileName, Context.MODE_PRIVATE).edit().clear().apply() + MasterKeys.getOrCreate(keySpec) } + return EncryptedSharedPreferences.create( + fileName, + keys, + context, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) } @Singleton diff --git a/Armadillo/src/main/java/com/scribd/armadillo/encryption/SecureStorage.kt b/Armadillo/src/main/java/com/scribd/armadillo/encryption/SecureStorage.kt index c5e68d5..17de01e 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/encryption/SecureStorage.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/encryption/SecureStorage.kt @@ -5,7 +5,6 @@ import android.content.SharedPreferences import android.util.Base64 import android.util.Log import com.scribd.armadillo.Constants -import com.scribd.armadillo.error.DrmDownloadException import com.scribd.armadillo.models.DrmDownload import com.scribd.armadillo.models.DrmType import kotlinx.serialization.decodeFromString @@ -29,9 +28,9 @@ internal interface SecureStorage { @Singleton internal class ArmadilloSecureStorage @Inject constructor( @Named(Constants.DI.STANDARD_STORAGE) private val legacyStandardStorage: SharedPreferences, - @Named(Constants.DI.STANDARD_SECURE_STORAGE) private val secureStandardStorage: SharedPreferences?, + @Named(Constants.DI.STANDARD_SECURE_STORAGE) private val secureStandardStorage: SharedPreferences, @Named(Constants.DI.DRM_DOWNLOAD_STORAGE) private val legacyDrmStorage: SharedPreferences, - @Named(Constants.DI.DRM_SECURE_STORAGE) private val secureDrmStorage: SharedPreferences? + @Named(Constants.DI.DRM_SECURE_STORAGE) private val secureDrmStorage: SharedPreferences ) : SecureStorage { companion object { const val DOWNLOAD_KEY = "download_key" @@ -42,8 +41,8 @@ internal class ArmadilloSecureStorage @Inject constructor( } override fun downloadSecretKey(context: Context): ByteArray { - return if (secureStandardStorage?.contains(DOWNLOAD_KEY) == true) { - val storedKey = secureDrmStorage?.getString(DOWNLOAD_KEY, DEFAULT) ?: DEFAULT + return if (secureStandardStorage.contains(DOWNLOAD_KEY)) { + val storedKey = secureDrmStorage.getString(DOWNLOAD_KEY, DEFAULT) ?: DEFAULT if (storedKey == DEFAULT) { Log.e(TAG, "Storage Is Out of Alignment") } @@ -54,13 +53,13 @@ internal class ArmadilloSecureStorage @Inject constructor( if (storedKey == DEFAULT) { Log.e(TAG, "Storage Is Out of Alignment") } - secureStandardStorage?.edit()?.putString(DOWNLOAD_KEY, storedKey)?.apply() + secureStandardStorage.edit().putString(DOWNLOAD_KEY, storedKey).apply() legacyStandardStorage.edit().remove(DOWNLOAD_KEY).apply() storedKey.toSecretByteArray } else { //no key exists anywhere yet createRandomString().also { - secureStandardStorage?.edit()?.putString(DOWNLOAD_KEY, it)?.apply() + secureStandardStorage.edit().putString(DOWNLOAD_KEY, it).apply() }.toSecretByteArray } } @@ -72,53 +71,48 @@ internal class ArmadilloSecureStorage @Inject constructor( } override fun saveDrmDownload(context: Context, id: String, drmDownload: DrmDownload) { - if(secureDrmStorage == null){ - throw DrmDownloadException(UnsupportedOperationException("This device cannot encrypt downloads")) - } val alias = getDrmDownloadAlias(id, drmDownload.drmType) val value = Base64.encodeToString(Json.encodeToString(drmDownload).toByteArray(StandardCharsets.UTF_8), Base64.NO_WRAP) - secureDrmStorage.edit()?.putString(alias, value)?.apply() + secureDrmStorage.edit().putString(alias, value).apply() } override fun getDrmDownload(context: Context, id: String, drmType: DrmType): DrmDownload? { val alias = getDrmDownloadAlias(id, drmType) - var download = secureDrmStorage?.getString(alias, null)?.decodeToDrmDownload() + var download = secureDrmStorage.getString(alias, null)?.decodeToDrmDownload() if (download == null && legacyDrmStorage.contains(alias)) { //migrate old storage to secure storage val downloadValue = legacyDrmStorage.getString(alias, null) download = downloadValue?.decodeToDrmDownload() - if(secureDrmStorage != null) { - secureDrmStorage.edit()?.putString(alias, downloadValue)?.apply() - legacyDrmStorage.edit().remove(alias).apply() - } + secureDrmStorage.edit().putString(alias, downloadValue).apply() + legacyDrmStorage.edit().remove(alias).apply() } return download } override fun getAllDrmDownloads(context: Context): Map { - val drmDownloads = secureDrmStorage?.all?.keys?.mapNotNull { alias -> + val drmDownloads = secureDrmStorage.all.keys.mapNotNull { alias -> secureDrmStorage.getString(alias, null)?.let { drmResult -> alias to drmResult.decodeToDrmDownload() } - }?.toMap() + }.toMap() val legacyDownloads = legacyDrmStorage.all.keys.mapNotNull { alias -> legacyDrmStorage.getString(alias, null)?.let { drmResult -> alias to drmResult.decodeToDrmDownload() } }.toMap() - return legacyDownloads.plus(drmDownloads ?: emptyMap()) + return legacyDownloads.plus(drmDownloads) } override fun removeDrmDownload(context: Context, id: String, drmType: DrmType) { val alias = getDrmDownloadAlias(id, drmType) legacyDrmStorage.edit().remove(alias).apply() - secureDrmStorage?.edit()?.remove(alias)?.apply() + secureDrmStorage.edit().remove(alias).apply() } override fun removeDrmDownload(context: Context, key: String) { legacyDrmStorage.edit().remove(key).apply() - secureDrmStorage?.edit()?.remove(key)?.apply() + secureDrmStorage.edit().remove(key).apply() } private val String.toSecretByteArray: ByteArray From 2c640c662d7915e1f8956f59d5a5296665690aef Mon Sep 17 00:00:00 2001 From: Katherine Blizard <414924+kabliz@users.noreply.github.com> Date: Wed, 18 Sep 2024 11:35:16 -0700 Subject: [PATCH 4/4] [APT-10393] Cleanup id copying. The data isn't used as an ID, but its passed into Exoplayer. We still have to set the ID ourselves. --- .../main/java/com/scribd/armadillo/download/DownloadEngine.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Armadillo/src/main/java/com/scribd/armadillo/download/DownloadEngine.kt b/Armadillo/src/main/java/com/scribd/armadillo/download/DownloadEngine.kt index a1671b6..283a89d 100644 --- a/Armadillo/src/main/java/com/scribd/armadillo/download/DownloadEngine.kt +++ b/Armadillo/src/main/java/com/scribd/armadillo/download/DownloadEngine.kt @@ -82,8 +82,8 @@ internal class ExoplayerDownloadEngine @Inject constructor( downloadHelper.prepare(object : DownloadHelper.Callback { override fun onPrepared(helper: DownloadHelper) { - var request = helper.getDownloadRequest(audioPlayable.id.encodeInByteArray()) - request = request.copyWithId(audioPlayable.id.toString()) + val request = helper.getDownloadRequest(audioPlayable.id.encodeInByteArray()) + .copyWithId(audioPlayable.id.toString()) try { startDownload(context, request) } catch (e: Exception) {