mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-05-04 07:44:25 +02:00
new plugin API WIP
This commit is contained in:
parent
ce38745dd3
commit
45b1d18685
@ -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")
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
)
|
||||
|
@ -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
|
||||
)
|
@ -33,6 +33,10 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.kotlinx.coroutines)
|
||||
}
|
||||
|
||||
publishing {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
|
@ -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) =
|
||||
|
@ -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) }
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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" }
|
||||
|
Loading…
x
Reference in New Issue
Block a user