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

View File

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

View File

@ -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<App> }
?: throw Exception("Could not find a valid downloader implementation in class $canonicalName")
}
}

View File

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

View File

@ -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<SelectedAppInfoDestination>(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)
}
}
*/
}
)
}

View File

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

View File

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

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 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 <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 {
@ -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 <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
}

View File

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

View File

@ -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<A : App> {
private var download: (suspend DownloadScope.(A) -> InputStream)? = null
private var get: (suspend GetScope.(String, String?) -> A?)? = null
typealias Size = Long
typealias DownloadResult = Pair<InputStream, Size?>
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?) {
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<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"),
get = get ?: error("get was not declared")
)
}
}
class Downloader<A : App> 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 <A : App> downloader(block: DownloaderBuilder<A>.() -> Unit) =
DownloaderBuilder<A>().apply(block).build()
fun <A : App> downloader(block: DownloaderScope<A>.() -> Unit) = DownloaderBuilder(block)
sealed class UserInteractionException(message: String) : Exception(message) {
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
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) }
}
}
fun <A : App> DownloaderScope<A>.download(block: suspend DownloadScope.(A, OutputStream) -> Unit) {
download = block
}

View File

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

View File

@ -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<InstalledApp> {
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<InstalledApp> {
val pm = application.packageManager
get { packageName, version ->
val packageInfo = try {
@ -38,7 +42,7 @@ fun installedAppDownloader(context: DownloaderContext) = downloader<InstalledApp
requestUserInteraction().launch(Intent().apply {
setClassName(
PLUGIN_PACKAGE_NAME,
pluginPackageName,
InteractionActivity::class.java.canonicalName!!
)
})
@ -50,12 +54,9 @@ fun installedAppDownloader(context: DownloaderContext) = downloader<InstalledApp
).takeIf { version == null || it.version == version }
}
/*
download { app ->
Path(app.apkPath).also {
reportSize(it.fileSize())
}.inputStream()
}*/
with(Path(app.apkPath)) { inputStream() to fileSize() }
}
download { app, outputStream ->
val path = Path(app.apkPath)

View File

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