From e14497a1ce23f45bd9a0f6c3846f8d6ea4f80d6e Mon Sep 17 00:00:00 2001 From: Ax333l Date: Thu, 22 Aug 2024 15:44:07 +0200 Subject: [PATCH] more api changes --- app/build.gradle.kts | 14 +-- .../repository/DownloadedAppRepository.kt | 67 +++++++------ .../repository/DownloaderPluginRepository.kt | 46 ++++----- .../downloader/LoadedDownloaderPlugin.kt | 5 +- .../ui/screen/SelectedAppInfoScreen.kt | 77 ++++++++++++++- .../ui/viewmodel/SelectedAppInfoViewModel.kt | 9 +- build.gradle.kts | 4 + downloader-plugin/api/downloader-plugin.api | 26 ++--- downloader-plugin/build.gradle.kts | 6 +- .../manager/plugin/downloader/Downloader.kt | 73 +++++++++----- .../plugin/downloader/DownloaderContext.kt | 12 --- .../manager/plugin/downloader/Extensions.kt | 94 +------------------ example-downloader-plugin/build.gradle.kts | 10 +- .../{ExamplePlugins.kt => ExamplePlugin.kt} | 27 +++--- gradle/libs.versions.toml | 23 ++--- 15 files changed, 243 insertions(+), 250 deletions(-) delete mode 100644 downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/DownloaderContext.kt rename example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/{ExamplePlugins.kt => ExamplePlugin.kt} (71%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d206e0bc..6bba16a5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -3,10 +3,11 @@ import kotlin.random.Random plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.kotlin.parcelize) alias(libs.plugins.devtools) alias(libs.plugins.about.libraries) - id("kotlin-parcelize") - kotlin("plugin.serialization") version "1.9.23" + alias(libs.plugins.compose.compiler) } android { @@ -81,9 +82,11 @@ android { jvmTarget = "17" } - buildFeatures.compose = true - buildFeatures.aidl = true - buildFeatures.buildConfig = true + buildFeatures { + compose = true + aidl = true + buildConfig = true + } android { androidResources { @@ -91,7 +94,6 @@ android { } } - composeOptions.kotlinCompilerExtensionVersion = "1.5.10" externalNativeBuild { cmake { path = file("src/main/cpp/CMakeLists.txt") 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 8cfecbc9..9c591c1c 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 @@ -14,10 +14,10 @@ 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 +import java.io.FilterOutputStream +import java.nio.file.StandardOpenOption +import java.util.concurrent.atomic.AtomicLong +import kotlin.io.path.outputStream class DownloadedAppRepository(app: Application, db: AppDatabase) { 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() try { - channelFlow { - var fileSize: Long? = null - var downloadedBytes = 0L + val downloadSize = AtomicLong(0) + val downloadedBytes = AtomicLong(0) + channelFlow { val scope = object : DownloadScope { override suspend fun reportSize(size: Long) { - fileSize = size - send(downloadedBytes to size) + require(size > 0) { "Size must be greater than zero" } + 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 -> - 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() - } + fun emitProgress(bytes: Long) { + val newValue = downloadedBytes.addAndGet(bytes) + val totalSize = downloadSize.get() + if (totalSize < 1) return + trySend(newValue to totalSize).getOrThrow() + } - 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.outputStream(StandardOpenOption.CREATE_NEW).buffered().use { + val stream = object : FilterOutputStream(it) { + override fun write(b: Int) = out.write(b).also { emitProgress(1) } + + override fun write(b: ByteArray?, off: Int, len: Int) = + out.write(b, off, len).also { + emitProgress( + (len - off).toLong() + ) } - }, targetFile, StandardCopyOption.REPLACE_EXISTING) + } + plugin.download(scope, app, stream) } } .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 (downloadedBytes.get() < 1) throw Exception("Downloader did not download any files") dao.insert( DownloadedApp( 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 f87a65ec..0dda0467 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 @@ -1,7 +1,6 @@ package app.revanced.manager.domain.repository import android.app.Application -import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.util.Log 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.ParceledDownloaderApp 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.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.tag import dalvik.system.PathClassLoader @@ -24,9 +23,9 @@ 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 +@OptIn(PluginHostApi::class) class DownloaderPluginRepository( private val pm: PM, private val prefs: PreferencesManager, @@ -88,32 +87,28 @@ class DownloaderPluginRepository( return try { 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) ?: throw Exception("Missing metadata attribute $METADATA_PLUGIN_CLASS") - val classLoader = PathClassLoader( - packageInfo.applicationInfo.sourceDir, - Downloader::class.java.classLoader - ) + + val classLoader = + PathClassLoader(packageInfo.applicationInfo.sourceDir, app.classLoader) + val pluginContext = app.createPackageContext(packageName, 0) val downloader = classLoader .loadClass(className) - .getDownloaderImplementation( - DownloaderContext( - androidContext = pluginContext, - pluginHostPackageName = app.packageName - ) + .getDownloaderBuilder() + .build( + hostPackageName = app.packageName, + context = pluginContext ) - @Suppress("UNCHECKED_CAST") DownloaderPluginState.Loaded( LoadedDownloaderPlugin( packageName, with(pm) { packageInfo.label() }, packageInfo.versionName, downloader.get, - downloader.download as suspend DownloadScope.(App) -> InputStream, + downloader.download, classLoader ) ) @@ -156,22 +151,15 @@ class DownloaderPluginRepository( const val PLUGIN_FEATURE = "app.revanced.manager.plugin.downloader" 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 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 - .filter { it.modifiers.isPublicStatic && it.returnType.isDownloader } - .firstNotNullOfOrNull callMethod@{ - 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 - } + .firstOrNull { it.modifiers.isPublicStatic && it.returnType.isDownloaderBuilder && it.parameterTypes.isEmpty() } + ?.let { it(null) as DownloaderBuilder } ?: throw Exception("Could not find a valid downloader implementation in class $canonicalName") } } \ No newline at end of file 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 feab85b7..d7be6b83 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 @@ -1,16 +1,15 @@ package app.revanced.manager.network.downloader -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 +import java.io.OutputStream 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) -> InputStream, + val download: suspend DownloadScope.(app: App, outputStream: OutputStream) -> Unit, val classLoader: ClassLoader ) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt index da3e02ee..7685a357 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt @@ -3,8 +3,11 @@ package app.revanced.manager.ui.screen import android.content.pm.PackageInfo import androidx.annotation.StringRes import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize 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.automirrored.outlined.ArrowRight 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.ListItem import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf 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.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle 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.AppTopBar 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.PatchSelection import app.revanced.manager.util.toast +import app.revanced.manager.util.transparentListItemColors import dev.olshevski.navigation.reimagined.AnimatedNavHost import dev.olshevski.navigation.reimagined.NavBackHandler import dev.olshevski.navigation.reimagined.navigate @@ -70,6 +80,10 @@ fun SelectedAppInfoScreen( } } + var showSourceSelectorDialog by rememberSaveable { + mutableStateOf(false) + } + val navController = rememberNavController(startDestination = SelectedAppInfoDestination.Main) @@ -102,7 +116,8 @@ fun SelectedAppInfoScreen( ) ) }, - onVersionSelectorClick = { + onSourceSelectorClick = { + showSourceSelectorDialog = true // navController.navigate(SelectedAppInfoDestination.VersionSelector) }, onBackClick = onBackClick, @@ -137,7 +152,7 @@ fun SelectedAppInfoScreen( private fun SelectedAppInfoScreen( onPatchClick: () -> Unit, onPatchSelectorClick: () -> Unit, - onVersionSelectorClick: () -> Unit, + onSourceSelectorClick: () -> Unit, onBackClick: () -> Unit, selectedPatchCount: Int, packageName: String, @@ -186,7 +201,7 @@ private fun SelectedAppInfoScreen( R.string.version_selector_item, version?.let { stringResource(R.string.version_selector_item_description, it) } ?: stringResource(R.string.version_selector_item_description_auto), - onVersionSelectorClick + onSourceSelectorClick ) } } @@ -216,4 +231,60 @@ private fun PageItem(@StringRes title: Int, description: String, onClick: () -> Icon(Icons.AutoMirrored.Outlined.ArrowRight, null) } ) +} + +@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) + } + } + */ + } + ) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt index ee6c5c87..6d20be65 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt @@ -45,6 +45,9 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent { mutableStateOf(input.app) } + var selectedAppInfo: PackageInfo? by mutableStateOf(null) + private set + var selectedApp get() = _selectedApp set(value) { @@ -52,8 +55,6 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent { invalidateSelectedAppInfo() } - var selectedAppInfo: PackageInfo? by mutableStateOf(null) - init { invalidateSelectedAppInfo() } @@ -64,8 +65,8 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent { viewModelScope.launch { if (!persistConfiguration) return@launch // TODO: save options for patched apps. - val packageName = - selectedApp.packageName // Accessing this from another thread may cause crashes. + // Accessing this from another thread may cause crashes. + val packageName = selectedApp.packageName state.value = withContext(Dispatchers.Default) { val bundlePatches = bundleRepository.bundles.first() diff --git a/build.gradle.kts b/build.gradle.kts index 12a5ac9d..11126717 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,11 +2,15 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.devtools) 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.android.library) apply false + alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.binary.compatibility.validator) } apiValidation { ignoredProjects.addAll(listOf("app", "example-downloader-plugin")) + nonPublicMarkers += "app.revanced.manager.plugin.downloader.PluginHostApi" } \ No newline at end of file diff --git a/downloader-plugin/api/downloader-plugin.api b/downloader-plugin/api/downloader-plugin.api index ae88af76..bc446e78 100644 --- a/downloader-plugin/api/downloader-plugin.api +++ b/downloader-plugin/api/downloader-plugin.api @@ -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 fun getTargetFile ()Ljava/io/File; - public abstract fun reportProgress (JLjava/lang/Long;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public abstract fun reportSize (JLkotlin/coroutines/Continuation;)Ljava/lang/Object; } 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 fun ()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 { @@ -48,17 +41,28 @@ public final class app/revanced/manager/plugin/downloader/DownloaderContext { 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 static final fun downloader (Lkotlin/jvm/functions/Function1;)Lapp/revanced/manager/plugin/downloader/Downloader; +public final class app/revanced/manager/plugin/downloader/DownloaderScope { + 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 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 synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V } diff --git a/downloader-plugin/build.gradle.kts b/downloader-plugin/build.gradle.kts index b3871e64..90c17add 100644 --- a/downloader-plugin/build.gradle.kts +++ b/downloader-plugin/build.gradle.kts @@ -1,7 +1,7 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) - id("kotlin-parcelize") + alias(libs.plugins.kotlin.parcelize) `maven-publish` } @@ -33,10 +33,6 @@ 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 47dd7b30..d853dfd1 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 @@ -1,14 +1,16 @@ package app.revanced.manager.plugin.downloader +import android.content.Context import android.content.Intent -import java.io.File import java.io.InputStream +import java.io.OutputStream -@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE) -@DslMarker -annotation class DownloaderDsl +@RequiresOptIn( + level = RequiresOptIn.Level.ERROR, + message = "This API is only intended for plugin hosts, don't use it in a plugin.", +) +annotation class PluginHostApi -@DownloaderDsl interface GetScope { suspend fun requestUserInteraction(): ActivityLaunchPermit } @@ -17,37 +19,66 @@ fun interface ActivityLaunchPermit { suspend fun launch(intent: Intent): Intent? } -@DownloaderDsl interface DownloadScope { suspend fun reportSize(size: Long) } -@DownloaderDsl -class DownloaderBuilder { - private var download: (suspend DownloadScope.(A) -> InputStream)? = null - private var get: (suspend GetScope.(String, String?) -> A?)? = null +typealias Size = Long +typealias DownloadResult = Pair + +class DownloaderScope 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?) { 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( - download = download ?: error("download was not declared"), - get = get ?: error("get was not declared") - ) + inputStream.use { + if (size != null) reportSize(size) + it.copyTo(outputStream) + } + } + } +} + +class DownloaderBuilder internal constructor(private val block: DownloaderScope.() -> Unit) { + @PluginHostApi + fun build(hostPackageName: String, context: Context) = + with(DownloaderScope(hostPackageName, context)) { + block() + + Downloader( + download = download ?: error("download was not declared"), + get = get ?: error("get was not declared") + ) + } } class Downloader internal constructor( - val get: suspend GetScope.(packageName: String, version: String?) -> A?, - val download: suspend DownloadScope.(app: A) -> InputStream + @property:PluginHostApi val get: suspend GetScope.(packageName: String, version: String?) -> A?, + @property:PluginHostApi val download: suspend DownloadScope.(app: A, outputStream: OutputStream) -> Unit ) -fun downloader(block: DownloaderBuilder.() -> Unit) = - DownloaderBuilder().apply(block).build() +fun downloader(block: DownloaderScope.() -> Unit) = DownloaderBuilder(block) sealed class UserInteractionException(message: String) : Exception(message) { class RequestDenied : UserInteractionException("Request was denied") diff --git a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/DownloaderContext.kt b/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/DownloaderContext.kt deleted file mode 100644 index a295bead..00000000 --- a/downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/DownloaderContext.kt +++ /dev/null @@ -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) \ No newline at end of file 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 index 334b446a..b0e9ead1 100644 --- 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 @@ -1,96 +1,8 @@ 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 +fun DownloaderScope.download(block: suspend DownloadScope.(A, OutputStream) -> Unit) { + download = block +} \ No newline at end of file diff --git a/example-downloader-plugin/build.gradle.kts b/example-downloader-plugin/build.gradle.kts index 130df4f5..da792645 100644 --- a/example-downloader-plugin/build.gradle.kts +++ b/example-downloader-plugin/build.gradle.kts @@ -1,7 +1,8 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) - id("kotlin-parcelize") + alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.compose.compiler) } android { @@ -16,7 +17,6 @@ android { targetSdk = 34 versionCode = 1 versionName = "1.0" - buildConfigField("String", "PLUGIN_PACKAGE_NAME", "\"$packageName\"") } buildTypes { @@ -39,11 +39,7 @@ android { kotlinOptions { jvmTarget = "17" } - composeOptions.kotlinCompilerExtensionVersion = "1.5.10" - buildFeatures { - compose = true - buildConfig = true - } + buildFeatures.compose = true } dependencies { 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/ExamplePlugin.kt similarity index 71% rename from example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/ExamplePlugins.kt rename to example-downloader-plugin/src/main/java/app/revanced/manager/plugin/downloader/example/ExamplePlugin.kt index 3f2b33b8..109a9241 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/ExamplePlugin.kt @@ -2,22 +2,19 @@ package app.revanced.manager.plugin.downloader.example +import android.app.Application 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 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. +// TODO: document and update API, change dispatcher, finish UI @Parcelize class InstalledApp( @@ -26,8 +23,15 @@ class InstalledApp( internal val apkPath: String ) : App(packageName, version) -fun installedAppDownloader(context: DownloaderContext) = downloader { - val pm = context.androidContext.packageManager +private val application by lazy { + // 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 { + val pm = application.packageManager get { packageName, version -> val packageInfo = try { @@ -38,7 +42,7 @@ fun installedAppDownloader(context: DownloaderContext) = downloader - Path(app.apkPath).also { - reportSize(it.fileSize()) - }.inputStream() - }*/ + with(Path(app.apkPath)) { inputStream() to fileSize() } + } download { app, outputStream -> val path = Path(app.apkPath) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 10030fba..3df65c95 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,18 +1,18 @@ [versions] +kotlin = "2.0.10" ktx = "1.13.1" -material3 = "1.3.0-beta04" +material3 = "1.3.0-beta05" ui-tooling = "1.6.8" -viewmodel-lifecycle = "2.8.3" +viewmodel-lifecycle = "2.8.4" splash-screen = "1.0.1" -compose-activity = "1.9.0" +compose-activity = "1.9.1" preferences-datastore = "1.1.1" -work-runtime = "2.9.0" +work-runtime = "2.9.1" compose-bom = "2024.06.00" accompanist = "0.34.0" placeholder = "1.1.2" reorderable = "1.5.2" -serialization = "1.6.3" -coroutines = "1.8.1" +serialization = "1.7.1" collection = "0.3.7" room-version = "2.6.1" revanced-patcher = "19.3.1" @@ -24,8 +24,7 @@ ktor = "2.3.9" markdown-renderer = "0.22.0" fading-edges = "1.0.4" android-gradle-plugin = "8.3.2" -kotlin-gradle-plugin = "1.9.22" -dev-tools-gradle-plugin = "1.9.22-1.0.17" +dev-tools-ksp-gradle-plugin = "2.0.10-1.0.24" about-libraries-gradle-plugin = "11.1.1" binary-compatibility-validator = "0.15.1" coil = "2.6.0" @@ -69,7 +68,6 @@ 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" } @@ -132,7 +130,10 @@ compose-icons-fontawesome = { group = "com.github.BenjaminHalko.compose-icons", [plugins] android-application = { id = "com.android.application", 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" } -devtools = { id = "com.google.devtools.ksp", version.ref = "dev-tools-gradle-plugin" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +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" } binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" } \ No newline at end of file