new plugin API WIP

This commit is contained in:
Ax333l 2024-08-18 20:12:44 +02:00
parent ce38745dd3
commit 45b1d18685
No known key found for this signature in database
GPG Key ID: D2B4D85271127D23
8 changed files with 176 additions and 35 deletions

View File

@ -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")

View File

@ -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
)
)

View File

@ -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
)

View File

@ -33,6 +33,10 @@ android {
}
}
dependencies {
implementation(libs.kotlinx.coroutines)
}
publishing {
repositories {
mavenLocal()

View File

@ -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<A : App> {
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<A : App> {
class Downloader<A : App> 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 <A : App> downloader(block: DownloaderBuilder<A>.() -> Unit) =

View File

@ -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 <A : App> DownloaderBuilder<A>.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 <A : App> DownloaderBuilder<A>.download(block: suspend DownloadScope.(A, (InputStream) -> Unit) -> Unit) =
download { app, outputStream: OutputStream ->
block(app) { inputStream ->
inputStream.use { it.copyTo(outputStream) }
}
}

View File

@ -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<InstalledApp
).takeIf { version == null || it.version == version }
}
download {
// Simulate download progress
for (i in 0..5) {
reportProgress(i.megaBytes, 5.megaBytes)
delay(1000L)
}
/*
download { app ->
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)
}

View File

@ -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" }