diff --git a/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt
index dc701cf4..8cfecbc9 100644
--- a/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt
+++ b/app/src/main/java/app/revanced/manager/domain/repository/DownloadedAppRepository.kt
@@ -8,8 +8,16 @@ import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
import app.revanced.manager.plugin.downloader.App
import app.revanced.manager.plugin.downloader.DownloadScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.channelFlow
+import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
import java.io.File
+import java.io.FilterInputStream
+import java.nio.file.Files
+import java.nio.file.StandardCopyOption
+import kotlin.io.path.exists
class DownloadedAppRepository(app: Application, db: AppDatabase) {
private val dir = app.getDir("downloaded-apps", Context.MODE_PRIVATE)
@@ -17,7 +25,9 @@ class DownloadedAppRepository(app: Application, db: AppDatabase) {
fun getAll() = dao.getAllApps().distinctUntilChanged()
- private fun getApkFileForApp(app: DownloadedApp): File = getApkFileForDir(dir.resolve(app.directory))
+ private fun getApkFileForApp(app: DownloadedApp): File =
+ getApkFileForDir(dir.resolve(app.directory))
+
private fun getApkFileForDir(directory: File) = directory.listFiles()!!.first()
suspend fun download(
@@ -32,21 +42,51 @@ class DownloadedAppRepository(app: Application, db: AppDatabase) {
// Converted integers cannot contain / or .. unlike the package name or version, so they are safer to use here.
val relativePath = File(generateUid().toString())
val saveDir = dir.resolve(relativePath).also { it.mkdirs() }
- val targetFile = saveDir.resolve("base.apk")
+ val targetFile = saveDir.resolve("base.apk").toPath()
try {
- val scope = object : DownloadScope {
- override val targetFile = targetFile
- override suspend fun reportProgress(bytesReceived: Long, bytesTotal: Long?) {
- require(bytesReceived >= 0) { "bytesReceived must not be negative" }
- require(bytesTotal == null || bytesTotal >= bytesReceived) { "bytesTotal must be greater than or equal to bytesReceived" }
- require(bytesTotal != 0L) { "bytesTotal must not be zero" }
+ channelFlow {
+ var fileSize: Long? = null
+ var downloadedBytes = 0L
- onDownload(bytesReceived.megaBytes to bytesTotal?.megaBytes)
+ val scope = object : DownloadScope {
+ override suspend fun reportSize(size: Long) {
+ fileSize = size
+ send(downloadedBytes to size)
+ }
+ /*
+ override val targetFile = targetFile
+ override suspend fun reportProgress(bytesReceived: Long, bytesTotal: Long?) {
+ require(bytesReceived >= 0) { "bytesReceived must not be negative" }
+ require(bytesTotal == null || bytesTotal >= bytesReceived) { "bytesTotal must be greater than or equal to bytesReceived" }
+ require(bytesTotal != 0L) { "bytesTotal must not be zero" }
+
+ onDownload(bytesReceived.megaBytes to bytesTotal?.megaBytes)
+ }*/
+ }
+
+ plugin.download(scope, app).use { inputStream ->
+ Files.copy(object : FilterInputStream(inputStream) {
+ override fun read(): Int {
+ val array = ByteArray(1)
+ if (read(array, 0, 1) != 1) return -1
+ return array[0].toInt()
+ }
+
+ override fun read(b: ByteArray?, off: Int, len: Int) =
+ super.read(b, off, len).also { result ->
+ // Report download progress
+ if (result > 0) {
+ downloadedBytes += result
+ fileSize?.let { trySend(downloadedBytes to it).getOrThrow() }
+ }
+ }
+ }, targetFile, StandardCopyOption.REPLACE_EXISTING)
}
}
-
- plugin.download(scope, app)
+ .conflate()
+ .flowOn(Dispatchers.IO)
+ .collect { (downloaded, size) -> onDownload(downloaded.megaBytes to size.megaBytes) }
if (!targetFile.exists()) throw Exception("Downloader did not download any files")
diff --git a/app/src/main/java/app/revanced/manager/domain/repository/DownloaderPluginRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/DownloaderPluginRepository.kt
index 20bbfb55..f87a65ec 100644
--- a/app/src/main/java/app/revanced/manager/domain/repository/DownloaderPluginRepository.kt
+++ b/app/src/main/java/app/revanced/manager/domain/repository/DownloaderPluginRepository.kt
@@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
+import java.io.InputStream
import java.lang.reflect.Modifier
class DownloaderPluginRepository(
@@ -112,7 +113,7 @@ class DownloaderPluginRepository(
with(pm) { packageInfo.label() },
packageInfo.versionName,
downloader.get,
- downloader.download as suspend DownloadScope.(App) -> Unit,
+ downloader.download as suspend DownloadScope.(App) -> InputStream,
classLoader
)
)
diff --git a/app/src/main/java/app/revanced/manager/network/downloader/LoadedDownloaderPlugin.kt b/app/src/main/java/app/revanced/manager/network/downloader/LoadedDownloaderPlugin.kt
index 54174cc3..feab85b7 100644
--- a/app/src/main/java/app/revanced/manager/network/downloader/LoadedDownloaderPlugin.kt
+++ b/app/src/main/java/app/revanced/manager/network/downloader/LoadedDownloaderPlugin.kt
@@ -4,12 +4,13 @@ import android.content.Context
import app.revanced.manager.plugin.downloader.App
import app.revanced.manager.plugin.downloader.DownloadScope
import app.revanced.manager.plugin.downloader.GetScope
+import java.io.InputStream
class LoadedDownloaderPlugin(
val packageName: String,
val name: String,
val version: String,
val get: suspend GetScope.(packageName: String, version: String?) -> App?,
- val download: suspend DownloadScope.(app: App) -> Unit,
+ val download: suspend DownloadScope.(app: App) -> InputStream,
val classLoader: ClassLoader
)
\ No newline at end of file
diff --git a/downloader-plugin/build.gradle.kts b/downloader-plugin/build.gradle.kts
index ec553d79..b3871e64 100644
--- a/downloader-plugin/build.gradle.kts
+++ b/downloader-plugin/build.gradle.kts
@@ -33,6 +33,10 @@ android {
}
}
+dependencies {
+ implementation(libs.kotlinx.coroutines)
+}
+
publishing {
repositories {
mavenLocal()
diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Downloader.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Downloader.kt
index 02b2831c..47dd7b30 100644
--- a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Downloader.kt
+++ b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Downloader.kt
@@ -2,6 +2,7 @@ package app.revanced.manager.plugin.downloader
import android.content.Intent
import java.io.File
+import java.io.InputStream
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
@DslMarker
@@ -18,27 +19,19 @@ fun interface ActivityLaunchPermit {
@DownloaderDsl
interface DownloadScope {
- /**
- * The location where the downloaded APK should be saved.
- */
- val targetFile: File
-
- /**
- * A callback for reporting download progress
- */
- suspend fun reportProgress(bytesReceived: Long, bytesTotal: Long?)
+ suspend fun reportSize(size: Long)
}
@DownloaderDsl
class DownloaderBuilder {
- private var download: (suspend DownloadScope.(A) -> Unit)? = null
+ private var download: (suspend DownloadScope.(A) -> InputStream)? = null
private var get: (suspend GetScope.(String, String?) -> A?)? = null
fun get(block: suspend GetScope.(packageName: String, version: String?) -> A?) {
get = block
}
- fun download(block: suspend DownloadScope.(app: A) -> Unit) {
+ fun download(block: suspend DownloadScope.(app: A) -> InputStream) {
download = block
}
@@ -50,7 +43,7 @@ class DownloaderBuilder {
class Downloader internal constructor(
val get: suspend GetScope.(packageName: String, version: String?) -> A?,
- val download: suspend DownloadScope.(app: A) -> Unit
+ val download: suspend DownloadScope.(app: A) -> InputStream
)
fun downloader(block: DownloaderBuilder.() -> Unit) =
diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Extensions.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Extensions.kt
new file mode 100644
index 00000000..334b446a
--- /dev/null
+++ b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/Extensions.kt
@@ -0,0 +1,96 @@
+package app.revanced.manager.plugin.downloader
+
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import java.io.FilterInputStream
+import java.io.FilterOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+import java.io.PipedInputStream
+import java.io.PipedOutputStream
+
+// OutputStream-based version of download
+fun DownloaderBuilder.download(block: suspend DownloadScope.(A, OutputStream) -> Unit) =
+ download { app ->
+ val input = PipedInputStream(1024 * 1024)
+ var currentThrowable: Throwable? = null
+
+ val coroutineScope =
+ CoroutineScope(Dispatchers.IO + Job() + CoroutineExceptionHandler { _, throwable ->
+ currentThrowable?.let {
+ it.addSuppressed(throwable)
+ return@CoroutineExceptionHandler
+ }
+
+ currentThrowable = throwable
+ })
+ var started = false
+
+ fun rethrowException() {
+ currentThrowable?.let {
+ currentThrowable = null
+ throw it
+ }
+ }
+
+ fun start() {
+ started = true
+ coroutineScope.launch {
+ PipedOutputStream(input).use {
+ block(app, object : FilterOutputStream(it) {
+ var closed = false
+
+ private fun assertIsOpen() {
+ if (closed) throw IOException("Stream is closed.")
+ }
+
+ override fun write(b: ByteArray?, off: Int, len: Int) {
+ assertIsOpen()
+ super.write(b, off, len)
+ }
+
+ override fun write(b: Int) {
+ assertIsOpen()
+ super.write(b)
+ }
+
+ override fun close() {
+ closed = true
+ }
+ })
+ }
+ }
+ }
+
+ object : FilterInputStream(input) {
+ override fun read(): Int {
+ val array = ByteArray(1)
+ if (read(array, 0, 1) != 1) return -1
+ return array[0].toInt()
+ }
+
+ override fun read(b: ByteArray?, off: Int, len: Int): Int {
+ if (!started) start()
+ rethrowException()
+ return super.read(b, off, len)
+ }
+
+ override fun close() {
+ super.close()
+ coroutineScope.cancel()
+ rethrowException()
+ }
+ }
+ }
+
+fun DownloaderBuilder.download(block: suspend DownloadScope.(A, (InputStream) -> Unit) -> Unit) =
+ download { app, outputStream: OutputStream ->
+ block(app) { inputStream ->
+ inputStream.use { it.copyTo(outputStream) }
+ }
+ }
\ No newline at end of file
diff --git a/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/ExamplePlugins.kt b/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/ExamplePlugins.kt
index d706b60d..3f2b33b8 100644
--- a/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/ExamplePlugins.kt
+++ b/example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/ExamplePlugins.kt
@@ -6,6 +6,7 @@ import android.content.Intent
import android.content.pm.PackageManager
import app.revanced.manager.plugin.downloader.App
import app.revanced.manager.plugin.downloader.DownloaderContext
+import app.revanced.manager.plugin.downloader.download
import app.revanced.manager.plugin.downloader.downloader
import app.revanced.manager.plugin.downloader.example.BuildConfig.PLUGIN_PACKAGE_NAME
import kotlinx.coroutines.delay
@@ -13,6 +14,8 @@ import kotlinx.parcelize.Parcelize
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import kotlin.io.path.Path
+import kotlin.io.path.fileSize
+import kotlin.io.path.inputStream
// TODO: document API, change dispatcher.
@@ -47,15 +50,16 @@ fun installedAppDownloader(context: DownloaderContext) = downloader
+ Path(app.apkPath).also {
+ reportSize(it.fileSize())
+ }.inputStream()
+ }*/
- Files.copy(Path(it.apkPath), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
+ download { app, outputStream ->
+ val path = Path(app.apkPath)
+ reportSize(path.fileSize())
+ Files.copy(path, outputStream)
}
-}
-
-private val Int.megaBytes get() = times(1_000_000)
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3b0ecfd1..10030fba 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -12,6 +12,7 @@ accompanist = "0.34.0"
placeholder = "1.1.2"
reorderable = "1.5.2"
serialization = "1.6.3"
+coroutines = "1.8.1"
collection = "0.3.7"
room-version = "2.6.1"
revanced-patcher = "19.3.1"
@@ -68,6 +69,7 @@ placeholder-material3 = { group = "io.github.fornewid", name = "placeholder-mate
# Kotlinx
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
kotlinx-collection-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "collection" }
+kotlinx-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
# Room
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room-version" }