mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-04-30 05:54:26 +02:00
more api changes
This commit is contained in:
parent
45b1d18685
commit
e14497a1ce
@ -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")
|
||||
|
@ -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(
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
)
|
||||
}
|
@ -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()
|
||||
|
@ -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"
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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")
|
||||
|
@ -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)
|
@ -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
|
||||
}
|
@ -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 {
|
||||
|
@ -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)
|
@ -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" }
|
Loading…
x
Reference in New Issue
Block a user