more api changes

This commit is contained in:
Ax333l 2024-08-22 15:44:07 +02:00
parent 45b1d18685
commit e14497a1ce
No known key found for this signature in database
GPG Key ID: D2B4D85271127D23
15 changed files with 243 additions and 250 deletions

View File

@ -3,10 +3,11 @@ import kotlin.random.Random
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.devtools) alias(libs.plugins.devtools)
alias(libs.plugins.about.libraries) alias(libs.plugins.about.libraries)
id("kotlin-parcelize") alias(libs.plugins.compose.compiler)
kotlin("plugin.serialization") version "1.9.23"
} }
android { android {
@ -81,9 +82,11 @@ android {
jvmTarget = "17" jvmTarget = "17"
} }
buildFeatures.compose = true buildFeatures {
buildFeatures.aidl = true compose = true
buildFeatures.buildConfig = true aidl = true
buildConfig = true
}
android { android {
androidResources { androidResources {
@ -91,7 +94,6 @@ android {
} }
} }
composeOptions.kotlinCompilerExtensionVersion = "1.5.10"
externalNativeBuild { externalNativeBuild {
cmake { cmake {
path = file("src/main/cpp/CMakeLists.txt") path = file("src/main/cpp/CMakeLists.txt")

View File

@ -14,10 +14,10 @@ import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import java.io.File import java.io.File
import java.io.FilterInputStream import java.io.FilterOutputStream
import java.nio.file.Files import java.nio.file.StandardOpenOption
import java.nio.file.StandardCopyOption import java.util.concurrent.atomic.AtomicLong
import kotlin.io.path.exists import kotlin.io.path.outputStream
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)
@ -45,50 +45,49 @@ class DownloadedAppRepository(app: Application, db: AppDatabase) {
val targetFile = saveDir.resolve("base.apk").toPath() val targetFile = saveDir.resolve("base.apk").toPath()
try { try {
channelFlow { val downloadSize = AtomicLong(0)
var fileSize: Long? = null val downloadedBytes = AtomicLong(0)
var downloadedBytes = 0L
channelFlow {
val scope = object : DownloadScope { val scope = object : DownloadScope {
override suspend fun reportSize(size: Long) { override suspend fun reportSize(size: Long) {
fileSize = size require(size > 0) { "Size must be greater than zero" }
send(downloadedBytes to size) require(
downloadSize.compareAndSet(
0,
size
)
) { "Download size has already been set" }
send(downloadedBytes.get() 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 -> fun emitProgress(bytes: Long) {
Files.copy(object : FilterInputStream(inputStream) { val newValue = downloadedBytes.addAndGet(bytes)
override fun read(): Int { val totalSize = downloadSize.get()
val array = ByteArray(1) if (totalSize < 1) return
if (read(array, 0, 1) != 1) return -1 trySend(newValue to totalSize).getOrThrow()
return array[0].toInt()
} }
override fun read(b: ByteArray?, off: Int, len: Int) = targetFile.outputStream(StandardOpenOption.CREATE_NEW).buffered().use {
super.read(b, off, len).also { result -> val stream = object : FilterOutputStream(it) {
// Report download progress override fun write(b: Int) = out.write(b).also { emitProgress(1) }
if (result > 0) {
downloadedBytes += result override fun write(b: ByteArray?, off: Int, len: Int) =
fileSize?.let { trySend(downloadedBytes to it).getOrThrow() } out.write(b, off, len).also {
emitProgress(
(len - off).toLong()
)
} }
} }
}, targetFile, StandardCopyOption.REPLACE_EXISTING) plugin.download(scope, app, stream)
} }
} }
.conflate() .conflate()
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.collect { (downloaded, size) -> onDownload(downloaded.megaBytes to size.megaBytes) } .collect { (downloaded, size) -> onDownload(downloaded.megaBytes to size.megaBytes) }
if (!targetFile.exists()) throw Exception("Downloader did not download any files") if (downloadedBytes.get() < 1) throw Exception("Downloader did not download any files")
dao.insert( dao.insert(
DownloadedApp( DownloadedApp(

View File

@ -1,7 +1,6 @@
package app.revanced.manager.domain.repository package app.revanced.manager.domain.repository
import android.app.Application import android.app.Application
import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.util.Log import android.util.Log
import app.revanced.manager.data.room.AppDatabase import app.revanced.manager.data.room.AppDatabase
@ -11,9 +10,9 @@ import app.revanced.manager.network.downloader.DownloaderPluginState
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
import app.revanced.manager.network.downloader.ParceledDownloaderApp import app.revanced.manager.network.downloader.ParceledDownloaderApp
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.Downloader import app.revanced.manager.plugin.downloader.Downloader
import app.revanced.manager.plugin.downloader.DownloaderContext import app.revanced.manager.plugin.downloader.DownloaderBuilder
import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import dalvik.system.PathClassLoader import dalvik.system.PathClassLoader
@ -24,9 +23,9 @@ 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
@OptIn(PluginHostApi::class)
class DownloaderPluginRepository( class DownloaderPluginRepository(
private val pm: PM, private val pm: PM,
private val prefs: PreferencesManager, private val prefs: PreferencesManager,
@ -88,32 +87,28 @@ class DownloaderPluginRepository(
return try { return try {
val packageInfo = pm.getPackageInfo(packageName, flags = PackageManager.GET_META_DATA)!! val packageInfo = pm.getPackageInfo(packageName, flags = PackageManager.GET_META_DATA)!!
val pluginContext = app.createPackageContext(packageName, 0)
val className = packageInfo.applicationInfo.metaData.getString(METADATA_PLUGIN_CLASS) val className = packageInfo.applicationInfo.metaData.getString(METADATA_PLUGIN_CLASS)
?: throw Exception("Missing metadata attribute $METADATA_PLUGIN_CLASS") ?: throw Exception("Missing metadata attribute $METADATA_PLUGIN_CLASS")
val classLoader = PathClassLoader(
packageInfo.applicationInfo.sourceDir, val classLoader =
Downloader::class.java.classLoader PathClassLoader(packageInfo.applicationInfo.sourceDir, app.classLoader)
) val pluginContext = app.createPackageContext(packageName, 0)
val downloader = classLoader val downloader = classLoader
.loadClass(className) .loadClass(className)
.getDownloaderImplementation( .getDownloaderBuilder()
DownloaderContext( .build(
androidContext = pluginContext, hostPackageName = app.packageName,
pluginHostPackageName = app.packageName context = pluginContext
)
) )
@Suppress("UNCHECKED_CAST")
DownloaderPluginState.Loaded( DownloaderPluginState.Loaded(
LoadedDownloaderPlugin( LoadedDownloaderPlugin(
packageName, packageName,
with(pm) { packageInfo.label() }, with(pm) { packageInfo.label() },
packageInfo.versionName, packageInfo.versionName,
downloader.get, downloader.get,
downloader.download as suspend DownloadScope.(App) -> InputStream, downloader.download,
classLoader classLoader
) )
) )
@ -156,22 +151,15 @@ class DownloaderPluginRepository(
const val PLUGIN_FEATURE = "app.revanced.manager.plugin.downloader" const val PLUGIN_FEATURE = "app.revanced.manager.plugin.downloader"
const val METADATA_PLUGIN_CLASS = "app.revanced.manager.plugin.downloader.class" const val METADATA_PLUGIN_CLASS = "app.revanced.manager.plugin.downloader.class"
val Class<*>.isDownloader get() = Downloader::class.java.isAssignableFrom(this)
const val PUBLIC_STATIC = Modifier.PUBLIC or Modifier.STATIC const val PUBLIC_STATIC = Modifier.PUBLIC or Modifier.STATIC
val Int.isPublicStatic get() = (this and PUBLIC_STATIC) == PUBLIC_STATIC val Int.isPublicStatic get() = (this and PUBLIC_STATIC) == PUBLIC_STATIC
val Class<*>.isDownloaderBuilder get() = DownloaderBuilder::class.java.isAssignableFrom(this)
fun Class<*>.getDownloaderImplementation(context: DownloaderContext) = @Suppress("UNCHECKED_CAST")
fun Class<*>.getDownloaderBuilder() =
declaredMethods declaredMethods
.filter { it.modifiers.isPublicStatic && it.returnType.isDownloader } .firstOrNull { it.modifiers.isPublicStatic && it.returnType.isDownloaderBuilder && it.parameterTypes.isEmpty() }
.firstNotNullOfOrNull callMethod@{ ?.let { it(null) as DownloaderBuilder<App> }
if (it.parameterTypes contentEquals arrayOf(DownloaderContext::class.java)) return@callMethod it(
null,
context
) as Downloader<*>
if (it.parameterTypes.isEmpty()) return@callMethod it(null) as Downloader<*>
return@callMethod null
}
?: throw Exception("Could not find a valid downloader implementation in class $canonicalName") ?: throw Exception("Could not find a valid downloader implementation in class $canonicalName")
} }
} }

View File

@ -1,16 +1,15 @@
package app.revanced.manager.network.downloader package app.revanced.manager.network.downloader
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 import java.io.OutputStream
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) -> InputStream, val download: suspend DownloadScope.(app: App, outputStream: OutputStream) -> Unit,
val classLoader: ClassLoader val classLoader: ClassLoader
) )

View File

@ -3,8 +3,11 @@ package app.revanced.manager.ui.screen
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowRight import androidx.compose.material.icons.automirrored.outlined.ArrowRight
import androidx.compose.material.icons.filled.AutoFixHigh import androidx.compose.material.icons.filled.AutoFixHigh
@ -13,18 +16,24 @@ import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.ui.component.AlertDialogExtended
import app.revanced.manager.ui.component.AppInfo import app.revanced.manager.ui.component.AppInfo
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ColumnWithScrollbar import app.revanced.manager.ui.component.ColumnWithScrollbar
@ -36,6 +45,7 @@ import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
import app.revanced.manager.util.Options import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import app.revanced.manager.util.transparentListItemColors
import dev.olshevski.navigation.reimagined.AnimatedNavHost import dev.olshevski.navigation.reimagined.AnimatedNavHost
import dev.olshevski.navigation.reimagined.NavBackHandler import dev.olshevski.navigation.reimagined.NavBackHandler
import dev.olshevski.navigation.reimagined.navigate import dev.olshevski.navigation.reimagined.navigate
@ -70,6 +80,10 @@ fun SelectedAppInfoScreen(
} }
} }
var showSourceSelectorDialog by rememberSaveable {
mutableStateOf(false)
}
val navController = val navController =
rememberNavController<SelectedAppInfoDestination>(startDestination = SelectedAppInfoDestination.Main) rememberNavController<SelectedAppInfoDestination>(startDestination = SelectedAppInfoDestination.Main)
@ -102,7 +116,8 @@ fun SelectedAppInfoScreen(
) )
) )
}, },
onVersionSelectorClick = { onSourceSelectorClick = {
showSourceSelectorDialog = true
// navController.navigate(SelectedAppInfoDestination.VersionSelector) // navController.navigate(SelectedAppInfoDestination.VersionSelector)
}, },
onBackClick = onBackClick, onBackClick = onBackClick,
@ -137,7 +152,7 @@ fun SelectedAppInfoScreen(
private fun SelectedAppInfoScreen( private fun SelectedAppInfoScreen(
onPatchClick: () -> Unit, onPatchClick: () -> Unit,
onPatchSelectorClick: () -> Unit, onPatchSelectorClick: () -> Unit,
onVersionSelectorClick: () -> Unit, onSourceSelectorClick: () -> Unit,
onBackClick: () -> Unit, onBackClick: () -> Unit,
selectedPatchCount: Int, selectedPatchCount: Int,
packageName: String, packageName: String,
@ -186,7 +201,7 @@ private fun SelectedAppInfoScreen(
R.string.version_selector_item, R.string.version_selector_item,
version?.let { stringResource(R.string.version_selector_item_description, it) } version?.let { stringResource(R.string.version_selector_item_description, it) }
?: stringResource(R.string.version_selector_item_description_auto), ?: stringResource(R.string.version_selector_item_description_auto),
onVersionSelectorClick onSourceSelectorClick
) )
} }
} }
@ -217,3 +232,59 @@ private fun PageItem(@StringRes title: Int, description: String, onClick: () ->
} }
) )
} }
@Composable
private fun AppSourceSelectorDialog(onDismissRequest: () -> Unit) {
AlertDialogExtended(
onDismissRequest = onDismissRequest,
confirmButton = {
TextButton(
onClick = {
}
) {
Text("Select")
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.cancel))
}
},
title = { Text("Select source") },
textHorizontalPadding = PaddingValues(horizontal = 0.dp),
text = {
/*
val presets = remember(scope.option.presets) {
scope.option.presets?.entries?.toList().orEmpty()
}
LazyColumn {
@Composable
fun Item(title: String, value: Any?, presetKey: String?) {
ListItem(
modifier = Modifier.clickable { selectedPreset = presetKey },
headlineContent = { Text(title) },
supportingContent = value?.toString()?.let { { Text(it) } },
leadingContent = {
RadioButton(
selected = selectedPreset == presetKey,
onClick = { selectedPreset = presetKey }
)
},
colors = transparentListItemColors
)
}
items(presets, key = { it.key }) {
Item(it.key, it.value, it.key)
}
item(key = null) {
Item(stringResource(R.string.option_preset_custom_value), null, null)
}
}
*/
}
)
}

View File

@ -45,6 +45,9 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
mutableStateOf(input.app) mutableStateOf(input.app)
} }
var selectedAppInfo: PackageInfo? by mutableStateOf(null)
private set
var selectedApp var selectedApp
get() = _selectedApp get() = _selectedApp
set(value) { set(value) {
@ -52,8 +55,6 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
invalidateSelectedAppInfo() invalidateSelectedAppInfo()
} }
var selectedAppInfo: PackageInfo? by mutableStateOf(null)
init { init {
invalidateSelectedAppInfo() invalidateSelectedAppInfo()
} }
@ -64,8 +65,8 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
viewModelScope.launch { viewModelScope.launch {
if (!persistConfiguration) return@launch // TODO: save options for patched apps. if (!persistConfiguration) return@launch // TODO: save options for patched apps.
val packageName = // Accessing this from another thread may cause crashes.
selectedApp.packageName // Accessing this from another thread may cause crashes. val packageName = selectedApp.packageName
state.value = withContext(Dispatchers.Default) { state.value = withContext(Dispatchers.Default) {
val bundlePatches = bundleRepository.bundles.first() val bundlePatches = bundleRepository.bundles.first()

View File

@ -2,11 +2,15 @@ plugins {
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
alias(libs.plugins.devtools) apply false alias(libs.plugins.devtools) apply false
alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.kotlin.parcelize) apply false
alias(libs.plugins.about.libraries) apply false alias(libs.plugins.about.libraries) apply false
alias(libs.plugins.android.library) apply false alias(libs.plugins.android.library) apply false
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.binary.compatibility.validator) alias(libs.plugins.binary.compatibility.validator)
} }
apiValidation { apiValidation {
ignoredProjects.addAll(listOf("app", "example-downloader-plugin")) ignoredProjects.addAll(listOf("app", "example-downloader-plugin"))
nonPublicMarkers += "app.revanced.manager.plugin.downloader.PluginHostApi"
} }

View File

@ -26,20 +26,13 @@ public final class app/revanced/manager/plugin/downloader/ConstantsKt {
} }
public abstract interface class app/revanced/manager/plugin/downloader/DownloadScope { public abstract interface class app/revanced/manager/plugin/downloader/DownloadScope {
public abstract fun getTargetFile ()Ljava/io/File; public abstract fun reportSize (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun reportProgress (JLjava/lang/Long;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
} }
public final class app/revanced/manager/plugin/downloader/Downloader { public final class app/revanced/manager/plugin/downloader/Downloader {
public final fun getDownload ()Lkotlin/jvm/functions/Function3;
public final fun getGet ()Lkotlin/jvm/functions/Function4;
} }
public final class app/revanced/manager/plugin/downloader/DownloaderBuilder { public final class app/revanced/manager/plugin/downloader/DownloaderBuilder {
public fun <init> ()V
public final fun build ()Lapp/revanced/manager/plugin/downloader/Downloader;
public final fun download (Lkotlin/jvm/functions/Function3;)V
public final fun get (Lkotlin/jvm/functions/Function4;)V
} }
public final class app/revanced/manager/plugin/downloader/DownloaderContext { public final class app/revanced/manager/plugin/downloader/DownloaderContext {
@ -48,17 +41,28 @@ public final class app/revanced/manager/plugin/downloader/DownloaderContext {
public final fun getPluginHostPackageName ()Ljava/lang/String; public final fun getPluginHostPackageName ()Ljava/lang/String;
} }
public abstract interface annotation class app/revanced/manager/plugin/downloader/DownloaderDsl : java/lang/annotation/Annotation { public final class app/revanced/manager/plugin/downloader/DownloaderKt {
public static final fun downloader (Lkotlin/jvm/functions/Function1;)Lapp/revanced/manager/plugin/downloader/DownloaderBuilder;
} }
public final class app/revanced/manager/plugin/downloader/DownloaderKt { public final class app/revanced/manager/plugin/downloader/DownloaderScope {
public static final fun downloader (Lkotlin/jvm/functions/Function1;)Lapp/revanced/manager/plugin/downloader/Downloader; public final fun download (Lkotlin/jvm/functions/Function2;)V
public final fun get (Lkotlin/jvm/functions/Function4;)V
public final fun getHostPackageName ()Ljava/lang/String;
public final fun getPluginPackageName ()Ljava/lang/String;
}
public final class app/revanced/manager/plugin/downloader/ExtensionsKt {
public static final fun download (Lapp/revanced/manager/plugin/downloader/DownloaderScope;Lkotlin/jvm/functions/Function4;)V
} }
public abstract interface class app/revanced/manager/plugin/downloader/GetScope { public abstract interface class app/revanced/manager/plugin/downloader/GetScope {
public abstract fun requestUserInteraction (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun requestUserInteraction (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
} }
public abstract interface annotation class app/revanced/manager/plugin/downloader/PluginHostApi : java/lang/annotation/Annotation {
}
public abstract class app/revanced/manager/plugin/downloader/UserInteractionException : java/lang/Exception { public abstract class app/revanced/manager/plugin/downloader/UserInteractionException : java/lang/Exception {
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
} }

View File

@ -1,7 +1,7 @@
plugins { plugins {
alias(libs.plugins.android.library) alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
id("kotlin-parcelize") alias(libs.plugins.kotlin.parcelize)
`maven-publish` `maven-publish`
} }
@ -33,10 +33,6 @@ android {
} }
} }
dependencies {
implementation(libs.kotlinx.coroutines)
}
publishing { publishing {
repositories { repositories {
mavenLocal() mavenLocal()

View File

@ -1,14 +1,16 @@
package app.revanced.manager.plugin.downloader package app.revanced.manager.plugin.downloader
import android.content.Context
import android.content.Intent import android.content.Intent
import java.io.File
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE) @RequiresOptIn(
@DslMarker level = RequiresOptIn.Level.ERROR,
annotation class DownloaderDsl message = "This API is only intended for plugin hosts, don't use it in a plugin.",
)
annotation class PluginHostApi
@DownloaderDsl
interface GetScope { interface GetScope {
suspend fun requestUserInteraction(): ActivityLaunchPermit suspend fun requestUserInteraction(): ActivityLaunchPermit
} }
@ -17,37 +19,66 @@ fun interface ActivityLaunchPermit {
suspend fun launch(intent: Intent): Intent? suspend fun launch(intent: Intent): Intent?
} }
@DownloaderDsl
interface DownloadScope { interface DownloadScope {
suspend fun reportSize(size: Long) suspend fun reportSize(size: Long)
} }
@DownloaderDsl typealias Size = Long
class DownloaderBuilder<A : App> { typealias DownloadResult = Pair<InputStream, Size?>
private var download: (suspend DownloadScope.(A) -> InputStream)? = null
private var get: (suspend GetScope.(String, String?) -> A?)? = null class DownloaderScope<A : App> internal constructor(
/**
* The package name of ReVanced Manager.
*/
val hostPackageName: String,
internal val context: Context
) {
internal var download: (suspend DownloadScope.(A, OutputStream) -> Unit)? = null
internal var get: (suspend GetScope.(String, String?) -> A?)? = null
/**
* The package name of the plugin.
*/
val pluginPackageName: String get() = context.packageName
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) -> InputStream) { /**
download = block * Define the download function for this plugin.
} */
fun download(block: suspend (app: A) -> DownloadResult) {
download = { app, outputStream ->
val (inputStream, size) = block(app)
fun build() = Downloader( inputStream.use {
if (size != null) reportSize(size)
it.copyTo(outputStream)
}
}
}
}
class DownloaderBuilder<A : App> internal constructor(private val block: DownloaderScope<A>.() -> Unit) {
@PluginHostApi
fun build(hostPackageName: String, context: Context) =
with(DownloaderScope<A>(hostPackageName, context)) {
block()
Downloader(
download = download ?: error("download was not declared"), download = download ?: error("download was not declared"),
get = get ?: error("get was not declared") get = get ?: error("get was not declared")
) )
}
} }
class Downloader<A : App> internal constructor( class Downloader<A : App> internal constructor(
val get: suspend GetScope.(packageName: String, version: String?) -> A?, @property:PluginHostApi val get: suspend GetScope.(packageName: String, version: String?) -> A?,
val download: suspend DownloadScope.(app: A) -> InputStream @property:PluginHostApi val download: suspend DownloadScope.(app: A, outputStream: OutputStream) -> Unit
) )
fun <A : App> downloader(block: DownloaderBuilder<A>.() -> Unit) = fun <A : App> downloader(block: DownloaderScope<A>.() -> Unit) = DownloaderBuilder(block)
DownloaderBuilder<A>().apply(block).build()
sealed class UserInteractionException(message: String) : Exception(message) { sealed class UserInteractionException(message: String) : Exception(message) {
class RequestDenied : UserInteractionException("Request was denied") class RequestDenied : UserInteractionException("Request was denied")

View File

@ -1,12 +0,0 @@
package app.revanced.manager.plugin.downloader
import android.content.Context
@Suppress("Unused", "MemberVisibilityCanBePrivate")
/**
* The downloader plugin context.
*
* @param androidContext An Android [Context] for this plugin.
* @param pluginHostPackageName The package name of the plugin host.
*/
class DownloaderContext(val androidContext: Context, val pluginHostPackageName: String)

View File

@ -1,96 +1,8 @@
package app.revanced.manager.plugin.downloader 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.OutputStream
import java.io.PipedInputStream
import java.io.PipedOutputStream
// OutputStream-based version of download // OutputStream-based version of download
fun <A : App> DownloaderBuilder<A>.download(block: suspend DownloadScope.(A, OutputStream) -> Unit) = fun <A : App> DownloaderScope<A>.download(block: suspend DownloadScope.(A, OutputStream) -> Unit) {
download { app -> download = block
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

@ -1,7 +1,8 @@
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
id("kotlin-parcelize") alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.compose.compiler)
} }
android { android {
@ -16,7 +17,6 @@ android {
targetSdk = 34 targetSdk = 34
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
buildConfigField("String", "PLUGIN_PACKAGE_NAME", "\"$packageName\"")
} }
buildTypes { buildTypes {
@ -39,11 +39,7 @@ android {
kotlinOptions { kotlinOptions {
jvmTarget = "17" jvmTarget = "17"
} }
composeOptions.kotlinCompilerExtensionVersion = "1.5.10" buildFeatures.compose = true
buildFeatures {
compose = true
buildConfig = true
}
} }
dependencies { dependencies {

View File

@ -2,22 +2,19 @@
package app.revanced.manager.plugin.downloader.example package app.revanced.manager.plugin.downloader.example
import android.app.Application
import android.content.Intent 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.download 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 kotlinx.coroutines.delay
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.StandardCopyOption
import kotlin.io.path.Path import kotlin.io.path.Path
import kotlin.io.path.fileSize import kotlin.io.path.fileSize
import kotlin.io.path.inputStream import kotlin.io.path.inputStream
// TODO: document API, change dispatcher. // TODO: document and update API, change dispatcher, finish UI
@Parcelize @Parcelize
class InstalledApp( class InstalledApp(
@ -26,8 +23,15 @@ class InstalledApp(
internal val apkPath: String internal val apkPath: String
) : App(packageName, version) ) : App(packageName, version)
fun installedAppDownloader(context: DownloaderContext) = downloader<InstalledApp> { private val application by lazy {
val pm = context.androidContext.packageManager // Don't do this in a real plugin.
val clazz = Class.forName("android.app.ActivityThread")
val activityThread = clazz.getMethod("currentActivityThread")(null)
clazz.getMethod("getApplication")(activityThread) as Application
}
val installedAppDownloader = downloader<InstalledApp> {
val pm = application.packageManager
get { packageName, version -> get { packageName, version ->
val packageInfo = try { val packageInfo = try {
@ -38,7 +42,7 @@ fun installedAppDownloader(context: DownloaderContext) = downloader<InstalledApp
requestUserInteraction().launch(Intent().apply { requestUserInteraction().launch(Intent().apply {
setClassName( setClassName(
PLUGIN_PACKAGE_NAME, pluginPackageName,
InteractionActivity::class.java.canonicalName!! InteractionActivity::class.java.canonicalName!!
) )
}) })
@ -50,12 +54,9 @@ fun installedAppDownloader(context: DownloaderContext) = downloader<InstalledApp
).takeIf { version == null || it.version == version } ).takeIf { version == null || it.version == version }
} }
/*
download { app -> download { app ->
Path(app.apkPath).also { with(Path(app.apkPath)) { inputStream() to fileSize() }
reportSize(it.fileSize()) }
}.inputStream()
}*/
download { app, outputStream -> download { app, outputStream ->
val path = Path(app.apkPath) val path = Path(app.apkPath)

View File

@ -1,18 +1,18 @@
[versions] [versions]
kotlin = "2.0.10"
ktx = "1.13.1" ktx = "1.13.1"
material3 = "1.3.0-beta04" material3 = "1.3.0-beta05"
ui-tooling = "1.6.8" ui-tooling = "1.6.8"
viewmodel-lifecycle = "2.8.3" viewmodel-lifecycle = "2.8.4"
splash-screen = "1.0.1" splash-screen = "1.0.1"
compose-activity = "1.9.0" compose-activity = "1.9.1"
preferences-datastore = "1.1.1" preferences-datastore = "1.1.1"
work-runtime = "2.9.0" work-runtime = "2.9.1"
compose-bom = "2024.06.00" compose-bom = "2024.06.00"
accompanist = "0.34.0" 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.7.1"
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"
@ -24,8 +24,7 @@ ktor = "2.3.9"
markdown-renderer = "0.22.0" markdown-renderer = "0.22.0"
fading-edges = "1.0.4" fading-edges = "1.0.4"
android-gradle-plugin = "8.3.2" android-gradle-plugin = "8.3.2"
kotlin-gradle-plugin = "1.9.22" dev-tools-ksp-gradle-plugin = "2.0.10-1.0.24"
dev-tools-gradle-plugin = "1.9.22-1.0.17"
about-libraries-gradle-plugin = "11.1.1" about-libraries-gradle-plugin = "11.1.1"
binary-compatibility-validator = "0.15.1" binary-compatibility-validator = "0.15.1"
coil = "2.6.0" coil = "2.6.0"
@ -69,7 +68,6 @@ 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" }
@ -132,7 +130,10 @@ compose-icons-fontawesome = { group = "com.github.BenjaminHalko.compose-icons",
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" } android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" } android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin-gradle-plugin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
devtools = { id = "com.google.devtools.ksp", version.ref = "dev-tools-gradle-plugin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
devtools = { id = "com.google.devtools.ksp", version.ref = "dev-tools-ksp-gradle-plugin" }
about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "about-libraries-gradle-plugin" } about-libraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "about-libraries-gradle-plugin" }
binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" } binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" }