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.content.Context
import android.os.Parcelable
import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
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.util.PM
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.conflate
@ -19,7 +20,7 @@ import java.nio.file.StandardOpenOption
import java.util.concurrent.atomic.AtomicLong
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 dao = db.downloadedAppDao()
@ -32,12 +33,15 @@ class DownloadedAppRepository(app: Application, db: AppDatabase) {
suspend fun download(
plugin: LoadedDownloaderPlugin,
app: App,
data: Parcelable,
expectedPackageName: String,
expectedVersion: String?,
onDownload: suspend (downloadProgress: Pair<Double, Double?>) -> Unit,
): File {
this.get(app.packageName, app.version)?.let { downloaded ->
return getApkFileForApp(downloaded)
}
if (expectedVersion != null) this.get(expectedPackageName, expectedVersion)
?.let { downloaded ->
return getApkFileForApp(downloaded)
}
// Converted integers cannot contain / or .. unlike the package name or version, so they are safer to use here.
val relativePath = File(generateUid().toString())
@ -80,19 +84,23 @@ class DownloadedAppRepository(app: Application, db: AppDatabase) {
)
}
}
plugin.download(scope, app, stream)
plugin.download(scope, data, stream)
}
}
.conflate()
.flowOn(Dispatchers.IO)
.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(
DownloadedApp(
packageName = app.packageName,
version = app.version,
packageName = pkgInfo.packageName,
version = pkgInfo.versionName,
directory = relativePath,
)
)

View file

@ -2,15 +2,14 @@ package app.revanced.manager.domain.repository
import android.app.Application
import android.content.pm.PackageManager
import android.os.Parcelable
import android.util.Log
import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.plugins.TrustedDownloaderPlugin
import app.revanced.manager.domain.manager.PreferencesManager
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.Downloader
import app.revanced.manager.network.downloader.ParceledDownloaderData
import app.revanced.manager.plugin.downloader.DownloaderBuilder
import app.revanced.manager.plugin.downloader.PluginHostApi
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 =
(_pluginStates.value[app.pluginPackageName] as? DownloaderPluginState.Loaded)?.plugin
?: throw Exception("Downloader plugin with name ${app.pluginPackageName} is not available")
(_pluginStates.value[data.pluginPackageName] as? DownloaderPluginState.Loaded)?.plugin
?: 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 {
@ -159,7 +158,7 @@ class DownloaderPluginRepository(
fun Class<*>.getDownloaderBuilder() =
declaredMethods
.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")
}
}

View file

@ -1,6 +1,6 @@
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.GetScope
import java.io.OutputStream
@ -9,7 +9,7 @@ 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, outputStream: OutputStream) -> Unit,
val get: suspend GetScope.(packageName: String, version: String?) -> Pair<Parcelable, String?>?,
val download: suspend DownloadScope.(data: Parcelable, outputStream: OutputStream) -> Unit,
val classLoader: ClassLoader
)

View file

@ -3,43 +3,42 @@ package app.revanced.manager.network.downloader
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import app.revanced.manager.plugin.downloader.App
import kotlinx.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,
private val bundle: Bundle
) : Parcelable {
constructor(plugin: LoadedDownloaderPlugin, app: App) : this(
constructor(plugin: LoadedDownloaderPlugin, data: Parcelable) : this(
plugin.packageName,
createBundle(app)
createBundle(data)
)
fun unwrapWith(plugin: LoadedDownloaderPlugin): App {
fun unwrapWith(plugin: LoadedDownloaderPlugin): Parcelable {
bundle.classLoader = plugin.classLoader
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val className = bundle.getString(CLASS_NAME_KEY)!!
val clazz = plugin.classLoader.loadClass(className)
bundle.getParcelable(APP_KEY, clazz)!! as App
} else @Suppress("Deprecation") bundle.getParcelable(APP_KEY)!!
bundle.getParcelable(DATA_KEY, clazz)!! as Parcelable
} else @Suppress("Deprecation") bundle.getParcelable(DATA_KEY)!!
}
private companion object {
const val CLASS_NAME_KEY = "class"
const val APP_KEY = "app"
const val DATA_KEY = "data"
fun createBundle(app: App) = Bundle().apply {
putParcelable(APP_KEY, app)
fun createBundle(data: Parcelable) = Bundle().apply {
putParcelable(DATA_KEY, data)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) putString(
CLASS_NAME_KEY,
app::class.java.canonicalName
data::class.java.canonicalName
)
}
}

View file

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

View file

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

View file

@ -1,6 +1,5 @@
package app.revanced.manager.ui.viewmodel
import android.app.Activity
import android.app.Application
import android.content.BroadcastReceiver
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.Logger
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.service.InstallService
import app.revanced.manager.ui.destination.Destination
@ -64,6 +63,7 @@ import java.time.Duration
import java.util.UUID
@Stable
@OptIn(PluginHostApi::class)
class PatcherViewModel(
private val input: Destination.Patcher
) : ViewModel(), KoinComponent {
@ -81,9 +81,8 @@ class PatcherViewModel(
var isInstalling by mutableStateOf(false)
private set
private var currentInteractionRequest: CompletableDeferred<ActivityLaunchPermit?>? by mutableStateOf(
null
)
// TODO: rename these
private var currentInteractionRequest: CompletableDeferred<Boolean>? by mutableStateOf(null)
val activeInteractionRequest by derivedStateOf { currentInteractionRequest != null }
private var launchedActivity: CompletableDeferred<ActivityResult>? = null
private val launchActivityChannel = Channel<Intent>()
@ -130,13 +129,29 @@ class PatcherViewModel(
downloadProgress,
patchesProgress,
setInputFile = { inputFile = it },
handleUserInteractionRequest = {
handleStartActivityRequest = { intent ->
withContext(Dispatchers.Main) {
if (activeInteractionRequest) throw Exception("Another request is already pending.")
if (currentInteractionRequest != null) throw Exception("Another request is already pending.")
try {
val job = CompletableDeferred<ActivityLaunchPermit?>()
currentInteractionRequest = job
job.await()
// Wait for the dialog interaction.
val accepted = with(CompletableDeferred<Boolean>()) {
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 {
currentInteractionRequest = null
}
@ -232,10 +247,12 @@ class PatcherViewModel(
}
fun rejectInteraction() {
currentInteractionRequest?.complete(null)
currentInteractionRequest?.complete(false)
}
fun allowInteraction() {
currentInteractionRequest?.complete(true)
/*
currentInteractionRequest?.complete(ActivityLaunchPermit { intent ->
withContext(Dispatchers.Main) {
if (launchedActivity != null) throw Exception("An activity has already been launched.")
@ -257,7 +274,7 @@ class PatcherViewModel(
launchedActivity = null
}
}
})
})*/
}
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 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/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 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 getHostPackageName ()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 {
@ -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 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 {
@ -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 fun <init> ()V
}
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 getResultCode ()I
}
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
import android.app.Service
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.os.Parcelable
import java.io.InputStream
import java.io.OutputStream
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@RequiresOptIn(
level = RequiresOptIn.Level.ERROR,
@ -12,43 +19,38 @@ import java.io.OutputStream
annotation class PluginHostApi
interface GetScope {
suspend fun requestUserInteraction(): ActivityLaunchPermit
}
fun interface ActivityLaunchPermit {
suspend fun launch(intent: Intent): Intent?
}
interface DownloadScope {
suspend fun reportSize(size: Long)
suspend fun requestStartActivity(intent: Intent): Intent?
}
typealias Size = Long
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.
*/
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
internal var download: (suspend DownloadScope.(T, OutputStream) -> Unit)? = null
internal var get: (suspend GetScope.(String, String?) -> GetResult<T>?)? = null
/**
* The package name of the plugin.
*/
val pluginPackageName: String get() = context.packageName
fun get(block: suspend GetScope.(packageName: String, version: String?) -> A?) {
fun get(block: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?) {
get = block
}
/**
* Define the download function for this plugin.
*/
fun download(block: suspend (app: A) -> DownloadResult) {
fun download(block: suspend (data: T) -> DownloadResult) {
download = { app, outputStream ->
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?) {}
}
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<A : App> internal constructor(private val block: DownloaderScope<A>.() -> Unit) {
class DownloaderBuilder<T : Parcelable> internal constructor(private val block: DownloaderScope<T>.() -> Unit) {
@PluginHostApi
fun build(hostPackageName: String, context: Context) =
with(DownloaderScope<A>(hostPackageName, context)) {
with(DownloaderScope<T>(hostPackageName, context)) {
block()
Downloader(
@ -73,19 +97,20 @@ class DownloaderBuilder<A : App> internal constructor(private val block: Downloa
}
}
class Downloader<A : App> internal constructor(
@property:PluginHostApi val get: suspend GetScope.(packageName: String, version: String?) -> A?,
@property:PluginHostApi val download: suspend DownloadScope.(app: A, outputStream: OutputStream) -> Unit
class Downloader<T : Parcelable> internal constructor(
@property:PluginHostApi val get: suspend GetScope.(packageName: String, version: String?) -> GetResult<T>?,
@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) {
class RequestDenied : UserInteractionException("Request was denied")
class RequestDenied @PluginHostApi constructor() :
UserInteractionException("Request was denied")
sealed class Activity(message: String) : UserInteractionException(message) {
class Cancelled : Activity("Interaction was cancelled")
class NotCompleted(val resultCode: Int, val intent: Intent?) :
class Cancelled @PluginHostApi constructor() : Activity("Interaction was cancelled")
class NotCompleted @PluginHostApi constructor(val resultCode: Int, val intent: Intent?) :
Activity("Unexpected activity result code: $resultCode")
}
}

View file

@ -1,8 +1,29 @@
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
interface DownloadScope {
suspend fun reportSize(size: Long)
}
// 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
}
}
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
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>
</manifest>

View file

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