Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
8b4401c
Add DownloadManager module
qnga Aug 16, 2023
4cf2f22
Fix multiple DownloadManagers
qnga Aug 18, 2023
7feeb87
Move import stuff to BookImporter
qnga Aug 18, 2023
be4ad76
Refactor importation code
qnga Aug 18, 2023
1dd3abb
Merge branch 'v3' into feature/download-manager
qnga Aug 23, 2023
a1ba25c
Make it work
qnga Aug 24, 2023
9b6d1fc
Small changes
qnga Aug 25, 2023
1dc6836
Reorganization
qnga Aug 25, 2023
d6034aa
Introduce DownloadRepository
qnga Aug 25, 2023
8a5f1cf
Centralize import feedback
qnga Aug 25, 2023
687aeec
Various changes
qnga Aug 25, 2023
a4a879b
Various changes
qnga Aug 25, 2023
09848c9
Merge branch 'v3' into feature/download-manager
qnga Aug 25, 2023
e715b5d
Small changess
qnga Aug 31, 2023
490849c
More changes
qnga Sep 1, 2023
ab5ef5b
A few more changes
qnga Sep 1, 2023
57413ec
Move to shared
qnga Sep 1, 2023
265a4ea
Add allowDownloadsOverMetered
qnga Sep 1, 2023
5e15894
Refactor downlaod repositories
qnga Sep 2, 2023
fd9af69
Move downloadManagerProvider to the constructor level in LcpService
qnga Sep 2, 2023
70f6dd1
Refactor download manager
qnga Sep 4, 2023
d93b2bc
Various changes
qnga Sep 4, 2023
d559407
Small fix
qnga Sep 4, 2023
93dbc65
Merge branch 'v3' of github.com:readium/kotlin-toolkit into feature/d…
qnga Sep 4, 2023
a143413
Fix ForegroundDownloadManager
qnga Sep 10, 2023
7fd2373
Refine cancellation
qnga Sep 10, 2023
d2e6c60
Add documentation
qnga Sep 10, 2023
eaa3750
Various changes
qnga Sep 14, 2023
840455c
Improve concurrency in LcpDownloadsRepository
qnga Sep 14, 2023
6055779
Documentation and formatting
mickael-menu Sep 14, 2023
16110a4
Refactor the test app
mickael-menu Sep 15, 2023
a0cfa4c
Adjust LCP downloads
mickael-menu Sep 15, 2023
97211c7
Fix `ForegroundDownloadManager` and progress report
mickael-menu Sep 15, 2023
a86e0bd
Fix PublicationRetriever listeners
mickael-menu Sep 15, 2023
fab5af0
Small fixes
qnga Sep 15, 2023
e395265
Introduce Download data class
qnga Sep 15, 2023
9e14172
Remove notifications
qnga Sep 15, 2023
52fe84a
Move permission
qnga Sep 15, 2023
d76998c
Merge branch 'v3' into feature/download-manager
mickael-menu Sep 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions readium/downloads/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2023 Readium Foundation. All rights reserved.
* Use of this source code is governed by the BSD-style license
* available in the top-level LICENSE file of the project.
*/

plugins {
id("com.android.library")
kotlin("android")
kotlin("plugin.parcelize")
}

android {
resourcePrefix = "readium_"

compileSdk = 34

defaultConfig {
minSdk = 21
targetSdk = 34
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
freeCompilerArgs = freeCompilerArgs + listOf(
"-opt-in=kotlin.RequiresOptIn",
"-opt-in=org.readium.r2.shared.InternalReadiumApi"
)
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android.txt"))
}
}

namespace = "org.readium.downloads"
}

kotlin {
explicitApi()
}

rootProject.ext["publish.artifactId"] = "readium-downloads"
apply(from = "$rootDir/scripts/publish-module.gradle")

dependencies {
api(project(":readium:readium-shared"))

implementation(libs.bundles.coroutines)
implementation(libs.androidx.datastore.preferences)
}
3 changes: 3 additions & 0 deletions readium/downloads/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright 2023 Readium Foundation. All rights reserved.
* Use of this source code is governed by the BSD-style license
* available in the top-level LICENSE file of the project.
*/

package org.readium.downloads

import java.io.File
import org.readium.r2.shared.util.Url

public interface DownloadManager {

public data class Request(
val url: Url,
val title: String,
val description: String? = null,
val headers: Map<String, List<String>> = emptyMap()
)

@JvmInline
public value class RequestId(public val value: Long)

public sealed class Error : org.readium.r2.shared.util.Error {

override val cause: org.readium.r2.shared.util.Error? =
null

public data object NotFound : Error() {

override val message: String =
"File not found."
}

public data object Unreachable : Error() {

override val message: String =
"Server is not reachable."
}

public data object Server : Error() {

override val message: String =
"An error occurred on the server-side."
}

public data object Forbidden : Error() {

override val message: String =
"Access to the resource was denied."
}

public data object DeviceNotFound : Error() {

override val message: String =
"The storage device is missing."
}

public data object CannotResume : Error() {

override val message: String =
"Download couldn't be resumed."
}

public data object InsufficientSpace : Error() {

override val message: String =
"There is not enough space to complete the download."
}

public data object FileError : Error() {

override val message: String =
"IO error on the local device."
}

public data object HttpData : Error() {

override val message: String =
"A data error occurred at the HTTP level."
}

public data object TooManyRedirects : Error() {

override val message: String =
"Too many redirects."
}

public data object Unknown : Error() {

override val message: String =
"An unknown error occurred."
}
}

public interface Listener {

public fun onDownloadCompleted(requestId: RequestId, file: File)

public fun onDownloadProgressed(requestId: RequestId, downloaded: Long, total: Long)

public fun onDownloadFailed(requestId: RequestId, error: Error)
}

public suspend fun submit(request: Request): RequestId

public suspend fun close()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright 2023 Readium Foundation. All rights reserved.
* Use of this source code is governed by the BSD-style license
* available in the top-level LICENSE file of the project.
*/

package org.readium.downloads

/**
* To be implemented by custom implementations of [DownloadManager].
*
* Downloads can keep going on the background and the listener be called at any time.
* Naming [DownloadManager]s is useful to retrieve the downloads they own and
* associated data after app restarted.
*/
public interface DownloadManagerProvider {

/**
* Creates a [DownloadManager].
*
* @param listener listener to implement to observe the status of downloads
* @param name name of the download manager
*/
public fun createDownloadManager(
listener: DownloadManager.Listener,
name: String = "default"
): DownloadManager
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* Copyright 2023 Readium Foundation. All rights reserved.
* Use of this source code is governed by the BSD-style license
* available in the top-level LICENSE file of the project.
*/

package org.readium.downloads.android

import android.app.DownloadManager as SystemDownloadManager
import android.content.Context
import android.database.Cursor
import android.net.Uri
import java.io.File
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.readium.downloads.DownloadManager
import org.readium.r2.shared.units.Hz
import org.readium.r2.shared.util.toUri

public class AndroidDownloadManager(
private val context: Context,
private val name: String,
private val destStorage: Storage,
private val dirType: String,
private val refreshRate: Hz,
private val listener: DownloadManager.Listener
) : DownloadManager {

public enum class Storage {
App,
Shared;
}

private val coroutineScope: CoroutineScope =
MainScope()

private val progressJob: Job = coroutineScope.launch {
while (true) {
val ids = downloadsRepository.idsForName(name)
val cursor = downloadManager.query(SystemDownloadManager.Query())
notify(cursor, ids)
delay((1.0 / refreshRate.value).seconds)
}
}

private val downloadManager: SystemDownloadManager =
context.getSystemService(Context.DOWNLOAD_SERVICE) as SystemDownloadManager

private val downloadsRepository: DownloadsRepository =
DownloadsRepository(context)

public override suspend fun submit(request: DownloadManager.Request): DownloadManager.RequestId {
val androidRequest = createRequest(
request.url.toUri(),
request.url.filename,
request.headers,
request.title,
request.description
)
val downloadId = downloadManager.enqueue(androidRequest)
downloadsRepository.addId(name, downloadId)
return DownloadManager.RequestId(downloadId)
}

private fun createRequest(
uri: Uri,
filename: String,
headers: Map<String, List<String>>,
title: String,
description: String?
): SystemDownloadManager.Request =
SystemDownloadManager.Request(uri)
.setNotificationVisibility(SystemDownloadManager.Request.VISIBILITY_VISIBLE)
.setDestination(filename)
.setHeaders(headers)
.setTitle(title)
.apply { description?.let { setDescription(it) } }
.setAllowedOverMetered(true)
.setAllowedOverRoaming(true)

private fun SystemDownloadManager.Request.setHeaders(
headers: Map<String, List<String>>
): SystemDownloadManager.Request {
for (header in headers) {
for (value in header.value) {
addRequestHeader(header.key, value)
}
}
return this
}

private fun SystemDownloadManager.Request.setDestination(
filename: String
): SystemDownloadManager.Request {
when (destStorage) {
Storage.App ->
setDestinationInExternalFilesDir(context, dirType, filename)

Storage.Shared ->
setDestinationInExternalPublicDir(dirType, filename)
}
return this
}

private suspend fun notify(cursor: Cursor, ids: List<Long>) = cursor.use {
while (cursor.moveToNext()) {
val facade = DownloadCursorFacade(cursor)
val id = DownloadManager.RequestId(facade.id)

if (id.value !in ids) {
continue
}

when (facade.status) {
SystemDownloadManager.STATUS_FAILED -> {
listener.onDownloadFailed(id, mapErrorCode(facade.reason!!))
downloadManager.remove(id.value)
}
SystemDownloadManager.STATUS_PAUSED -> {}
SystemDownloadManager.STATUS_PENDING -> {}
SystemDownloadManager.STATUS_SUCCESSFUL -> {
val destUri = Uri.parse(facade.localUri!!)
listener.onDownloadCompleted(id, File(destUri.path!!))
downloadManager.remove(id.value)
downloadsRepository.removeId(name, id.value)
}
SystemDownloadManager.STATUS_RUNNING -> {
val total = facade.total
if (total > 0) {
listener.onDownloadProgressed(id, facade.downloadedSoFar, total)
}
}
}
}
}

private fun mapErrorCode(code: Int): DownloadManager.Error =
when (code) {
401, 403 ->
DownloadManager.Error.Forbidden
404 ->
DownloadManager.Error.NotFound
500, 501 ->
DownloadManager.Error.Server
502, 503, 504 ->
DownloadManager.Error.Unreachable
SystemDownloadManager.ERROR_CANNOT_RESUME ->
DownloadManager.Error.CannotResume
SystemDownloadManager.ERROR_DEVICE_NOT_FOUND ->
DownloadManager.Error.DeviceNotFound
SystemDownloadManager.ERROR_FILE_ERROR ->
DownloadManager.Error.FileError
SystemDownloadManager.ERROR_HTTP_DATA_ERROR ->
DownloadManager.Error.HttpData
SystemDownloadManager.ERROR_INSUFFICIENT_SPACE ->
DownloadManager.Error.InsufficientSpace
SystemDownloadManager.ERROR_TOO_MANY_REDIRECTS ->
DownloadManager.Error.TooManyRedirects
SystemDownloadManager.ERROR_UNHANDLED_HTTP_CODE ->
DownloadManager.Error.Unknown
SystemDownloadManager.ERROR_UNKNOWN ->
DownloadManager.Error.Unknown
else ->
DownloadManager.Error.Unknown
}

public override suspend fun close() {
progressJob.cancel()
}
}
Loading