diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt index 9a9c3fac83..aa9cf626dc 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/LcpService.kt @@ -13,8 +13,6 @@ import android.content.Context import java.io.File import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.readium.r2.lcp.auth.LcpDialogAuthentication import org.readium.r2.lcp.license.model.LicenseDocument @@ -55,9 +53,18 @@ public interface LcpService { * Acquires a protected publication from a standalone LCPL's bytes. * * You can cancel the on-going acquisition by cancelling its parent coroutine context. - * + * @Deprecated( + "Use a LcpPublicationRetriever instead.", + ReplaceWith("publicationRetriever()"), + level = DeprecationLevel.ERROR + ) * @param onProgress Callback to follow the acquisition progress from 0.0 to 1.0. */ + @Deprecated( + "Use a LcpPublicationRetriever instead.", + ReplaceWith("publicationRetriever()"), + level = DeprecationLevel.ERROR + ) public suspend fun acquirePublication(lcpl: ByteArray, onProgress: (Double) -> Unit = {}): Try /** @@ -67,14 +74,15 @@ public interface LcpService { * * @param onProgress Callback to follow the acquisition progress from 0.0 to 1.0. */ + @Deprecated( + "Use a LcpPublicationRetriever instead.", + ReplaceWith("publicationRetriever()"), + level = DeprecationLevel.ERROR + ) public suspend fun acquirePublication(lcpl: File, onProgress: (Double) -> Unit = {}): Try = withContext( Dispatchers.IO ) { - try { - acquirePublication(lcpl.readBytes(), onProgress) - } catch (e: Exception) { - Try.failure(LcpException.wrap(e)) - } + throw NotImplementedError() } /** @@ -120,10 +128,12 @@ public interface LcpService { * Creates a [LcpPublicationRetriever] instance which can be used to acquire a protected * publication from standalone LCPL's bytes. * - * @param listener listener to implement to be notified about the status of the download. + * You should use only one instance of [LcpPublicationRetriever] in your app. If you don't, + * behaviour is undefined. + * + * @param listener listener to implement to be notified about the status of the downloads. */ public fun publicationRetriever( - downloadManagerProvider: DownloadManagerProvider, listener: LcpPublicationRetriever.Listener ): LcpPublicationRetriever @@ -169,7 +179,8 @@ public interface LcpService { public operator fun invoke( context: Context, assetRetriever: AssetRetriever, - mediaTypeRetriever: MediaTypeRetriever + mediaTypeRetriever: MediaTypeRetriever, + downloadManagerProvider: DownloadManagerProvider ): LcpService? { if (!LcpClient.isAvailable()) { return null @@ -195,7 +206,8 @@ public interface LcpService { passphrases = passphrases, context = context, assetRetriever = assetRetriever, - mediaTypeRetriever = mediaTypeRetriever + mediaTypeRetriever = mediaTypeRetriever, + downloadManagerProvider = downloadManagerProvider ) } @@ -219,11 +231,7 @@ public interface LcpService { authentication: LcpAuthenticating?, completion: (AcquiredPublication?, LcpException?) -> Unit ) { - GlobalScope.launch { - acquirePublication(lcpl) - .onSuccess { completion(it, null) } - .onFailure { completion(null, it) } - } + throw NotImplementedError() } @Deprecated( diff --git a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt index d48beba62a..980179bf6f 100644 --- a/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt +++ b/readium/lcp/src/main/java/org/readium/r2/lcp/service/LicensesService.kt @@ -49,7 +49,8 @@ internal class LicensesService( private val passphrases: PassphrasesService, private val context: Context, private val assetRetriever: AssetRetriever, - private val mediaTypeRetriever: MediaTypeRetriever + private val mediaTypeRetriever: MediaTypeRetriever, + private val downloadManagerProvider: DownloadManagerProvider ) : LcpService, CoroutineScope by MainScope() { override suspend fun isLcpProtected(file: File): Boolean { @@ -75,7 +76,6 @@ internal class LicensesService( LcpContentProtection(this, authentication, assetRetriever) override fun publicationRetriever( - downloadManagerProvider: DownloadManagerProvider, listener: LcpPublicationRetriever.Listener ): LcpPublicationRetriever { return LcpPublicationRetriever( @@ -86,6 +86,11 @@ internal class LicensesService( ) } + @Deprecated( + "Use a LcpPublicationRetriever instead.", + ReplaceWith("publicationRetriever()"), + level = DeprecationLevel.ERROR + ) override suspend fun acquirePublication(lcpl: ByteArray, onProgress: (Double) -> Unit): Try = try { val licenseDocument = LicenseDocument(lcpl) diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt new file mode 100644 index 0000000000..2cf328908f --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManager.kt @@ -0,0 +1,111 @@ +/* + * 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.r2.shared.util.downloads.foreground + +import java.io.File +import java.util.UUID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.readium.r2.shared.util.ThrowableError +import org.readium.r2.shared.util.Try +import org.readium.r2.shared.util.downloads.DownloadManager +import org.readium.r2.shared.util.http.HttpClient +import org.readium.r2.shared.util.http.HttpException +import org.readium.r2.shared.util.http.HttpRequest + +public class ForegroundDownloadManager( + private val httpClient: HttpClient, + private val listener: DownloadManager.Listener +) : DownloadManager { + + private val coroutineScope: CoroutineScope = + MainScope() + + private val jobs: MutableMap = + mutableMapOf() + + override suspend fun submit(request: DownloadManager.Request): DownloadManager.RequestId { + val requestId = DownloadManager.RequestId(UUID.randomUUID().toString()) + jobs[requestId] = coroutineScope.launch { doRequest(request, requestId) } + return requestId + } + + private suspend fun doRequest(request: DownloadManager.Request, id: DownloadManager.RequestId) { + val response = httpClient.fetch( + HttpRequest( + url = request.url.toString(), + headers = request.headers.mapValues { it.value.joinToString(",") } + ) + ) + + val dottedExtension = request.url.extension + ?.let { ".$it" } + .orEmpty() + + when (response) { + is Try.Success -> { + withContext(Dispatchers.IO) { + try { + val dest = File.createTempFile( + UUID.randomUUID().toString(), + dottedExtension + ) + dest.writeBytes(response.value.body) + } catch (e: Exception) { + val error = DownloadManager.Error.FileError(ThrowableError(e)) + listener.onDownloadFailed(id, error) + } + } + } + is Try.Failure -> { + val error = mapError(response.value) + listener.onDownloadFailed(id, error) + } + } + } + + private fun mapError(httpException: HttpException): DownloadManager.Error { + val httpError = ThrowableError(httpException) + return when (httpException.kind) { + HttpException.Kind.MalformedRequest -> + DownloadManager.Error.Unknown(httpError) + HttpException.Kind.MalformedResponse -> + DownloadManager.Error.HttpData(httpError) + HttpException.Kind.Timeout -> + DownloadManager.Error.Unreachable(httpError) + HttpException.Kind.BadRequest -> + DownloadManager.Error.Unknown(httpError) + HttpException.Kind.Unauthorized -> + DownloadManager.Error.Forbidden(httpError) + HttpException.Kind.Forbidden -> + DownloadManager.Error.Forbidden(httpError) + HttpException.Kind.NotFound -> + DownloadManager.Error.NotFound(httpError) + HttpException.Kind.ClientError -> + DownloadManager.Error.HttpData(httpError) + HttpException.Kind.ServerError -> + DownloadManager.Error.Server(httpError) + HttpException.Kind.Offline -> + DownloadManager.Error.Unreachable(httpError) + HttpException.Kind.Cancelled -> + DownloadManager.Error.Unknown(httpError) + HttpException.Kind.Other -> + DownloadManager.Error.Unknown(httpError) + } + } + + override suspend fun cancel(requestId: DownloadManager.RequestId) { + jobs.remove(requestId)?.cancel() + } + + override suspend fun close() { + } +} diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManagerProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManagerProvider.kt new file mode 100644 index 0000000000..704abd6511 --- /dev/null +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/downloads/foreground/ForegroundDownloadManagerProvider.kt @@ -0,0 +1,23 @@ +/* + * 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.r2.shared.util.downloads.foreground + +import org.readium.r2.shared.util.downloads.DownloadManager +import org.readium.r2.shared.util.downloads.DownloadManagerProvider +import org.readium.r2.shared.util.http.HttpClient + +public class ForegroundDownloadManagerProvider( + private val httpClient: HttpClient +) : DownloadManagerProvider { + + override fun createDownloadManager( + listener: DownloadManager.Listener, + name: String + ): DownloadManager { + return ForegroundDownloadManager(httpClient, listener) + } +} diff --git a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt index 94ee79994c..8caef34284 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/Readium.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/Readium.kt @@ -80,7 +80,8 @@ class Readium(context: Context) { val lcpService = LcpService( context, assetRetriever, - mediaTypeRetriever + mediaTypeRetriever, + downloadManagerProvider )?.let { Try.success(it) } ?: Try.failure(UserException("liblcp is missing on the classpath")) diff --git a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt index 82985a2ec3..50c0a156f6 100644 --- a/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt +++ b/test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt @@ -62,7 +62,7 @@ class Bookshelf( private val assetRetriever: AssetRetriever, private val protectionRetriever: ContentProtectionSchemeRetriever, private val formatRegistry: FormatRegistry, - private val downloadManagerProvider: DownloadManagerProvider + downloadManagerProvider: DownloadManagerProvider ) { sealed class ImportError( content: Content, @@ -162,7 +162,6 @@ class Bookshelf( private val lcpPublicationRetriever = lcpService.map { it.publicationRetriever( - downloadManagerProvider, LcpRetrieverListener() ) }