I think the new API is done

This commit is contained in:
Ax333l 2024-08-30 20:21:11 +02:00
parent e14497a1ce
commit 38fe7bf9fd
No known key found for this signature in database
GPG Key ID: D2B4D85271127D23
14 changed files with 209 additions and 154 deletions

View File

@ -2,12 +2,13 @@ package app.revanced.manager.domain.repository
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.os.Parcelable
import app.revanced.manager.data.room.AppDatabase import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
import app.revanced.manager.plugin.downloader.App
import app.revanced.manager.plugin.downloader.DownloadScope import app.revanced.manager.plugin.downloader.DownloadScope
import app.revanced.manager.util.PM
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.conflate
@ -19,7 +20,7 @@ import java.nio.file.StandardOpenOption
import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicLong
import kotlin.io.path.outputStream import kotlin.io.path.outputStream
class DownloadedAppRepository(app: Application, db: AppDatabase) { class DownloadedAppRepository(app: Application, db: AppDatabase, private val pm: PM) {
private val dir = app.getDir("downloaded-apps", Context.MODE_PRIVATE) private val dir = app.getDir("downloaded-apps", Context.MODE_PRIVATE)
private val dao = db.downloadedAppDao() private val dao = db.downloadedAppDao()
@ -32,10 +33,13 @@ class DownloadedAppRepository(app: Application, db: AppDatabase) {
suspend fun download( suspend fun download(
plugin: LoadedDownloaderPlugin, plugin: LoadedDownloaderPlugin,
app: App, data: Parcelable,
expectedPackageName: String,
expectedVersion: String?,
onDownload: suspend (downloadProgress: Pair<Double, Double?>) -> Unit, onDownload: suspend (downloadProgress: Pair<Double, Double?>) -> Unit,
): File { ): File {
this.get(app.packageName, app.version)?.let { downloaded -> if (expectedVersion != null) this.get(expectedPackageName, expectedVersion)
?.let { downloaded ->
return getApkFileForApp(downloaded) return getApkFileForApp(downloaded)
} }
@ -80,19 +84,23 @@ class DownloadedAppRepository(app: Application, db: AppDatabase) {
) )
} }
} }
plugin.download(scope, app, stream) plugin.download(scope, data, stream)
} }
} }
.conflate() .conflate()
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.collect { (downloaded, size) -> onDownload(downloaded.megaBytes to size.megaBytes) } .collect { (downloaded, size) -> onDownload(downloaded.megaBytes to size.megaBytes) }
if (downloadedBytes.get() < 1) throw Exception("Downloader did not download any files") if (downloadedBytes.get() < 1) error("Downloader did not download anything.")
val pkgInfo =
pm.getPackageInfo(targetFile.toFile()) ?: error("Downloaded APK file is invalid")
if (pkgInfo.packageName != expectedPackageName) error("Downloaded APK has the wrong package name. Expected: $expectedPackageName, Actual: ${pkgInfo.packageName}")
if (expectedVersion != null && pkgInfo.versionName != expectedVersion) error("Downloaded APK has the wrong version. Expected: $expectedVersion, Actual: ${pkgInfo.versionName}")
dao.insert( dao.insert(
DownloadedApp( DownloadedApp(
packageName = app.packageName, packageName = pkgInfo.packageName,
version = app.version, version = pkgInfo.versionName,
directory = relativePath, directory = relativePath,
) )
) )

View File

@ -2,15 +2,14 @@ package app.revanced.manager.domain.repository
import android.app.Application import android.app.Application
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Parcelable
import android.util.Log import android.util.Log
import app.revanced.manager.data.room.AppDatabase import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.plugins.TrustedDownloaderPlugin import app.revanced.manager.data.room.plugins.TrustedDownloaderPlugin
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.network.downloader.DownloaderPluginState import app.revanced.manager.network.downloader.DownloaderPluginState
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
import app.revanced.manager.network.downloader.ParceledDownloaderApp import app.revanced.manager.network.downloader.ParceledDownloaderData
import app.revanced.manager.plugin.downloader.App
import app.revanced.manager.plugin.downloader.Downloader
import app.revanced.manager.plugin.downloader.DownloaderBuilder import app.revanced.manager.plugin.downloader.DownloaderBuilder
import app.revanced.manager.plugin.downloader.PluginHostApi import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
@ -67,12 +66,12 @@ class DownloaderPluginRepository(
} }
} }
fun unwrapParceledApp(app: ParceledDownloaderApp): Pair<LoadedDownloaderPlugin, App> { fun unwrapParceledData(data: ParceledDownloaderData): Pair<LoadedDownloaderPlugin, Parcelable> {
val plugin = val plugin =
(_pluginStates.value[app.pluginPackageName] as? DownloaderPluginState.Loaded)?.plugin (_pluginStates.value[data.pluginPackageName] as? DownloaderPluginState.Loaded)?.plugin
?: throw Exception("Downloader plugin with name ${app.pluginPackageName} is not available") ?: throw Exception("Downloader plugin with name ${data.pluginPackageName} is not available")
return plugin to app.unwrapWith(plugin) return plugin to data.unwrapWith(plugin)
} }
private suspend fun loadPlugin(packageName: String): DownloaderPluginState { private suspend fun loadPlugin(packageName: String): DownloaderPluginState {
@ -159,7 +158,7 @@ class DownloaderPluginRepository(
fun Class<*>.getDownloaderBuilder() = fun Class<*>.getDownloaderBuilder() =
declaredMethods declaredMethods
.firstOrNull { it.modifiers.isPublicStatic && it.returnType.isDownloaderBuilder && it.parameterTypes.isEmpty() } .firstOrNull { it.modifiers.isPublicStatic && it.returnType.isDownloaderBuilder && it.parameterTypes.isEmpty() }
?.let { it(null) as DownloaderBuilder<App> } ?.let { it(null) as DownloaderBuilder<Parcelable> }
?: throw Exception("Could not find a valid downloader implementation in class $canonicalName") ?: throw Exception("Could not find a valid downloader implementation in class $canonicalName")
} }
} }

View File

@ -1,6 +1,6 @@
package app.revanced.manager.network.downloader package app.revanced.manager.network.downloader
import app.revanced.manager.plugin.downloader.App import android.os.Parcelable
import app.revanced.manager.plugin.downloader.DownloadScope import app.revanced.manager.plugin.downloader.DownloadScope
import app.revanced.manager.plugin.downloader.GetScope import app.revanced.manager.plugin.downloader.GetScope
import java.io.OutputStream import java.io.OutputStream
@ -9,7 +9,7 @@ class LoadedDownloaderPlugin(
val packageName: String, val packageName: String,
val name: String, val name: String,
val version: String, val version: String,
val get: suspend GetScope.(packageName: String, version: String?) -> App?, val get: suspend GetScope.(packageName: String, version: String?) -> Pair<Parcelable, String?>?,
val download: suspend DownloadScope.(app: App, outputStream: OutputStream) -> Unit, val download: suspend DownloadScope.(data: Parcelable, outputStream: OutputStream) -> Unit,
val classLoader: ClassLoader val classLoader: ClassLoader
) )

View File

@ -3,43 +3,42 @@ package app.revanced.manager.network.downloader
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import app.revanced.manager.plugin.downloader.App
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
/** /**
* A parceled [App]. Instances of this class can be safely stored in a bundle without needing to set the [ClassLoader]. * A container for [Parcelable] data returned from downloaders. Instances of this class can be safely stored in a bundle without needing to set the [ClassLoader].
*/ */
class ParceledDownloaderApp private constructor( class ParceledDownloaderData private constructor(
val pluginPackageName: String, val pluginPackageName: String,
private val bundle: Bundle private val bundle: Bundle
) : Parcelable { ) : Parcelable {
constructor(plugin: LoadedDownloaderPlugin, app: App) : this( constructor(plugin: LoadedDownloaderPlugin, data: Parcelable) : this(
plugin.packageName, plugin.packageName,
createBundle(app) createBundle(data)
) )
fun unwrapWith(plugin: LoadedDownloaderPlugin): App { fun unwrapWith(plugin: LoadedDownloaderPlugin): Parcelable {
bundle.classLoader = plugin.classLoader bundle.classLoader = plugin.classLoader
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val className = bundle.getString(CLASS_NAME_KEY)!! val className = bundle.getString(CLASS_NAME_KEY)!!
val clazz = plugin.classLoader.loadClass(className) val clazz = plugin.classLoader.loadClass(className)
bundle.getParcelable(APP_KEY, clazz)!! as App bundle.getParcelable(DATA_KEY, clazz)!! as Parcelable
} else @Suppress("Deprecation") bundle.getParcelable(APP_KEY)!! } else @Suppress("Deprecation") bundle.getParcelable(DATA_KEY)!!
} }
private companion object { private companion object {
const val CLASS_NAME_KEY = "class" const val CLASS_NAME_KEY = "class"
const val APP_KEY = "app" const val DATA_KEY = "data"
fun createBundle(app: App) = Bundle().apply { fun createBundle(data: Parcelable) = Bundle().apply {
putParcelable(APP_KEY, app) putParcelable(DATA_KEY, data)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) putString( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) putString(
CLASS_NAME_KEY, CLASS_NAME_KEY,
app::class.java.canonicalName data::class.java.canonicalName
) )
} }
} }

View File

@ -1,5 +1,6 @@
package app.revanced.manager.patcher.worker package app.revanced.manager.patcher.worker
import android.app.Activity
import android.app.Notification import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
@ -9,9 +10,11 @@ import android.content.Intent
import android.content.pm.ServiceInfo import android.content.pm.ServiceInfo
import android.graphics.drawable.Icon import android.graphics.drawable.Icon
import android.os.Build import android.os.Build
import android.os.Parcelable
import android.os.PowerManager import android.os.PowerManager
import android.util.Log import android.util.Log
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.result.ActivityResult
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.work.ForegroundInfo import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
@ -30,10 +33,9 @@ import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
import app.revanced.manager.patcher.logger.Logger import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.runtime.CoroutineRuntime import app.revanced.manager.patcher.runtime.CoroutineRuntime
import app.revanced.manager.patcher.runtime.ProcessRuntime import app.revanced.manager.patcher.runtime.ProcessRuntime
import app.revanced.manager.plugin.downloader.ActivityLaunchPermit
import app.revanced.manager.plugin.downloader.GetScope import app.revanced.manager.plugin.downloader.GetScope
import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.plugin.downloader.UserInteractionException import app.revanced.manager.plugin.downloader.UserInteractionException
import app.revanced.manager.plugin.downloader.App as DownloaderApp
import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.model.State import app.revanced.manager.ui.model.State
import app.revanced.manager.util.Options import app.revanced.manager.util.Options
@ -49,6 +51,7 @@ import java.io.File
typealias ProgressEventHandler = (name: String?, state: State?, message: String?) -> Unit typealias ProgressEventHandler = (name: String?, state: State?, message: String?) -> Unit
@OptIn(PluginHostApi::class)
class PatcherWorker( class PatcherWorker(
context: Context, context: Context,
parameters: WorkerParameters parameters: WorkerParameters
@ -71,7 +74,7 @@ class PatcherWorker(
val logger: Logger, val logger: Logger,
val downloadProgress: MutableStateFlow<Pair<Double, Double?>?>, val downloadProgress: MutableStateFlow<Pair<Double, Double?>?>,
val patchesProgress: MutableStateFlow<Pair<Int, Int>>, val patchesProgress: MutableStateFlow<Pair<Int, Int>>,
val handleUserInteractionRequest: suspend () -> ActivityLaunchPermit?, val handleStartActivityRequest: suspend (Intent) -> ActivityResult,
val setInputFile: (File) -> Unit, val setInputFile: (File) -> Unit,
val onProgress: ProgressEventHandler val onProgress: ProgressEventHandler
) { ) {
@ -150,10 +153,12 @@ class PatcherWorker(
} }
} }
suspend fun download(plugin: LoadedDownloaderPlugin, app: DownloaderApp) = suspend fun download(plugin: LoadedDownloaderPlugin, data: Parcelable) =
downloadedAppRepository.download( downloadedAppRepository.download(
plugin, plugin,
app, data,
args.packageName,
args.input.version,
onDownload = args.downloadProgress::emit onDownload = args.downloadProgress::emit
).also { ).also {
args.setInputFile(it) args.setInputFile(it)
@ -162,16 +167,24 @@ class PatcherWorker(
val inputFile = when (val selectedApp = args.input) { val inputFile = when (val selectedApp = args.input) {
is SelectedApp.Download -> { is SelectedApp.Download -> {
val (plugin, app) = downloaderPluginRepository.unwrapParceledApp(selectedApp.app) val (plugin, data) = downloaderPluginRepository.unwrapParceledData(selectedApp.app)
download(plugin, app) download(plugin, data)
} }
is SelectedApp.Search -> { is SelectedApp.Search -> {
val getScope = object : GetScope { val getScope = object : GetScope {
override suspend fun requestUserInteraction() = override suspend fun requestStartActivity(intent: Intent): Intent? {
args.handleUserInteractionRequest() val result = args.handleStartActivityRequest(intent)
?: throw UserInteractionException.RequestDenied() return when (result.resultCode) {
Activity.RESULT_OK -> result.data
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
else -> throw UserInteractionException.Activity.NotCompleted(
result.resultCode,
result.data
)
}
}
} }
downloaderPluginRepository.loadedPluginsFlow.first() downloaderPluginRepository.loadedPluginsFlow.first()
@ -182,12 +195,12 @@ class PatcherWorker(
selectedApp.packageName, selectedApp.packageName,
selectedApp.version selectedApp.version
) )
?.takeIf { selectedApp.version == null || it.version == selectedApp.version } ?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
} catch (e: UserInteractionException.Activity.NotCompleted) { } catch (e: UserInteractionException.Activity.NotCompleted) {
throw e throw e
} catch (_: UserInteractionException) { } catch (_: UserInteractionException) {
null null
}?.let { app -> download(plugin, app) } }?.let { (data, _) -> download(plugin, data) }
} ?: throw Exception("App is not available.") } ?: throw Exception("App is not available.")
} }

View File

@ -1,7 +1,7 @@
package app.revanced.manager.ui.model package app.revanced.manager.ui.model
import android.os.Parcelable import android.os.Parcelable
import app.revanced.manager.network.downloader.ParceledDownloaderApp import app.revanced.manager.network.downloader.ParceledDownloaderData
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.io.File import java.io.File
@ -13,7 +13,7 @@ sealed interface SelectedApp : Parcelable {
data class Download( data class Download(
override val packageName: String, override val packageName: String,
override val version: String, override val version: String,
val app: ParceledDownloaderApp val app: ParceledDownloaderData
) : SelectedApp ) : SelectedApp
@Parcelize @Parcelize

View File

@ -1,6 +1,5 @@
package app.revanced.manager.ui.viewmodel package app.revanced.manager.ui.viewmodel
import android.app.Activity
import android.app.Application import android.app.Application
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
@ -32,7 +31,7 @@ import app.revanced.manager.domain.worker.WorkerRepository
import app.revanced.manager.patcher.logger.LogLevel import app.revanced.manager.patcher.logger.LogLevel
import app.revanced.manager.patcher.logger.Logger import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.worker.PatcherWorker import app.revanced.manager.patcher.worker.PatcherWorker
import app.revanced.manager.plugin.downloader.ActivityLaunchPermit import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.plugin.downloader.UserInteractionException import app.revanced.manager.plugin.downloader.UserInteractionException
import app.revanced.manager.service.InstallService import app.revanced.manager.service.InstallService
import app.revanced.manager.ui.destination.Destination import app.revanced.manager.ui.destination.Destination
@ -64,6 +63,7 @@ import java.time.Duration
import java.util.UUID import java.util.UUID
@Stable @Stable
@OptIn(PluginHostApi::class)
class PatcherViewModel( class PatcherViewModel(
private val input: Destination.Patcher private val input: Destination.Patcher
) : ViewModel(), KoinComponent { ) : ViewModel(), KoinComponent {
@ -81,9 +81,8 @@ class PatcherViewModel(
var isInstalling by mutableStateOf(false) var isInstalling by mutableStateOf(false)
private set private set
private var currentInteractionRequest: CompletableDeferred<ActivityLaunchPermit?>? by mutableStateOf( // TODO: rename these
null private var currentInteractionRequest: CompletableDeferred<Boolean>? by mutableStateOf(null)
)
val activeInteractionRequest by derivedStateOf { currentInteractionRequest != null } val activeInteractionRequest by derivedStateOf { currentInteractionRequest != null }
private var launchedActivity: CompletableDeferred<ActivityResult>? = null private var launchedActivity: CompletableDeferred<ActivityResult>? = null
private val launchActivityChannel = Channel<Intent>() private val launchActivityChannel = Channel<Intent>()
@ -130,13 +129,29 @@ class PatcherViewModel(
downloadProgress, downloadProgress,
patchesProgress, patchesProgress,
setInputFile = { inputFile = it }, setInputFile = { inputFile = it },
handleUserInteractionRequest = { handleStartActivityRequest = { intent ->
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (activeInteractionRequest) throw Exception("Another request is already pending.") if (currentInteractionRequest != null) throw Exception("Another request is already pending.")
try { try {
val job = CompletableDeferred<ActivityLaunchPermit?>() // Wait for the dialog interaction.
currentInteractionRequest = job val accepted = with(CompletableDeferred<Boolean>()) {
job.await() currentInteractionRequest = this
println(activeInteractionRequest)
await()
}
if (!accepted) throw UserInteractionException.RequestDenied()
// Launch the activity and wait for the result.
try {
with(CompletableDeferred<ActivityResult>()) {
launchedActivity = this
launchActivityChannel.send(intent)
await()
}
} finally {
launchedActivity = null
}
} finally { } finally {
currentInteractionRequest = null currentInteractionRequest = null
} }
@ -232,10 +247,12 @@ class PatcherViewModel(
} }
fun rejectInteraction() { fun rejectInteraction() {
currentInteractionRequest?.complete(null) currentInteractionRequest?.complete(false)
} }
fun allowInteraction() { fun allowInteraction() {
currentInteractionRequest?.complete(true)
/*
currentInteractionRequest?.complete(ActivityLaunchPermit { intent -> currentInteractionRequest?.complete(ActivityLaunchPermit { intent ->
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (launchedActivity != null) throw Exception("An activity has already been launched.") if (launchedActivity != null) throw Exception("An activity has already been launched.")
@ -257,7 +274,7 @@ class PatcherViewModel(
launchedActivity = null launchedActivity = null
} }
} }
}) })*/
} }
fun handleActivityResult(result: ActivityResult) { fun handleActivityResult(result: ActivityResult) {

View File

@ -1,26 +1,3 @@
public abstract interface class app/revanced/manager/plugin/downloader/ActivityLaunchPermit {
public abstract fun launch (Landroid/content/Intent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public class app/revanced/manager/plugin/downloader/App : android/os/Parcelable {
public static final field CREATOR Landroid/os/Parcelable$Creator;
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
public fun describeContents ()I
public fun equals (Ljava/lang/Object;)Z
public fun getPackageName ()Ljava/lang/String;
public fun getVersion ()Ljava/lang/String;
public fun hashCode ()I
public fun writeToParcel (Landroid/os/Parcel;I)V
}
public final class app/revanced/manager/plugin/downloader/App$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/App;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/App;
public synthetic fun newArray (I)[Ljava/lang/Object;
}
public final class app/revanced/manager/plugin/downloader/ConstantsKt { public final class app/revanced/manager/plugin/downloader/ConstantsKt {
public static final field PLUGIN_HOST_PERMISSION Ljava/lang/String; public static final field PLUGIN_HOST_PERMISSION Ljava/lang/String;
} }
@ -35,12 +12,6 @@ public final class app/revanced/manager/plugin/downloader/Downloader {
public final class app/revanced/manager/plugin/downloader/DownloaderBuilder { public final class app/revanced/manager/plugin/downloader/DownloaderBuilder {
} }
public final class app/revanced/manager/plugin/downloader/DownloaderContext {
public fun <init> (Landroid/content/Context;Ljava/lang/String;)V
public final fun getAndroidContext ()Landroid/content/Context;
public final fun getPluginHostPackageName ()Ljava/lang/String;
}
public final class app/revanced/manager/plugin/downloader/DownloaderKt { 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 static final fun downloader (Lkotlin/jvm/functions/Function1;)Lapp/revanced/manager/plugin/downloader/DownloaderBuilder;
} }
@ -50,6 +21,7 @@ public final class app/revanced/manager/plugin/downloader/DownloaderScope {
public final fun get (Lkotlin/jvm/functions/Function4;)V public final fun get (Lkotlin/jvm/functions/Function4;)V
public final fun getHostPackageName ()Ljava/lang/String; public final fun getHostPackageName ()Ljava/lang/String;
public final fun getPluginPackageName ()Ljava/lang/String; public final fun getPluginPackageName ()Ljava/lang/String;
public final fun withBoundService (Landroid/content/Intent;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
} }
public final class app/revanced/manager/plugin/downloader/ExtensionsKt { public final class app/revanced/manager/plugin/downloader/ExtensionsKt {
@ -57,7 +29,31 @@ public final class app/revanced/manager/plugin/downloader/ExtensionsKt {
} }
public abstract interface class app/revanced/manager/plugin/downloader/GetScope { public abstract interface class app/revanced/manager/plugin/downloader/GetScope {
public abstract fun requestUserInteraction (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun requestStartActivity (Landroid/content/Intent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public final class app/revanced/manager/plugin/downloader/Package : android/os/Parcelable {
public static final field CREATOR Landroid/os/Parcelable$Creator;
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/manager/plugin/downloader/Package;
public static synthetic fun copy$default (Lapp/revanced/manager/plugin/downloader/Package;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lapp/revanced/manager/plugin/downloader/Package;
public final fun describeContents ()I
public fun equals (Ljava/lang/Object;)Z
public final fun getName ()Ljava/lang/String;
public final fun getVersion ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public final fun writeToParcel (Landroid/os/Parcel;I)V
}
public final class app/revanced/manager/plugin/downloader/Package$Creator : android/os/Parcelable$Creator {
public fun <init> ()V
public final fun createFromParcel (Landroid/os/Parcel;)Lapp/revanced/manager/plugin/downloader/Package;
public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
public final fun newArray (I)[Lapp/revanced/manager/plugin/downloader/Package;
public synthetic fun newArray (I)[Ljava/lang/Object;
} }
public abstract interface annotation class app/revanced/manager/plugin/downloader/PluginHostApi : java/lang/annotation/Annotation { public abstract interface annotation class app/revanced/manager/plugin/downloader/PluginHostApi : java/lang/annotation/Annotation {
@ -72,16 +68,13 @@ public abstract class app/revanced/manager/plugin/downloader/UserInteractionExce
} }
public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$Cancelled : app/revanced/manager/plugin/downloader/UserInteractionException$Activity { public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$Cancelled : app/revanced/manager/plugin/downloader/UserInteractionException$Activity {
public fun <init> ()V
} }
public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$NotCompleted : app/revanced/manager/plugin/downloader/UserInteractionException$Activity { public final class app/revanced/manager/plugin/downloader/UserInteractionException$Activity$NotCompleted : app/revanced/manager/plugin/downloader/UserInteractionException$Activity {
public fun <init> (ILandroid/content/Intent;)V
public final fun getIntent ()Landroid/content/Intent; public final fun getIntent ()Landroid/content/Intent;
public final fun getResultCode ()I public final fun getResultCode ()I
} }
public final class app/revanced/manager/plugin/downloader/UserInteractionException$RequestDenied : app/revanced/manager/plugin/downloader/UserInteractionException { public final class app/revanced/manager/plugin/downloader/UserInteractionException$RequestDenied : app/revanced/manager/plugin/downloader/UserInteractionException {
public fun <init> ()V
} }

View File

@ -1,15 +0,0 @@
package app.revanced.manager.plugin.downloader
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import java.util.Objects
@Parcelize
open class App(open val packageName: String, open val version: String) : Parcelable {
override fun hashCode() = Objects.hash(packageName, version)
override fun equals(other: Any?): Boolean {
if (other !is App) return false
return other.packageName == packageName && other.version == version
}
}

View File

@ -1,9 +1,16 @@
package app.revanced.manager.plugin.downloader package app.revanced.manager.plugin.downloader
import android.app.Service
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.os.Parcelable
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@RequiresOptIn( @RequiresOptIn(
level = RequiresOptIn.Level.ERROR, level = RequiresOptIn.Level.ERROR,
@ -12,43 +19,38 @@ import java.io.OutputStream
annotation class PluginHostApi annotation class PluginHostApi
interface GetScope { interface GetScope {
suspend fun requestUserInteraction(): ActivityLaunchPermit suspend fun requestStartActivity(intent: Intent): Intent?
}
fun interface ActivityLaunchPermit {
suspend fun launch(intent: Intent): Intent?
}
interface DownloadScope {
suspend fun reportSize(size: Long)
} }
typealias Size = Long typealias Size = Long
typealias DownloadResult = Pair<InputStream, Size?> typealias DownloadResult = Pair<InputStream, Size?>
class DownloaderScope<A : App> internal constructor( typealias Version = String
typealias GetResult<T> = Pair<T, Version?>
class DownloaderScope<T : Parcelable> internal constructor(
/** /**
* The package name of ReVanced Manager. * The package name of ReVanced Manager.
*/ */
val hostPackageName: String, val hostPackageName: String,
internal val context: Context internal val context: Context
) { ) {
internal var download: (suspend DownloadScope.(A, OutputStream) -> Unit)? = null internal var download: (suspend DownloadScope.(T, OutputStream) -> Unit)? = null
internal var get: (suspend GetScope.(String, String?) -> A?)? = null internal var get: (suspend GetScope.(String, String?) -> GetResult<T>?)? = null
/** /**
* The package name of the plugin. * The package name of the plugin.
*/ */
val pluginPackageName: String get() = context.packageName val pluginPackageName: String get() = context.packageName
fun get(block: suspend GetScope.(packageName: String, version: String?) -> A?) { fun get(block: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?) {
get = block get = block
} }
/** /**
* Define the download function for this plugin. * Define the download function for this plugin.
*/ */
fun download(block: suspend (app: A) -> DownloadResult) { fun download(block: suspend (data: T) -> DownloadResult) {
download = { app, outputStream -> download = { app, outputStream ->
val (inputStream, size) = block(app) val (inputStream, size) = block(app)
@ -58,12 +60,34 @@ class DownloaderScope<A : App> internal constructor(
} }
} }
} }
suspend fun <R : Any?> withBoundService(intent: Intent, block: suspend (IBinder) -> R): R {
var onBind: ((IBinder) -> Unit)? = null
val serviceConn = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) =
onBind!!(service!!)
override fun onServiceDisconnected(name: ComponentName?) {}
} }
class DownloaderBuilder<A : App> internal constructor(private val block: DownloaderScope<A>.() -> Unit) { return try {
// TODO: add a timeout
block(suspendCoroutine { continuation ->
onBind = continuation::resume
context.bindService(intent, serviceConn, Context.BIND_AUTO_CREATE)
})
} finally {
onBind = null
// TODO: should we stop it?
context.unbindService(serviceConn)
}
}
}
class DownloaderBuilder<T : Parcelable> internal constructor(private val block: DownloaderScope<T>.() -> Unit) {
@PluginHostApi @PluginHostApi
fun build(hostPackageName: String, context: Context) = fun build(hostPackageName: String, context: Context) =
with(DownloaderScope<A>(hostPackageName, context)) { with(DownloaderScope<T>(hostPackageName, context)) {
block() block()
Downloader( Downloader(
@ -73,19 +97,20 @@ class DownloaderBuilder<A : App> internal constructor(private val block: Downloa
} }
} }
class Downloader<A : App> internal constructor( class Downloader<T : Parcelable> internal constructor(
@property:PluginHostApi val get: suspend GetScope.(packageName: String, version: String?) -> A?, @property:PluginHostApi val get: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?,
@property:PluginHostApi val download: suspend DownloadScope.(app: A, outputStream: OutputStream) -> Unit @property:PluginHostApi val download: suspend DownloadScope.(data: T, outputStream: OutputStream) -> Unit
) )
fun <A : App> downloader(block: DownloaderScope<A>.() -> Unit) = DownloaderBuilder(block) fun <T : Parcelable> downloader(block: DownloaderScope<T>.() -> Unit) = DownloaderBuilder(block)
sealed class UserInteractionException(message: String) : Exception(message) { sealed class UserInteractionException(message: String) : Exception(message) {
class RequestDenied : UserInteractionException("Request was denied") class RequestDenied @PluginHostApi constructor() :
UserInteractionException("Request was denied")
sealed class Activity(message: String) : UserInteractionException(message) { sealed class Activity(message: String) : UserInteractionException(message) {
class Cancelled : Activity("Interaction was cancelled") class Cancelled @PluginHostApi constructor() : Activity("Interaction was cancelled")
class NotCompleted(val resultCode: Int, val intent: Intent?) : class NotCompleted @PluginHostApi constructor(val resultCode: Int, val intent: Intent?) :
Activity("Unexpected activity result code: $resultCode") Activity("Unexpected activity result code: $resultCode")
} }
} }

View File

@ -1,8 +1,29 @@
package app.revanced.manager.plugin.downloader package app.revanced.manager.plugin.downloader
import android.app.Activity
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.os.Parcelable
import java.io.OutputStream import java.io.OutputStream
interface DownloadScope {
suspend fun reportSize(size: Long)
}
// OutputStream-based version of download // OutputStream-based version of download
fun <A : App> DownloaderScope<A>.download(block: suspend DownloadScope.(A, OutputStream) -> Unit) { fun <T : Parcelable> DownloaderScope<T>.download(block: suspend DownloadScope.(T, OutputStream) -> Unit) {
download = block download = block
} }
suspend inline fun <reified ACTIVITY : Activity> GetScope.requestStartActivity(packageName: String) =
requestStartActivity(
Intent().apply { setClassName(packageName, ACTIVITY::class.qualifiedName!!) }
)
suspend inline fun <reified SERVICE : Service, R : Any?> DownloaderScope<*>.withBoundService(
packageName: String,
noinline block: suspend (IBinder) -> R
) = withBoundService(
Intent().apply { setClassName(packageName, SERVICE::class.qualifiedName!!) }, block
)

View File

@ -0,0 +1,7 @@
package app.revanced.manager.plugin.downloader
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class Package(val name: String, val version: String) : Parcelable

View File

@ -17,7 +17,7 @@
<meta-data <meta-data
android:name="app.revanced.manager.plugin.downloader.class" android:name="app.revanced.manager.plugin.downloader.class"
android:value="app.revanced.manager.plugin.downloader.example.ExamplePluginsKt" /> android:value="app.revanced.manager.plugin.downloader.example.ExamplePluginKt" />
</application> </application>
</manifest> </manifest>

View File

@ -3,25 +3,21 @@
package app.revanced.manager.plugin.downloader.example package app.revanced.manager.plugin.downloader.example
import android.app.Application import android.app.Application
import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import app.revanced.manager.plugin.downloader.App import android.os.Parcelable
import app.revanced.manager.plugin.downloader.download import app.revanced.manager.plugin.downloader.download
import app.revanced.manager.plugin.downloader.downloader import app.revanced.manager.plugin.downloader.downloader
import app.revanced.manager.plugin.downloader.requestStartActivity
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.nio.file.Files import java.nio.file.Files
import kotlin.io.path.Path import kotlin.io.path.Path
import kotlin.io.path.fileSize import kotlin.io.path.fileSize
import kotlin.io.path.inputStream import kotlin.io.path.inputStream
// TODO: document and update API, change dispatcher, finish UI // TODO: document and update API (update requestUserInteraction, add bound service function), change dispatcher, finish UI
@Parcelize @Parcelize
class InstalledApp( class InstalledApp(val path: String) : Parcelable
override val packageName: String,
override val version: String,
internal val apkPath: String
) : App(packageName, version)
private val application by lazy { private val application by lazy {
// Don't do this in a real plugin. // Don't do this in a real plugin.
@ -39,27 +35,19 @@ val installedAppDownloader = downloader<InstalledApp> {
} catch (_: PackageManager.NameNotFoundException) { } catch (_: PackageManager.NameNotFoundException) {
return@get null return@get null
} }
if (version != null && packageInfo.versionName != version) return@get null
requestUserInteraction().launch(Intent().apply { requestStartActivity<InteractionActivity>(pluginPackageName)
setClassName(
pluginPackageName,
InteractionActivity::class.java.canonicalName!!
)
})
InstalledApp( InstalledApp(packageInfo.applicationInfo.sourceDir) to packageInfo.versionName
packageName,
packageInfo.versionName,
packageInfo.applicationInfo.sourceDir
).takeIf { version == null || it.version == version }
} }
download { app -> download { app ->
with(Path(app.apkPath)) { inputStream() to fileSize() } with(Path(app.path)) { inputStream() to fileSize() }
} }
download { app, outputStream -> download { app, outputStream ->
val path = Path(app.apkPath) val path = Path(app.path)
reportSize(path.fileSize()) reportSize(path.fileSize())
Files.copy(path, outputStream) Files.copy(path, outputStream)
} }