Skip to content

Commit

Permalink
Closes mozilla-mobile#7103 mozilla-mobile#5217: DownloadService: Read…
Browse files Browse the repository at this point in the history
… initial download state from store
  • Loading branch information
csadilek committed May 28, 2020
1 parent ff02047 commit afbfbde
Show file tree
Hide file tree
Showing 19 changed files with 455 additions and 226 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -558,3 +558,23 @@ sealed class MediaAction : BrowserAction() {
val aggregate: MediaState.Aggregate
) : MediaAction()
}

/**
* [BrowserAction] implementations related to updating the global download state.
*/
sealed class DownloadAction : BrowserAction() {
/**
* Updates the [BrowserState] to track the provided [download] as queued.
*/
data class QueueDownloadAction(val download: DownloadState) : DownloadAction()

/**
* Updates the [BrowserState] to remove the queued download with the provided [downloadId].
*/
data class RemoveQueuedDownloadAction(val downloadId: Long) : DownloadAction()

/**
* Updates the [BrowserState] to remove all queued downloads.
*/
object RemoveAllQueuedDownloadsAction : DownloadAction()
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package mozilla.components.browser.state.reducer
import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.CustomTabListAction
import mozilla.components.browser.state.action.DownloadAction
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.action.MediaAction
import mozilla.components.browser.state.action.ReaderAction
Expand Down Expand Up @@ -39,6 +40,7 @@ internal object BrowserStateReducer {
is TrackingProtectionAction -> TrackingProtectionStateReducer.reduce(state, action)
is WebExtensionAction -> WebExtensionReducer.reduce(state, action)
is MediaAction -> MediaReducer.reduce(state, action)
is DownloadAction -> DownloadStateReducer.reduce(state, action)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.browser.state.reducer

import mozilla.components.browser.state.action.DownloadAction
import mozilla.components.browser.state.state.BrowserState

internal object DownloadStateReducer {

/**
* [DownloadAction] Reducer function for modifying [BrowserState.queuedDownloads].
*/
fun reduce(state: BrowserState, action: DownloadAction): BrowserState {
return when (action) {
is DownloadAction.QueueDownloadAction -> {
state.copy(queuedDownloads = state.queuedDownloads + (action.download.id to action.download))
}
is DownloadAction.RemoveQueuedDownloadAction -> {
state.copy(queuedDownloads = state.queuedDownloads - action.downloadId)
}
is DownloadAction.RemoveAllQueuedDownloadsAction -> {
state.copy(queuedDownloads = emptyMap())
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

package mozilla.components.browser.state.state

import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.lib.state.State

/**
Expand All @@ -16,11 +17,13 @@ import mozilla.components.lib.state.State
* The extensions here represent the default values for all [BrowserState.extensions] and can
* be overridden per [SessionState].
* @property media The state of all media elements and playback states for all tabs.
* @property queuedDownloads queued downloads ([DownloadState]s) mapped to their IDs.
*/
data class BrowserState(
val tabs: List<TabSessionState> = emptyList(),
val selectedTabId: String? = null,
val customTabs: List<CustomTabSessionState> = emptyList(),
val extensions: Map<String, WebExtensionState> = emptyMap(),
val media: MediaState = MediaState()
val media: MediaState = MediaState(),
val queuedDownloads: Map<Long, DownloadState> = emptyMap()
) : State
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.browser.state.action

import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.support.test.ext.joinBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

class DownloadActionTest {

@Test
fun `QueueDownloadAction adds download`() {
val store = BrowserStore(BrowserState())

val download1 = DownloadState("https://mozilla.org/download1", destinationDirectory = "")
store.dispatch(DownloadAction.QueueDownloadAction(download1)).joinBlocking()
assertEquals(download1, store.state.queuedDownloads[download1.id])
assertEquals(1, store.state.queuedDownloads.size)

val download2 = DownloadState("https://mozilla.org/download2", destinationDirectory = "")
store.dispatch(DownloadAction.QueueDownloadAction(download2)).joinBlocking()
assertEquals(download2, store.state.queuedDownloads[download2.id])
assertEquals(2, store.state.queuedDownloads.size)
}

@Test
fun `RemoveQueuedDownloadAction removes download`() {
val store = BrowserStore(BrowserState())

val download = DownloadState("https://mozilla.org/download1", destinationDirectory = "")
store.dispatch(DownloadAction.QueueDownloadAction(download)).joinBlocking()
assertEquals(download, store.state.queuedDownloads[download.id])
assertFalse(store.state.queuedDownloads.isEmpty())

store.dispatch(DownloadAction.RemoveQueuedDownloadAction(download.id)).joinBlocking()
assertTrue(store.state.queuedDownloads.isEmpty())
}

@Test
fun `RemoveAllQueuedDownloadsAction removes all downloads`() {
val store = BrowserStore(BrowserState())

val download = DownloadState("https://mozilla.org/download1", destinationDirectory = "")
val download2 = DownloadState("https://mozilla.org/download2", destinationDirectory = "")
store.dispatch(DownloadAction.QueueDownloadAction(download)).joinBlocking()
store.dispatch(DownloadAction.QueueDownloadAction(download2)).joinBlocking()

assertFalse(store.state.queuedDownloads.isEmpty())
assertEquals(2, store.state.queuedDownloads.size)

store.dispatch(DownloadAction.RemoveAllQueuedDownloadsAction).joinBlocking()
assertTrue(store.state.queuedDownloads.isEmpty())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import mozilla.components.browser.state.action.DownloadAction
import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.fetch.Client
import mozilla.components.concept.fetch.Headers.Names.CONTENT_RANGE
import mozilla.components.concept.fetch.Headers.Names.RANGE
Expand All @@ -53,7 +55,6 @@ import mozilla.components.feature.downloads.AbstractFetchDownloadService.Downloa
import mozilla.components.feature.downloads.AbstractFetchDownloadService.DownloadJobStatus.PAUSED
import mozilla.components.feature.downloads.DownloadNotification.NOTIFICATION_DOWNLOAD_GROUP_ID
import mozilla.components.feature.downloads.ext.addCompletedDownload
import mozilla.components.feature.downloads.ext.getDownloadExtra
import mozilla.components.feature.downloads.ext.withResponse
import mozilla.components.feature.downloads.facts.emitNotificationResumeFact
import mozilla.components.feature.downloads.facts.emitNotificationPauseFact
Expand All @@ -77,6 +78,7 @@ import kotlin.random.Random
*/
@Suppress("TooManyFunctions", "LargeClass")
abstract class AbstractFetchDownloadService : Service() {
protected abstract val store: BrowserStore

private val notificationUpdateScope = MainScope()

Expand All @@ -90,6 +92,8 @@ abstract class AbstractFetchDownloadService : Service() {

internal var downloadJobs = mutableMapOf<Long, DownloadJobState>()

// TODO Move this to browser store and make immutable:
// https://github.com/mozilla-mobile/android-components/issues/7050
internal data class DownloadJobState(
var job: Job? = null,
@Volatile var state: DownloadState,
Expand Down Expand Up @@ -230,7 +234,9 @@ abstract class AbstractFetchDownloadService : Service() {
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val download = intent?.getDownloadExtra() ?: return START_REDELIVER_INTENT
val download = intent?.getLongExtra(EXTRA_DOWNLOAD_ID, -1)?.let {
store.state.queuedDownloads[it]
} ?: return START_REDELIVER_INTENT

// If the job already exists, then don't create a new ID. This can happen when calling tryAgain
val foregroundServiceId = downloadJobs[download.id]?.foregroundServiceId ?: Random.nextInt()
Expand Down Expand Up @@ -376,6 +382,7 @@ abstract class AbstractFetchDownloadService : Service() {
@VisibleForTesting
internal fun removeDownloadJob(downloadJobState: DownloadJobState) {
downloadJobs.remove(downloadJobState.state.id)
store.dispatch(DownloadAction.RemoveQueuedDownloadAction(downloadJobState.state.id))
if (downloadJobs.isEmpty()) {
stopSelf()
} else {
Expand Down Expand Up @@ -570,7 +577,6 @@ abstract class AbstractFetchDownloadService : Service() {

val intent = Intent(ACTION_DOWNLOAD_COMPLETE)
intent.putExtra(EXTRA_DOWNLOAD_STATUS, getDownloadJobStatus(downloadState))
intent.putExtra(EXTRA_DOWNLOAD, downloadState.state)
intent.putExtra(EXTRA_DOWNLOAD_ID, downloadState.state.id)

broadcastManager.sendBroadcast(intent)
Expand Down Expand Up @@ -725,7 +731,6 @@ abstract class AbstractFetchDownloadService : Service() {
*/
internal const val PROGRESS_UPDATE_INTERVAL = 750L

const val EXTRA_DOWNLOAD = "mozilla.components.feature.downloads.extras.DOWNLOAD"
const val EXTRA_DOWNLOAD_STATUS = "mozilla.components.feature.downloads.extras.DOWNLOAD_STATUS"
const val ACTION_OPEN = "mozilla.components.feature.downloads.OPEN"
const val ACTION_PAUSE = "mozilla.components.feature.downloads.PAUSE"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.feature.downloads

import android.app.DownloadManager
import android.content.Context
import android.content.Intent
import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.DownloadAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareStore

/**
* [Middleware] implementation for managing downloads via the provided download service. It's
* purpose is to react to global download state changes (e.g. of [BrowserState.queuedDownloads])
* and notify the download service, as needed.
*/
class DownloadMiddleware(
private val applicationContext: Context,
private val downloadServiceClass: Class<*>
) : Middleware<BrowserState, BrowserAction> {

override fun invoke(
store: MiddlewareStore<BrowserState, BrowserAction>,
next: (BrowserAction) -> Unit,
action: BrowserAction
) {
next(action)
when (action) {
is DownloadAction.QueueDownloadAction -> {
val intent = Intent(applicationContext, downloadServiceClass)
intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, action.download.id)
applicationContext.startService(intent)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
* @property dialog a reference to a [DownloadDialogFragment]. If not provided, an
* instance of [SimpleDownloadDialogFragment] will be used.
*/
@Suppress("TooManyFunctions")
@Suppress("TooManyFunctions", "LongParameterList")
class DownloadsFeature(
private val applicationContext: Context,
private val store: BrowserStore,
private val useCases: DownloadsUseCases,
override var onNeedToRequestPermissions: OnNeedToRequestPermissions = { },
onDownloadStopped: onDownloadStopped = noop,
private val downloadManager: DownloadManager = AndroidDownloadManager(applicationContext),
private val downloadManager: DownloadManager = AndroidDownloadManager(applicationContext, store),
private val tabId: String? = null,
private val fragmentManager: FragmentManager? = null,
private val promptsStyling: PromptsStyling? = null,
Expand Down

This file was deleted.

Loading

0 comments on commit afbfbde

Please sign in to comment.