mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-05-04 15:54: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.network.downloader.LoadedDownloaderPlugin
|
||||||
import app.revanced.manager.plugin.downloader.App
|
import app.revanced.manager.plugin.downloader.App
|
||||||
import app.revanced.manager.plugin.downloader.DownloadScope
|
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.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import java.io.File
|
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) {
|
class DownloadedAppRepository(app: Application, db: AppDatabase) {
|
||||||
private val dir = app.getDir("downloaded-apps", Context.MODE_PRIVATE)
|
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()
|
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()
|
private fun getApkFileForDir(directory: File) = directory.listFiles()!!.first()
|
||||||
|
|
||||||
suspend fun download(
|
suspend fun download(
|
||||||
@ -32,10 +42,19 @@ class DownloadedAppRepository(app: Application, db: AppDatabase) {
|
|||||||
// Converted integers cannot contain / or .. unlike the package name or version, so they are safer to use here.
|
// Converted integers cannot contain / or .. unlike the package name or version, so they are safer to use here.
|
||||||
val relativePath = File(generateUid().toString())
|
val relativePath = File(generateUid().toString())
|
||||||
val saveDir = dir.resolve(relativePath).also { it.mkdirs() }
|
val saveDir = dir.resolve(relativePath).also { it.mkdirs() }
|
||||||
val targetFile = saveDir.resolve("base.apk")
|
val targetFile = saveDir.resolve("base.apk").toPath()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
channelFlow {
|
||||||
|
var fileSize: Long? = null
|
||||||
|
var downloadedBytes = 0L
|
||||||
|
|
||||||
val scope = object : DownloadScope {
|
val scope = object : DownloadScope {
|
||||||
|
override suspend fun reportSize(size: Long) {
|
||||||
|
fileSize = size
|
||||||
|
send(downloadedBytes to size)
|
||||||
|
}
|
||||||
|
/*
|
||||||
override val targetFile = targetFile
|
override val targetFile = targetFile
|
||||||
override suspend fun reportProgress(bytesReceived: Long, bytesTotal: Long?) {
|
override suspend fun reportProgress(bytesReceived: Long, bytesTotal: Long?) {
|
||||||
require(bytesReceived >= 0) { "bytesReceived must not be negative" }
|
require(bytesReceived >= 0) { "bytesReceived must not be negative" }
|
||||||
@ -43,10 +62,31 @@ class DownloadedAppRepository(app: Application, db: AppDatabase) {
|
|||||||
require(bytesTotal != 0L) { "bytesTotal must not be zero" }
|
require(bytesTotal != 0L) { "bytesTotal must not be zero" }
|
||||||
|
|
||||||
onDownload(bytesReceived.megaBytes to bytesTotal?.megaBytes)
|
onDownload(bytesReceived.megaBytes to bytesTotal?.megaBytes)
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
plugin.download(scope, app)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.conflate()
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.collect { (downloaded, size) -> onDownload(downloaded.megaBytes to size.megaBytes) }
|
||||||
|
|
||||||
if (!targetFile.exists()) throw Exception("Downloader did not download any files")
|
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.combine
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.InputStream
|
||||||
import java.lang.reflect.Modifier
|
import java.lang.reflect.Modifier
|
||||||
|
|
||||||
class DownloaderPluginRepository(
|
class DownloaderPluginRepository(
|
||||||
@ -112,7 +113,7 @@ class DownloaderPluginRepository(
|
|||||||
with(pm) { packageInfo.label() },
|
with(pm) { packageInfo.label() },
|
||||||
packageInfo.versionName,
|
packageInfo.versionName,
|
||||||
downloader.get,
|
downloader.get,
|
||||||
downloader.download as suspend DownloadScope.(App) -> Unit,
|
downloader.download as suspend DownloadScope.(App) -> InputStream,
|
||||||
classLoader
|
classLoader
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -4,12 +4,13 @@ import android.content.Context
|
|||||||
import app.revanced.manager.plugin.downloader.App
|
import app.revanced.manager.plugin.downloader.App
|
||||||
import app.revanced.manager.plugin.downloader.DownloadScope
|
import app.revanced.manager.plugin.downloader.DownloadScope
|
||||||
import app.revanced.manager.plugin.downloader.GetScope
|
import app.revanced.manager.plugin.downloader.GetScope
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
class LoadedDownloaderPlugin(
|
class LoadedDownloaderPlugin(
|
||||||
val packageName: String,
|
val packageName: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val version: String,
|
val version: String,
|
||||||
val get: suspend GetScope.(packageName: String, version: String?) -> App?,
|
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
|
val classLoader: ClassLoader
|
||||||
)
|
)
|
@ -33,6 +33,10 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.kotlinx.coroutines)
|
||||||
|
}
|
||||||
|
|
||||||
publishing {
|
publishing {
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
|
@ -2,6 +2,7 @@ package app.revanced.manager.plugin.downloader
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
|
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
|
||||||
@DslMarker
|
@DslMarker
|
||||||
@ -18,27 +19,19 @@ fun interface ActivityLaunchPermit {
|
|||||||
|
|
||||||
@DownloaderDsl
|
@DownloaderDsl
|
||||||
interface DownloadScope {
|
interface DownloadScope {
|
||||||
/**
|
suspend fun reportSize(size: Long)
|
||||||
* 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?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@DownloaderDsl
|
@DownloaderDsl
|
||||||
class DownloaderBuilder<A : App> {
|
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
|
private var get: (suspend GetScope.(String, String?) -> A?)? = null
|
||||||
|
|
||||||
fun get(block: suspend GetScope.(packageName: String, version: String?) -> A?) {
|
fun get(block: suspend GetScope.(packageName: String, version: String?) -> A?) {
|
||||||
get = block
|
get = block
|
||||||
}
|
}
|
||||||
|
|
||||||
fun download(block: suspend DownloadScope.(app: A) -> Unit) {
|
fun download(block: suspend DownloadScope.(app: A) -> InputStream) {
|
||||||
download = block
|
download = block
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +43,7 @@ class DownloaderBuilder<A : App> {
|
|||||||
|
|
||||||
class Downloader<A : App> internal constructor(
|
class Downloader<A : App> internal constructor(
|
||||||
val get: suspend GetScope.(packageName: String, version: String?) -> A?,
|
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) =
|
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 android.content.pm.PackageManager
|
||||||
import app.revanced.manager.plugin.downloader.App
|
import app.revanced.manager.plugin.downloader.App
|
||||||
import app.revanced.manager.plugin.downloader.DownloaderContext
|
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.downloader
|
||||||
import app.revanced.manager.plugin.downloader.example.BuildConfig.PLUGIN_PACKAGE_NAME
|
import app.revanced.manager.plugin.downloader.example.BuildConfig.PLUGIN_PACKAGE_NAME
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@ -13,6 +14,8 @@ import kotlinx.parcelize.Parcelize
|
|||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.StandardCopyOption
|
import java.nio.file.StandardCopyOption
|
||||||
import kotlin.io.path.Path
|
import kotlin.io.path.Path
|
||||||
|
import kotlin.io.path.fileSize
|
||||||
|
import kotlin.io.path.inputStream
|
||||||
|
|
||||||
// TODO: document API, change dispatcher.
|
// TODO: document API, change dispatcher.
|
||||||
|
|
||||||
@ -47,15 +50,16 @@ fun installedAppDownloader(context: DownloaderContext) = downloader<InstalledApp
|
|||||||
).takeIf { version == null || it.version == version }
|
).takeIf { version == null || it.version == version }
|
||||||
}
|
}
|
||||||
|
|
||||||
download {
|
/*
|
||||||
// Simulate download progress
|
download { app ->
|
||||||
for (i in 0..5) {
|
Path(app.apkPath).also {
|
||||||
reportProgress(i.megaBytes, 5.megaBytes)
|
reportSize(it.fileSize())
|
||||||
delay(1000L)
|
}.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"
|
placeholder = "1.1.2"
|
||||||
reorderable = "1.5.2"
|
reorderable = "1.5.2"
|
||||||
serialization = "1.6.3"
|
serialization = "1.6.3"
|
||||||
|
coroutines = "1.8.1"
|
||||||
collection = "0.3.7"
|
collection = "0.3.7"
|
||||||
room-version = "2.6.1"
|
room-version = "2.6.1"
|
||||||
revanced-patcher = "19.3.1"
|
revanced-patcher = "19.3.1"
|
||||||
@ -68,6 +69,7 @@ placeholder-material3 = { group = "io.github.fornewid", name = "placeholder-mate
|
|||||||
# Kotlinx
|
# Kotlinx
|
||||||
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
|
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-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
|
||||||
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room-version" }
|
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room-version" }
|
||||||
|
Loading…
x
Reference in New Issue
Block a user