diff --git a/Armadillo/src/main/AndroidManifest.xml b/Armadillo/src/main/AndroidManifest.xml
index eafc9f5..88927bf 100644
--- a/Armadillo/src/main/AndroidManifest.xml
+++ b/Armadillo/src/main/AndroidManifest.xml
@@ -2,6 +2,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
+
diff --git a/Armadillo/src/main/java/com/scribd/armadillo/Util.kt b/Armadillo/src/main/java/com/scribd/armadillo/Util.kt
index 9ccb077..c3be0c5 100644
--- a/Armadillo/src/main/java/com/scribd/armadillo/Util.kt
+++ b/Armadillo/src/main/java/com/scribd/armadillo/Util.kt
@@ -1,6 +1,8 @@
package com.scribd.armadillo
import android.content.Context
+import android.net.ConnectivityManager
+import android.net.NetworkCapabilities
import android.os.Build
import androidx.annotation.ChecksSdkIntAtLeast
import com.scribd.armadillo.models.Chapter
@@ -47,5 +49,20 @@ fun sanitizeChapters(chapters: List): List {
}
}
+fun isInternetAvailable(context: Context): Boolean {
+ val connectivityManager =
+ context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ val networkCapabilities = connectivityManager.activeNetwork ?: return false
+ val actNw =
+ connectivityManager.getNetworkCapabilities(networkCapabilities) ?: return false
+ return when {
+ actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
+ actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
+ actNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
+ actNw.hasTransport(NetworkCapabilities.TRANSPORT_VPN) -> true
+ else -> false
+ }
+}
+
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S)
fun hasSnowCone() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
\ No newline at end of file
diff --git a/Armadillo/src/main/java/com/scribd/armadillo/di/PlaybackModule.kt b/Armadillo/src/main/java/com/scribd/armadillo/di/PlaybackModule.kt
index 529e4d5..89d314c 100644
--- a/Armadillo/src/main/java/com/scribd/armadillo/di/PlaybackModule.kt
+++ b/Armadillo/src/main/java/com/scribd/armadillo/di/PlaybackModule.kt
@@ -18,6 +18,7 @@ import com.scribd.armadillo.playback.MediaMetadataCompatBuilderImpl
import com.scribd.armadillo.playback.PlaybackEngineFactoryHolder
import com.scribd.armadillo.playback.PlaybackStateBuilderImpl
import com.scribd.armadillo.playback.PlaybackStateCompatBuilder
+import com.scribd.armadillo.playback.PlayerEventListener
import com.scribd.armadillo.playback.mediasource.DrmMediaSourceHelper
import com.scribd.armadillo.playback.mediasource.DrmMediaSourceHelperImpl
import com.scribd.armadillo.playback.mediasource.HeadersMediaSourceFactoryFactory
@@ -75,4 +76,8 @@ internal class PlaybackModule {
@Provides
@Singleton
fun drmSessionManagerProvider(stateStore: StateStore.Modifier): DrmSessionManagerProvider = ArmadilloDrmSessionManagerProvider(stateStore)
+
+ @Provides
+ @Singleton
+ fun playerEventListener(context: Context): PlayerEventListener = PlayerEventListener(context)
}
\ No newline at end of file
diff --git a/Armadillo/src/main/java/com/scribd/armadillo/download/DownloadManagerExt.kt b/Armadillo/src/main/java/com/scribd/armadillo/download/DownloadManagerExt.kt
index a1d7594..979749e 100644
--- a/Armadillo/src/main/java/com/scribd/armadillo/download/DownloadManagerExt.kt
+++ b/Armadillo/src/main/java/com/scribd/armadillo/download/DownloadManagerExt.kt
@@ -14,7 +14,8 @@ internal data class TestableDownloadState(val id: Int,
val url: String,
val state: Int,
val downloadPercentage: Int,
- val downloadedBytes: Long) {
+ val downloadedBytes: Long,
+ val failureReason: Int? = null) {
companion object {
const val QUEUED = ExoplayerDownload.STATE_QUEUED
const val COMPLETED = ExoplayerDownload.STATE_COMPLETED
@@ -24,11 +25,12 @@ internal data class TestableDownloadState(val id: Int,
}
constructor(download: ExoplayerDownload) : this(
- download.request.data.decodeToInt(),
- download.request.uri.toString(),
- download.state,
- download.percentDownloaded.toInt(),
- download.bytesDownloaded)
+ download.request.data.decodeToInt(),
+ download.request.uri.toString(),
+ download.state,
+ download.percentDownloaded.toInt(),
+ download.bytesDownloaded,
+ download.failureReason)
/**
* This method converts [TestableDownloadState] (a testable wrapper fo exoplayer's [DownloadManager.TaskState])
@@ -47,13 +49,15 @@ internal data class TestableDownloadState(val id: Int,
}
DownloadState.STARTED(percent, downloadedBytes)
}
+
QUEUED -> return null
- else -> DownloadState.FAILED
+ else -> DownloadState.FAILED(failureReason)
}
return DownloadProgressInfo(
- id = id,
- url = url,
- downloadState = downloadState)
+ id = id,
+ url = url,
+ downloadState = downloadState,
+ exoPlayerDownloadState = state)
}
}
\ No newline at end of file
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 a15fb8d..45b16cd 100644
--- a/Armadillo/src/main/java/com/scribd/armadillo/download/ExoplayerDownloadTracker.kt
+++ b/Armadillo/src/main/java/com/scribd/armadillo/download/ExoplayerDownloadTracker.kt
@@ -20,8 +20,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.IOException
-import java.lang.Exception
-import java.util.HashMap
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton
@@ -111,7 +109,7 @@ internal class ExoplayerDownloadTracker @Inject constructor(
override fun updateProgress() {
downloadManager.currentDownloads.forEach { download ->
- if(downloads.containsKey(download.request.uri.toString())){
+ if (downloads.containsKey(download.request.uri.toString())) {
downloads[download.request.uri.toString()] = download //older usage
} else {
downloads[download.request.id] = download
@@ -129,7 +127,7 @@ internal class ExoplayerDownloadTracker @Inject constructor(
override fun onDownloadChanged(downloadManager: DownloadManager, download: Download, finalException: Exception?) {
Log.v(TAG, "onDownloadChanged")
- if(downloads.containsKey(download.request.uri.toString())){
+ if (downloads.containsKey(download.request.uri.toString())) {
downloads[download.request.uri.toString()] = download //older usage
} else {
downloads[download.request.id] = download
@@ -155,6 +153,7 @@ internal class ExoplayerDownloadTracker @Inject constructor(
@VisibleForTesting
fun dispatchActionsForProgress(downloadInfo: DownloadProgressInfo) {
val taskFailed = downloadInfo.isFailed()
+ val exoplayerDownloadState = downloadInfo.exoPlayerDownloadState
val isRemoveDownloadComplete = downloadInfo.downloadState is DownloadState.REMOVED
val isDownloadComplete = downloadInfo.isDownloaded()
@@ -173,7 +172,13 @@ internal class ExoplayerDownloadTracker @Inject constructor(
}
if (taskFailed) {
- actions.add(ErrorAction(DownloadFailed()))
+ actions.add(ErrorAction(DownloadFailed(
+ mapOf(
+ "exo_player_download_state" to exoplayerDownloadState.toString(),
+ "is_download_complete" to isDownloadComplete.toString(),
+ "failure_reason" to (downloadInfo.downloadState as? DownloadState.FAILED)?.failureReason.toString()
+ )
+ )))
}
actions.forEach {
diff --git a/Armadillo/src/main/java/com/scribd/armadillo/error/ArmadilloException.kt b/Armadillo/src/main/java/com/scribd/armadillo/error/ArmadilloException.kt
index 32bfe83..a6107ae 100644
--- a/Armadillo/src/main/java/com/scribd/armadillo/error/ArmadilloException.kt
+++ b/Armadillo/src/main/java/com/scribd/armadillo/error/ArmadilloException.kt
@@ -57,8 +57,12 @@ class InvalidRequest(message: String)
/**
* Playback Errors
*/
-data class HttpResponseCodeException(val responseCode: Int, val url: String?, override val cause: Exception)
- : ArmadilloException(cause = cause, message = "HTTP Error $responseCode.", isNetworkRelatedError = true) {
+data class HttpResponseCodeException(
+ val responseCode: Int,
+ val url: String?,
+ override val cause: Exception,
+ val extraData: Map = emptyMap(),
+) : ArmadilloException(cause = cause, message = "HTTP Error $responseCode.", isNetworkRelatedError = true) {
override val errorCode: Int = 200
}
@@ -96,6 +100,11 @@ class ConnectivityException(cause: Exception)
override val errorCode: Int = 206
}
+class ParsingException(cause: Exception)
+ : ArmadilloException(cause = cause, message = "The content cannot be parsed and it is not playable: ${cause.message}") {
+ override val errorCode = 207
+}
+
/**
* Download Errors
*/
@@ -104,7 +113,7 @@ class MissingInfoDownloadException(message: String)
override val errorCode = 301
}
-class DownloadFailed
+class DownloadFailed(val extraData: Map)
: ArmadilloException(message = "The download has failed to finish.", isNetworkRelatedError = true) {
override val errorCode = 302
}
diff --git a/Armadillo/src/main/java/com/scribd/armadillo/models/ArmadilloState.kt b/Armadillo/src/main/java/com/scribd/armadillo/models/ArmadilloState.kt
index cb45aa7..b489aa3 100644
--- a/Armadillo/src/main/java/com/scribd/armadillo/models/ArmadilloState.kt
+++ b/Armadillo/src/main/java/com/scribd/armadillo/models/ArmadilloState.kt
@@ -98,11 +98,12 @@ data class MediaControlState(
data class DownloadProgressInfo(
val id: Int,
val url: String,
- val downloadState: DownloadState) {
+ val downloadState: DownloadState,
+ val exoPlayerDownloadState: Int? = null) {
fun isDownloaded(): Boolean = DownloadState.COMPLETED == downloadState
- fun isFailed(): Boolean = DownloadState.FAILED == downloadState
+ fun isFailed(): Boolean = downloadState is DownloadState.FAILED
companion object {
const val PROGRESS_UNSET = C.PERCENTAGE_UNSET
@@ -129,25 +130,25 @@ sealed class DownloadState {
override fun toString() = "REMOVED"
}
- object FAILED : DownloadState() {
+ data class FAILED(val failureReason: Int? = null) : DownloadState() {
override fun toString() = "FAILED"
}
}
data class InternalState(val isPlaybackEngineReady: Boolean = false)
-sealed class DrmState (val drmType: DrmType?, val expireMillis: Milliseconds, val isSessionValid: Boolean) {
+sealed class DrmState(val drmType: DrmType?, val expireMillis: Milliseconds, val isSessionValid: Boolean) {
/** This Content is not utilizing DRM protections, or is now first initializing **/
object NoDRM : DrmState(null, 0.milliseconds, true)
/** Attempt to open the license and decrypt */
- class LicenseOpening(drmType: DrmType?, expireMillis: Milliseconds = 0.milliseconds): DrmState(drmType, expireMillis, true)
+ class LicenseOpening(drmType: DrmType?, expireMillis: Milliseconds = 0.milliseconds) : DrmState(drmType, expireMillis, true)
/** A DRM License has been obtained. */
- class LicenseAcquired(drmType: DrmType, expireMillis: Milliseconds): DrmState(drmType, expireMillis, true)
+ class LicenseAcquired(drmType: DrmType, expireMillis: Milliseconds) : DrmState(drmType, expireMillis, true)
/** The player encountered an expiration event */
- class LicenseExpired(drmType: DrmType?, expireMillis: Milliseconds): DrmState(drmType, expireMillis, false)
+ class LicenseExpired(drmType: DrmType?, expireMillis: Milliseconds) : DrmState(drmType, expireMillis, false)
/** A DRM license exists and content is able to be decrypted. */
class LicenseUsable(drmType: DrmType?, expireMillis: Milliseconds) : DrmState(drmType, expireMillis, true)
diff --git a/Armadillo/src/main/java/com/scribd/armadillo/playback/ExoPlaybackExceptionExt.kt b/Armadillo/src/main/java/com/scribd/armadillo/playback/ExoPlaybackExceptionExt.kt
index cde6af7..ca9d88e 100644
--- a/Armadillo/src/main/java/com/scribd/armadillo/playback/ExoPlaybackExceptionExt.kt
+++ b/Armadillo/src/main/java/com/scribd/armadillo/playback/ExoPlaybackExceptionExt.kt
@@ -1,38 +1,58 @@
package com.scribd.armadillo.playback
+import android.content.Context
import com.google.android.exoplayer2.ExoPlaybackException
import com.google.android.exoplayer2.ExoPlaybackException.TYPE_RENDERER
import com.google.android.exoplayer2.ExoPlaybackException.TYPE_SOURCE
+import com.google.android.exoplayer2.ParserException
import com.google.android.exoplayer2.audio.AudioSink
import com.google.android.exoplayer2.drm.MediaDrmCallbackException
+import com.google.android.exoplayer2.upstream.DataSpec
import com.google.android.exoplayer2.upstream.HttpDataSource
import com.scribd.armadillo.error.ArmadilloException
import com.scribd.armadillo.error.ArmadilloIOException
-import com.scribd.armadillo.error.HttpResponseCodeException
import com.scribd.armadillo.error.ConnectivityException
+import com.scribd.armadillo.error.HttpResponseCodeException
+import com.scribd.armadillo.error.ParsingException
import com.scribd.armadillo.error.RendererConfigurationException
import com.scribd.armadillo.error.RendererInitializationException
import com.scribd.armadillo.error.RendererWriteException
import com.scribd.armadillo.error.UnexpectedException
import com.scribd.armadillo.error.UnknownRendererException
+import com.scribd.armadillo.isInternetAvailable
import java.net.SocketTimeoutException
import java.net.UnknownHostException
-internal fun ExoPlaybackException.toArmadilloException(): ArmadilloException {
+internal fun ExoPlaybackException.toArmadilloException(context: Context): ArmadilloException {
return if (TYPE_SOURCE == type) {
return this.sourceException.let { source ->
when (source) {
is HttpDataSource.InvalidResponseCodeException ->
- HttpResponseCodeException(source.responseCode, source.dataSpec.uri.toString(), source)
+ HttpResponseCodeException(source.responseCode, source.dataSpec.uri.toString(), source, source.dataSpec.toAnalyticsMap
+ (context))
+
is HttpDataSource.HttpDataSourceException ->
- HttpResponseCodeException(0, source.dataSpec.uri.toString(), source)
+ HttpResponseCodeException(source.reason, source.dataSpec.uri.toString(), source, source.dataSpec.toAnalyticsMap(context))
+
is MediaDrmCallbackException -> {
val httpCause = source.cause as? HttpDataSource.InvalidResponseCodeException
- HttpResponseCodeException(httpCause?.responseCode ?: 0, httpCause?.dataSpec?.uri.toString(), source)
+ HttpResponseCodeException(httpCause?.responseCode
+ ?: 0, httpCause?.dataSpec?.uri.toString(), source, source.dataSpec.toAnalyticsMap(context))
}
+
is UnknownHostException,
is SocketTimeoutException -> ConnectivityException(source)
- else -> ArmadilloIOException(cause = this, actionThatFailedMessage = "Exoplayer error.")
+
+ else -> {
+ var cause: Throwable? = source
+ while (source.cause != null && cause !is ParserException) {
+ cause = source.cause
+ }
+ when (cause) {
+ is ParserException -> ParsingException(cause = this)
+ else -> ArmadilloIOException(cause = this, actionThatFailedMessage = "Exoplayer error.")
+ }
+ }
}
}
} else if (TYPE_RENDERER == type) {
@@ -47,4 +67,18 @@ internal fun ExoPlaybackException.toArmadilloException(): ArmadilloException {
} else {
UnexpectedException(cause = this, actionThatFailedMessage = "Exoplayer error")
}
+}
+
+private fun DataSpec.toAnalyticsMap(context: Context): Map {
+ return mapOf(
+ "uri" to uri.toString(),
+ "uriPositionOffset" to uriPositionOffset.toString(),
+ "httpMethod" to httpMethod.toString(),
+ "position" to position.toString(),
+ "length" to length.toString(),
+ "key" to key.toString(),
+ "flags" to flags.toString(),
+ "customData" to customData.toString(),
+ "isInternetConnected" to isInternetAvailable(context).toString(),
+ )
}
\ No newline at end of file
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 08d303d..fc36c00 100644
--- a/Armadillo/src/main/java/com/scribd/armadillo/playback/PlaybackEngine.kt
+++ b/Armadillo/src/main/java/com/scribd/armadillo/playback/PlaybackEngine.kt
@@ -105,7 +105,8 @@ internal class ExoplayerPlaybackEngine(private var audioPlayable: AudioPlayable)
@VisibleForTesting
internal lateinit var exoPlayer: ExoPlayer
- private val playerEventListener = PlayerEventListener()
+ @Inject
+ internal lateinit var playerEventListener: PlayerEventListener
override val currentChapterIndex: Int
get() = audioPlayable.chapters.indexOf(currentChapter)
@@ -155,7 +156,7 @@ internal class ExoplayerPlaybackEngine(private var audioPlayable: AudioPlayable)
stateModifier.dispatch(PlaybackEngineReady(true))
stateModifier.dispatch(PlayerStateAction(PlaybackState.PAUSED))
} catch (ex: Exception) {
- val armadilloException = if(ex is ArmadilloException) ex else PlaybackStartFailureException(cause = ex)
+ val armadilloException = if (ex is ArmadilloException) ex else PlaybackStartFailureException(cause = ex)
stateModifier.dispatch(ErrorAction(armadilloException))
}
}
diff --git a/Armadillo/src/main/java/com/scribd/armadillo/playback/PlayerEventListener.kt b/Armadillo/src/main/java/com/scribd/armadillo/playback/PlayerEventListener.kt
index 5271099..47a50f3 100644
--- a/Armadillo/src/main/java/com/scribd/armadillo/playback/PlayerEventListener.kt
+++ b/Armadillo/src/main/java/com/scribd/armadillo/playback/PlayerEventListener.kt
@@ -1,5 +1,6 @@
package com.scribd.armadillo.playback
+import android.content.Context
import android.util.Log
import com.google.android.exoplayer2.ExoPlaybackException
import com.google.android.exoplayer2.ExoPlayer
@@ -22,7 +23,7 @@ import javax.inject.Inject
*
* It communicates changes by sending [Action]s with [StateStore.Modifier].
*/
-internal class PlayerEventListener : Player.Listener {
+internal class PlayerEventListener @Inject constructor(private val context: Context) : Player.Listener {
init {
Injector.mainComponent.inject(this)
}
@@ -35,7 +36,7 @@ internal class PlayerEventListener : Player.Listener {
internal lateinit var stateModifier: StateStore.Modifier
override fun onPlayerError(error: PlaybackException) {
- val exception = (error as ExoPlaybackException).toArmadilloException()
+ val exception = (error as ExoPlaybackException).toArmadilloException(context)
stateModifier.dispatch(ErrorAction(exception))
Log.e(TAG, "onPlayerError: $error")
}
diff --git a/Armadillo/src/test/java/com/scribd/armadillo/download/DownloadManagerExtKtTest.kt b/Armadillo/src/test/java/com/scribd/armadillo/download/DownloadManagerExtKtTest.kt
index 112bb88..a905296 100644
--- a/Armadillo/src/test/java/com/scribd/armadillo/download/DownloadManagerExtKtTest.kt
+++ b/Armadillo/src/test/java/com/scribd/armadillo/download/DownloadManagerExtKtTest.kt
@@ -15,14 +15,15 @@ class DownloadManagerExtKtTest {
}
private lateinit var downloadState: TestableDownloadState
+
@Before
fun setUp() {
downloadState = TestableDownloadState(
- ID,
- URL,
- TestableDownloadState.COMPLETED,
- DOWNLOAD_PERCENT,
- DOWNLOADED_BYTES)
+ ID,
+ URL,
+ TestableDownloadState.COMPLETED,
+ DOWNLOAD_PERCENT,
+ DOWNLOADED_BYTES)
}
@Test
@@ -35,7 +36,7 @@ class DownloadManagerExtKtTest {
@Test
fun toDownloadInfo_downloadRemovalComplete_returnsRemoveProgress() {
val state = downloadState.copy(
- state = TestableDownloadState.REMOVING)
+ state = TestableDownloadState.REMOVING)
val downloadInfo = state.toDownloadInfo()!!
assertThat(downloadInfo.id).isEqualTo(ID)
assertThat(downloadInfo.url).isEqualTo(URL)
@@ -45,7 +46,7 @@ class DownloadManagerExtKtTest {
@Test
fun toDownloadInfo_downloadComplete_returnsCompletedProgress() {
val state = downloadState.copy(
- state = TestableDownloadState.COMPLETED)
+ state = TestableDownloadState.COMPLETED)
val downloadInfo = state.toDownloadInfo()!!
assertThat(downloadInfo.id).isEqualTo(ID)
assertThat(downloadInfo.url).isEqualTo(URL)
@@ -55,8 +56,8 @@ class DownloadManagerExtKtTest {
@Test
fun toDownloadInfo_downloadProgressJustBegan_returnsProgress() {
val state = downloadState.copy(
- state = TestableDownloadState.IN_PROGRESS,
- downloadPercentage = DownloadProgressInfo.PROGRESS_UNSET)
+ state = TestableDownloadState.IN_PROGRESS,
+ downloadPercentage = DownloadProgressInfo.PROGRESS_UNSET)
val downloadInfo = state.toDownloadInfo()!!
assertThat(downloadInfo.id).isEqualTo(ID)
assertThat(downloadInfo.url).isEqualTo(URL)
@@ -66,7 +67,7 @@ class DownloadManagerExtKtTest {
@Test
fun toDownloadInfo_downloadProgressWithProgress_returnsProgress() {
val state = downloadState.copy(
- state = TestableDownloadState.IN_PROGRESS)
+ state = TestableDownloadState.IN_PROGRESS)
val downloadInfo = state.toDownloadInfo()!!
assertThat(downloadInfo.id).isEqualTo(ID)
assertThat(downloadInfo.url).isEqualTo(URL)
@@ -76,10 +77,10 @@ class DownloadManagerExtKtTest {
@Test
fun toDownloadInfo_unknownState_returnsFailed() {
val state = downloadState.copy(
- state = 1000)
+ state = 1000)
val downloadInfo = state.toDownloadInfo()!!
assertThat(downloadInfo.id).isEqualTo(ID)
assertThat(downloadInfo.url).isEqualTo(URL)
- assertThat(downloadInfo.downloadState).isEqualTo(DownloadState.FAILED)
+ assertThat(downloadInfo.downloadState).isEqualTo(DownloadState.FAILED())
}
}
\ No newline at end of file
diff --git a/Armadillo/src/test/java/com/scribd/armadillo/download/ExoplayerDownloadTrackerTest.kt b/Armadillo/src/test/java/com/scribd/armadillo/download/ExoplayerDownloadTrackerTest.kt
index da14219..bf46c89 100644
--- a/Armadillo/src/test/java/com/scribd/armadillo/download/ExoplayerDownloadTrackerTest.kt
+++ b/Armadillo/src/test/java/com/scribd/armadillo/download/ExoplayerDownloadTrackerTest.kt
@@ -45,7 +45,7 @@ class ExoplayerDownloadTrackerTest {
@Test
fun dispatchActionsForProgress_taskFailed_dispatchesActions() {
- downloadInfo = DownloadProgressInfo(ID, URL, DownloadState.FAILED)
+ downloadInfo = DownloadProgressInfo(ID, URL, DownloadState.FAILED())
exoplayerDownloadTracker.dispatchActionsForProgress(downloadInfo)
verify(stateModifier).dispatch(UpdateDownloadAction(downloadInfo))
verify(stateModifier).dispatch(isA())