-
Notifications
You must be signed in to change notification settings - Fork 109
Add a download manager #381
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 13 commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
8b4401c
Add DownloadManager module
qnga 4cf2f22
Fix multiple DownloadManagers
qnga 7feeb87
Move import stuff to BookImporter
qnga be4ad76
Refactor importation code
qnga 1dd3abb
Merge branch 'v3' into feature/download-manager
qnga a1ba25c
Make it work
qnga 9b6d1fc
Small changes
qnga 1dc6836
Reorganization
qnga d6034aa
Introduce DownloadRepository
qnga 8a5f1cf
Centralize import feedback
qnga 687aeec
Various changes
qnga a4a879b
Various changes
qnga 09848c9
Merge branch 'v3' into feature/download-manager
qnga e715b5d
Small changess
qnga 490849c
More changes
qnga ab5ef5b
A few more changes
qnga 57413ec
Move to shared
qnga 265a4ea
Add allowDownloadsOverMetered
qnga 5e15894
Refactor downlaod repositories
qnga fd9af69
Move downloadManagerProvider to the constructor level in LcpService
qnga 70f6dd1
Refactor download manager
qnga d93b2bc
Various changes
qnga d559407
Small fix
qnga 93dbc65
Merge branch 'v3' of github.com:readium/kotlin-toolkit into feature/d…
qnga a143413
Fix ForegroundDownloadManager
qnga 7fd2373
Refine cancellation
qnga d2e6c60
Add documentation
qnga eaa3750
Various changes
qnga 840455c
Improve concurrency in LcpDownloadsRepository
qnga 6055779
Documentation and formatting
mickael-menu 16110a4
Refactor the test app
mickael-menu a0cfa4c
Adjust LCP downloads
mickael-menu 97211c7
Fix `ForegroundDownloadManager` and progress report
mickael-menu a86e0bd
Fix PublicationRetriever listeners
mickael-menu fab5af0
Small fixes
qnga e395265
Introduce Download data class
qnga 9e14172
Remove notifications
qnga 52fe84a
Move permission
qnga d76998c
Merge branch 'v3' into feature/download-manager
mickael-menu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
108 changes: 108 additions & 0 deletions
108
readium/downloads/src/main/java/org/readium/downloads/DownloadManager.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
qnga marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
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." | ||
} | ||
|
||
qnga marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
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) | ||
qnga marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
public fun onDownloadFailed(requestId: RequestId, error: Error) | ||
} | ||
|
||
public suspend fun submit(request: Request): RequestId | ||
qnga marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
public suspend fun close() | ||
} |
28 changes: 28 additions & 0 deletions
28
readium/downloads/src/main/java/org/readium/downloads/DownloadManagerProvider.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
174 changes: 174 additions & 0 deletions
174
readium/downloads/src/main/java/org/readium/downloads/android/AndroidDownloadManager.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | ||
qnga marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
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 | ||
) | ||
qnga marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
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 | ||
qnga marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
): 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() | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.