Skip to content

Commit

Permalink
Closes issue mozilla-mobile#7762: Adds support for persisting/restori…
Browse files Browse the repository at this point in the history
…ng downloads.
  • Loading branch information
Amejia481 committed Aug 21, 2020
1 parent 0fbcf31 commit 3a30754
Show file tree
Hide file tree
Showing 22 changed files with 1,089 additions and 27 deletions.
Expand Up @@ -574,7 +574,7 @@ sealed class DownloadAction : BrowserAction() {
/**
* Updates the [BrowserState] to track the provided [download] as added.
*/
data class AddDownloadAction(val download: DownloadState) : DownloadAction()
data class AddDownloadAction(val download: DownloadState, val restored: Boolean = false) : DownloadAction()

/**
* Updates the [BrowserState] to remove the download with the provided [downloadId].
Expand All @@ -590,6 +590,11 @@ sealed class DownloadAction : BrowserAction() {
* Updates the provided [download] on the [BrowserState].
*/
data class UpdateDownloadAction(val download: DownloadState) : DownloadAction()

/**
* Restore the [BrowserState.downloads] state from the storage.
*/
object RestoreDownloadsState : DownloadAction()
}

/**
Expand Down
Expand Up @@ -25,6 +25,7 @@ internal object DownloadStateReducer {
is DownloadAction.RemoveAllDownloadsAction -> {
state.copy(downloads = emptyMap())
}
DownloadAction.RestoreDownloadsState -> state
}
}

Expand Down
Expand Up @@ -24,7 +24,7 @@ import kotlin.random.Random
* @property referrerUrl The site that linked to this download.
* @property skipConfirmation Whether or not the confirmation dialog should be shown before the download begins.
* @property id The unique identifier of this download.
* @property sessionId Identifier of the session that spawned the download.
* @property createdTime A timestamp when the download was created.
* @
*/
@Suppress("Deprecation")
Expand All @@ -41,39 +41,41 @@ data class DownloadState(
val referrerUrl: String? = null,
val skipConfirmation: Boolean = false,
val id: Long = Random.nextLong(),
val sessionId: String? = null
val sessionId: String? = null,
val createdTime: Long = System.currentTimeMillis()
) : Parcelable {
val filePath: String get() =
Environment.getExternalStoragePublicDirectory(destinationDirectory).path + "/" + fileName

/**
* Status that represents every state that a download can be in.
*/
enum class Status {
@Suppress("MagicNumber")
enum class Status(val id: Int) {
/**
* Indicates that the download is in the first state after creation but not yet [DOWNLOADING].
*/
INITIATED,
INITIATED(1),
/**
* Indicates that an [INITIATED] download is now actively being downloaded.
*/
DOWNLOADING,
DOWNLOADING(2),
/**
* Indicates that the download that has been [DOWNLOADING] has been paused.
*/
PAUSED,
PAUSED(3),
/**
* Indicates that the download that has been [DOWNLOADING] has been cancelled.
*/
CANCELLED,
CANCELLED(4),
/**
* Indicates that the download that has been [DOWNLOADING] has moved to failed because
* something unexpected has happened.
*/
FAILED,
FAILED(5),
/**
* Indicates that the [DOWNLOADING] download has been completed.
*/
COMPLETED
COMPLETED(6)
}
}
27 changes: 27 additions & 0 deletions components/feature/downloads/build.gradle
Expand Up @@ -5,13 +5,21 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

android {
compileSdkVersion config.compileSdkVersion

defaultConfig {
minSdkVersion config.minSdkVersion
targetSdkVersion config.targetSdkVersion
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

kapt {
arguments {
arg("room.schemaLocation", "$projectDir/schemas".toString())
}
}
}

buildTypes {
Expand All @@ -20,6 +28,10 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}

sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
}

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
Expand All @@ -43,6 +55,11 @@ dependencies {
implementation Dependencies.kotlin_stdlib
implementation Dependencies.androidx_recyclerview
implementation Dependencies.androidx_constraintlayout
implementation Dependencies.androidx_room_runtime
implementation Dependencies.androidx_paging
implementation Dependencies.androidx_lifecycle_livedata

kapt Dependencies.androidx_room_compiler

testImplementation Dependencies.androidx_test_core
testImplementation Dependencies.androidx_test_junit
Expand All @@ -52,6 +69,16 @@ dependencies {
testImplementation project(':concept-engine')
testImplementation project(':support-test')
testImplementation project(':support-test-libstate')

androidTestImplementation project(':support-android-test')

androidTestImplementation Dependencies.androidx_room_testing
androidTestImplementation Dependencies.androidx_arch_core_testing
androidTestImplementation Dependencies.androidx_test_core
androidTestImplementation Dependencies.androidx_test_runner
androidTestImplementation Dependencies.androidx_test_rules
androidTestImplementation Dependencies.testing_coroutines

}

apply from: '../../../publish.gradle'
Expand Down
@@ -0,0 +1,76 @@
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "342d0e5d0a0fcde72b88ac4585caf842",
"entities": [
{
"tableName": "downloads",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `url` TEXT NOT NULL, `file_name` TEXT, `content_type` TEXT, `content_length` INTEGER, `status` INTEGER NOT NULL, `destination_directory` TEXT NOT NULL, `created_at` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "fileName",
"columnName": "file_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "contentType",
"columnName": "content_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "contentLength",
"columnName": "content_length",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "destinationDirectory",
"columnName": "destination_directory",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "createdAt",
"columnName": "created_at",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '342d0e5d0a0fcde72b88ac4585caf842')"
]
}
}
@@ -0,0 +1,139 @@
/* 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.content.Context
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.paging.PagedList
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runBlockingTest
import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.feature.downloads.db.DownloadsDatabase
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

@ExperimentalCoroutinesApi
class OnDeviceDownloadStorageTest {
private lateinit var context: Context
private lateinit var storage: DownloadStorage
private lateinit var executor: ExecutorService

@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()

@Before
fun setUp() {
executor = Executors.newSingleThreadExecutor()

context = ApplicationProvider.getApplicationContext()
val database = Room.inMemoryDatabaseBuilder(context, DownloadsDatabase::class.java).build()

storage = DownloadStorage(context)
storage.database = lazy { database }
}

@After
fun tearDown() {
executor.shutdown()
}

@Test
fun testAddingDownload() = runBlockingTest {
val download1 = createMockDownload("1", "url1")
val download2 = createMockDownload("2", "url2")
val download3 = createMockDownload("3", "url3")

storage.add(download1)
storage.add(download2)
storage.add(download3)

val downloads = getDownloadsPagedList()

assertEquals(3, downloads.size)

assertTrue(DownloadStorage.areTheSame(download1, downloads.first()))
assertTrue(DownloadStorage.areTheSame(download2, downloads[1]!!))
assertTrue(DownloadStorage.areTheSame(download3, downloads[2]!!))
}

@Test
fun testRemovingDownload() = runBlockingTest {
val download1 = createMockDownload("1", "url1")
val download2 = createMockDownload("2", "url2")

storage.add(download1)
storage.add(download2)

assertEquals(2, getDownloadsPagedList().size)

storage.remove(download1)

val downloads = getDownloadsPagedList()
val downloadFromDB = downloads.first()

assertEquals(1, downloads.size)
assertTrue(DownloadStorage.areTheSame(download2, downloadFromDB))
}

@Test
fun testGettingDownloads() = runBlockingTest {
val download1 = createMockDownload("1", "url1")
val download2 = createMockDownload("2", "url2")

storage.add(download1)
storage.add(download2)

val downloads = getDownloadsPagedList()

assertEquals(2, downloads.size)

assertTrue(DownloadStorage.areTheSame(download1, downloads.first()))
assertTrue(DownloadStorage.areTheSame(download2, downloads[1]!!))
}

@Test
fun testRemovingDownloads() = runBlocking {
for (index in 1..2) {
storage.add(createMockDownload(index.toString(), "url1"))
}

var pagedList = getDownloadsPagedList()

assertEquals(2, pagedList.size)

pagedList.forEach {
storage.remove(it)
}

pagedList = getDownloadsPagedList()

assertTrue(pagedList.isEmpty())
}

private fun createMockDownload(id: String, url: String): DownloadState {
return DownloadState(
id = id,
url = url, contentType = "application/zip", contentLength = 5242880,
userAgent = "Mozilla/5.0 (Linux; Android 7.1.1) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Focus/8.0 Chrome/69.0.3497.100 Mobile Safari/537.36"
)
}

private fun getDownloadsPagedList(): PagedList<DownloadState> {
val dataSource = storage.getDownloadsPaged().create()
return PagedList.Builder(dataSource, 10)
.setNotifyExecutor(executor)
.setFetchExecutor(executor)
.build()
}
}

0 comments on commit 3a30754

Please sign in to comment.