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 25, 2020
1 parent 5713f81 commit b01478b
Show file tree
Hide file tree
Showing 22 changed files with 1,088 additions and 27 deletions.
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ internal object DownloadStateReducer {
is DownloadAction.RemoveAllDownloadsAction -> {
state.copy(downloads = emptyMap())
}
DownloadAction.RestoreDownloadsState -> state
}
}

Expand Down
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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')"
]
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
Loading

0 comments on commit b01478b

Please sign in to comment.