start implementing the new API

This commit is contained in:
Ax333l 2024-07-19 14:41:41 +02:00
parent 7ec3be460b
commit f9e8d30ff6
No known key found for this signature in database
GPG Key ID: D2B4D85271127D23
26 changed files with 381 additions and 727 deletions

View File

@ -83,7 +83,7 @@ android {
buildFeatures.compose = true buildFeatures.compose = true
buildFeatures.aidl = true buildFeatures.aidl = true
buildFeatures.buildConfig=true buildFeatures.buildConfig = true
android { android {
androidResources { androidResources {

View File

@ -9,13 +9,13 @@ import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import app.revanced.manager.ui.destination.Destination import app.revanced.manager.ui.destination.Destination
import app.revanced.manager.ui.destination.SettingsDestination import app.revanced.manager.ui.destination.SettingsDestination
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.screen.AppSelectorScreen import app.revanced.manager.ui.screen.AppSelectorScreen
import app.revanced.manager.ui.screen.DashboardScreen import app.revanced.manager.ui.screen.DashboardScreen
import app.revanced.manager.ui.screen.InstalledAppInfoScreen import app.revanced.manager.ui.screen.InstalledAppInfoScreen
import app.revanced.manager.ui.screen.PatcherScreen import app.revanced.manager.ui.screen.PatcherScreen
import app.revanced.manager.ui.screen.SelectedAppInfoScreen import app.revanced.manager.ui.screen.SelectedAppInfoScreen
import app.revanced.manager.ui.screen.SettingsScreen import app.revanced.manager.ui.screen.SettingsScreen
import app.revanced.manager.ui.screen.VersionSelectorScreen
import app.revanced.manager.ui.theme.ReVancedManagerTheme import app.revanced.manager.ui.theme.ReVancedManagerTheme
import app.revanced.manager.ui.theme.Theme import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.ui.viewmodel.MainViewModel import app.revanced.manager.ui.viewmodel.MainViewModel
@ -76,12 +76,13 @@ class MainActivity : ComponentActivity() {
is Destination.InstalledApplicationInfo -> InstalledAppInfoScreen( is Destination.InstalledApplicationInfo -> InstalledAppInfoScreen(
onPatchClick = { packageName, patchSelection -> onPatchClick = { packageName, patchSelection ->
/*
navController.navigate( navController.navigate(
Destination.VersionSelector( Destination.VersionSelector(
packageName, packageName,
patchSelection patchSelection
) )
) )*/
}, },
onBackClick = { navController.pop() }, onBackClick = { navController.pop() },
viewModel = getComposeViewModel { parametersOf(destination.installedApp) } viewModel = getComposeViewModel { parametersOf(destination.installedApp) }
@ -93,7 +94,14 @@ class MainActivity : ComponentActivity() {
) )
is Destination.AppSelector -> AppSelectorScreen( is Destination.AppSelector -> AppSelectorScreen(
onAppClick = { navController.navigate(Destination.VersionSelector(it)) }, // onAppClick = { navController.navigate(Destination.VersionSelector(it)) },
onAppClick = { packageName, version ->
navController.navigate(
Destination.SelectedApplicationInfo(
SelectedApp.Downloadable(packageName, version.orEmpty())
)
)
},
onStorageClick = { onStorageClick = {
navController.navigate( navController.navigate(
Destination.SelectedApplicationInfo( Destination.SelectedApplicationInfo(
@ -104,24 +112,6 @@ class MainActivity : ComponentActivity() {
onBackClick = { navController.pop() } onBackClick = { navController.pop() }
) )
is Destination.VersionSelector -> VersionSelectorScreen(
onBackClick = { navController.pop() },
onAppClick = { selectedApp ->
navController.navigate(
Destination.SelectedApplicationInfo(
selectedApp,
destination.patchSelection,
)
)
},
viewModel = getComposeViewModel {
parametersOf(
destination.packageName,
destination.patchSelection
)
}
)
is Destination.SelectedApplicationInfo -> SelectedAppInfoScreen( is Destination.SelectedApplicationInfo -> SelectedAppInfoScreen(
onPatchClick = { app, patches, options -> onPatchClick = { app, patches, options ->
navController.navigate( navController.navigate(

View File

@ -12,7 +12,6 @@ val viewModelModule = module {
viewModelOf(::SettingsViewModel) viewModelOf(::SettingsViewModel)
viewModelOf(::AdvancedSettingsViewModel) viewModelOf(::AdvancedSettingsViewModel)
viewModelOf(::AppSelectorViewModel) viewModelOf(::AppSelectorViewModel)
viewModelOf(::VersionSelectorViewModel)
viewModelOf(::PatcherViewModel) viewModelOf(::PatcherViewModel)
viewModelOf(::UpdateViewModel) viewModelOf(::UpdateViewModel)
viewModelOf(::ChangelogsViewModel) viewModelOf(::ChangelogsViewModel)

View File

@ -5,9 +5,6 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.Signature import android.content.pm.Signature
import android.util.Log import android.util.Log
import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import androidx.paging.PagingState
import app.revanced.manager.data.platform.Filesystem import app.revanced.manager.data.platform.Filesystem
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
@ -19,8 +16,6 @@ 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.plugin.downloader.Downloader import app.revanced.manager.plugin.downloader.Downloader
import app.revanced.manager.plugin.downloader.DownloaderContext import app.revanced.manager.plugin.downloader.DownloaderContext
import app.revanced.manager.plugin.downloader.DownloaderMarker
import app.revanced.manager.plugin.downloader.PaginatedDownloader
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import dalvik.system.PathClassLoader import dalvik.system.PathClassLoader
@ -115,51 +110,14 @@ class DownloaderPluginRepository(
.loadClass(className) .loadClass(className)
.getDownloaderImplementation(downloaderContext) .getDownloaderImplementation(downloaderContext)
class PluginComponents(
val download: suspend DownloadScope.(App) -> Unit,
val pagingConfig: PagingConfig,
val versionPager: (String, String?) -> PagingSource<*, out App>
)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val components = when (downloader) {
is PaginatedDownloader<*> -> PluginComponents(
downloader.download as suspend DownloadScope.(App) -> Unit,
downloader.pagingConfig,
downloader.versionPager
)
is Downloader<*> -> PluginComponents(
downloader.download as suspend DownloadScope.(App) -> Unit,
PagingConfig(pageSize = 1)
) { packageName: String, versionHint: String? ->
// Convert the lambda into a PagingSource.
object : PagingSource<Nothing, App>() {
override fun getRefreshKey(state: PagingState<Nothing, App>) = null
override suspend fun load(params: LoadParams<Nothing>) = try {
LoadResult.Page(
downloader.getVersions(packageName, versionHint),
nextKey = null,
prevKey = null
)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
}
DownloaderPluginState.Loaded( DownloaderPluginState.Loaded(
LoadedDownloaderPlugin( LoadedDownloaderPlugin(
packageInfo.packageName, packageInfo.packageName,
with(pm) { packageInfo.label() }, with(pm) { packageInfo.label() },
packageInfo.versionName, packageInfo.versionName,
components.versionPager, downloader.get,
components.download, downloader.download as suspend DownloadScope.(App) -> Unit,
components.pagingConfig,
classLoader classLoader
) )
) )
@ -204,19 +162,19 @@ class DownloaderPluginRepository(
val packageFlags = PackageManager.GET_META_DATA or PM.signaturesFlag val packageFlags = PackageManager.GET_META_DATA or PM.signaturesFlag
val Class<*>.isDownloaderMarker get() = DownloaderMarker::class.java.isAssignableFrom(this) val Class<*>.isDownloader get() = Downloader::class.java.isAssignableFrom(this)
const val PUBLIC_STATIC = Modifier.PUBLIC or Modifier.STATIC const val PUBLIC_STATIC = Modifier.PUBLIC or Modifier.STATIC
val Int.isPublicStatic get() = (this and PUBLIC_STATIC) == PUBLIC_STATIC val Int.isPublicStatic get() = (this and PUBLIC_STATIC) == PUBLIC_STATIC
fun Class<*>.getDownloaderImplementation(context: DownloaderContext) = fun Class<*>.getDownloaderImplementation(context: DownloaderContext) =
declaredMethods declaredMethods
.filter { it.modifiers.isPublicStatic && it.returnType.isDownloaderMarker } .filter { it.modifiers.isPublicStatic && it.returnType.isDownloader }
.firstNotNullOfOrNull callMethod@{ .firstNotNullOfOrNull callMethod@{
if (it.parameterTypes contentEquals arrayOf(DownloaderContext::class.java)) return@callMethod it( if (it.parameterTypes contentEquals arrayOf(DownloaderContext::class.java)) return@callMethod it(
null, null,
context context
) as DownloaderMarker ) as Downloader<*>
if (it.parameterTypes.isEmpty()) return@callMethod it(null) as DownloaderMarker if (it.parameterTypes.isEmpty()) return@callMethod it(null) as Downloader<*>
return@callMethod null return@callMethod null
} }

View File

@ -1,16 +1,14 @@
package app.revanced.manager.network.downloader package app.revanced.manager.network.downloader
import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import app.revanced.manager.plugin.downloader.App 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.plugin.downloader.GetScope
class LoadedDownloaderPlugin( class LoadedDownloaderPlugin(
val packageName: String, val packageName: String,
val name: String, val name: String,
val version: String, val version: String,
val createVersionPagingSource: (packageName: String, versionHint: String?) -> PagingSource<*, out App>, val get: suspend GetScope.(packageName: String, version: String?) -> App?,
val download: suspend DownloadScope.(app: App) -> Unit, val download: suspend DownloadScope.(app: App) -> Unit,
val pagingConfig: PagingConfig,
val classLoader: ClassLoader val classLoader: ClassLoader
) )

View File

@ -26,9 +26,14 @@ import app.revanced.manager.domain.repository.DownloaderPluginRepository
import app.revanced.manager.domain.repository.InstalledAppRepository import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.worker.Worker import app.revanced.manager.domain.worker.Worker
import app.revanced.manager.domain.worker.WorkerRepository import app.revanced.manager.domain.worker.WorkerRepository
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.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
@ -36,6 +41,7 @@ import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
@ -57,7 +63,7 @@ class PatcherWorker(
private val installedAppRepository: InstalledAppRepository by inject() private val installedAppRepository: InstalledAppRepository by inject()
private val rootInstaller: RootInstaller by inject() private val rootInstaller: RootInstaller by inject()
data class Args( class Args(
val input: SelectedApp, val input: SelectedApp,
val output: String, val output: String,
val selectedPatches: PatchSelection, val selectedPatches: PatchSelection,
@ -65,6 +71,7 @@ class PatcherWorker(
val logger: Logger, val logger: Logger,
val downloadProgress: MutableStateFlow<Pair<Float, Float?>?>, val downloadProgress: MutableStateFlow<Pair<Float, Float?>?>,
val patchesProgress: MutableStateFlow<Pair<Int, Int>>, val patchesProgress: MutableStateFlow<Pair<Int, Int>>,
val handleUserInteractionRequest: suspend () -> ActivityLaunchPermit?,
val setInputFile: (File) -> Unit, val setInputFile: (File) -> Unit,
val onProgress: ProgressEventHandler val onProgress: ProgressEventHandler
) { ) {
@ -143,10 +150,7 @@ class PatcherWorker(
} }
} }
val inputFile = when (val selectedApp = args.input) { suspend fun download(plugin: LoadedDownloaderPlugin, app: DownloaderApp) =
is SelectedApp.Download -> {
val (plugin, app) = downloaderPluginRepository.unwrapParceledApp(selectedApp.app)
downloadedAppRepository.download( downloadedAppRepository.download(
plugin, plugin,
app, app,
@ -155,6 +159,34 @@ class PatcherWorker(
args.setInputFile(it) args.setInputFile(it)
updateProgress(state = State.COMPLETED) // Download APK updateProgress(state = State.COMPLETED) // Download APK
} }
val inputFile = when (val selectedApp = args.input) {
is SelectedApp.Download -> {
val (plugin, app) = downloaderPluginRepository.unwrapParceledApp(selectedApp.app)
download(plugin, app)
}
is SelectedApp.Downloadable -> {
val getScope = object : GetScope {
override suspend fun requestUserInteraction() =
args.handleUserInteractionRequest()
?: throw UserInteractionException.RequestDenied()
}
downloaderPluginRepository.loadedPluginsFlow.first()
.firstNotNullOfOrNull { plugin ->
try {
plugin.get(
getScope,
selectedApp.packageName,
selectedApp.suggestedVersion
)
?.takeIf { selectedApp.suggestedVersion == null || it.version == selectedApp.suggestedVersion }
} catch (_: UserInteractionException) {
null
}?.let { app -> download(plugin, app) }
} ?: throw Exception("App is not available.")
} }
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) } is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
@ -188,7 +220,10 @@ class PatcherWorker(
Log.i(tag, "Patching succeeded".logFmt()) Log.i(tag, "Patching succeeded".logFmt())
Result.success() Result.success()
} catch (e: ProcessRuntime.RemoteFailureException) { } catch (e: ProcessRuntime.RemoteFailureException) {
Log.e(tag, "An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt()) Log.e(
tag,
"An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt()
)
updateProgress(state = State.FAILED, message = e.originalStackTrace) updateProgress(state = State.FAILED, message = e.originalStackTrace)
Result.failure() Result.failure()
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -22,9 +22,6 @@ sealed interface Destination : Parcelable {
@Parcelize @Parcelize
data class Settings(val startDestination: SettingsDestination = SettingsDestination.Settings) : Destination data class Settings(val startDestination: SettingsDestination = SettingsDestination.Settings) : Destination
@Parcelize
data class VersionSelector(val packageName: String, val patchSelection: PatchSelection? = null) : Destination
@Parcelize @Parcelize
data class SelectedApplicationInfo(val selectedApp: SelectedApp, val patchSelection: PatchSelection? = null) : Destination data class SelectedApplicationInfo(val selectedApp: SelectedApp, val patchSelection: PatchSelection? = null) : Destination

View File

@ -13,7 +13,4 @@ sealed interface SelectedAppInfoDestination : Parcelable {
@Parcelize @Parcelize
data class PatchesSelector(val app: SelectedApp, val currentSelection: PatchSelection?, val options: @RawValue Options) : SelectedAppInfoDestination data class PatchesSelector(val app: SelectedApp, val currentSelection: PatchSelection?, val options: @RawValue Options) : SelectedAppInfoDestination
@Parcelize
data object VersionSelector: SelectedAppInfoDestination
} }

View File

@ -2,19 +2,38 @@ 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.ParceledDownloaderApp
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.io.File import java.io.File
sealed class SelectedApp : Parcelable { sealed interface SelectedApp : Parcelable {
abstract val packageName: String val packageName: String
abstract val version: String val version: String // TODO: make this nullable
@Parcelize @Parcelize
data class Download(override val packageName: String, override val version: String, val app: ParceledDownloaderApp) : SelectedApp() data class Download(
override val packageName: String,
override val version: String,
val app: ParceledDownloaderApp
) : SelectedApp
@Parcelize @Parcelize
data class Local(override val packageName: String, override val version: String, val file: File, val temporary: Boolean) : SelectedApp() data class Downloadable(override val packageName: String, val suggestedVersion: String?) : SelectedApp {
@IgnoredOnParcel
override val version = suggestedVersion.orEmpty()
}
@Parcelize @Parcelize
data class Installed(override val packageName: String, override val version: String) : SelectedApp() data class Local(
override val packageName: String,
override val version: String,
val file: File,
val temporary: Boolean
) : SelectedApp
@Parcelize
data class Installed(
override val packageName: String,
override val version: String
) : SelectedApp
} }

View File

@ -38,7 +38,7 @@ import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AppSelectorScreen( fun AppSelectorScreen(
onAppClick: (packageName: String) -> Unit, onAppClick: (packageName: String, version: String?) -> Unit,
onStorageClick: (SelectedApp.Local) -> Unit, onStorageClick: (SelectedApp.Local) -> Unit,
onBackClick: () -> Unit, onBackClick: () -> Unit,
vm: AppSelectorViewModel = koinViewModel() vm: AppSelectorViewModel = koinViewModel()
@ -90,7 +90,12 @@ fun AppSelectorScreen(
key = { it.packageName } key = { it.packageName }
) { app -> ) { app ->
ListItem( ListItem(
modifier = Modifier.clickable { onAppClick(app.packageName) }, modifier = Modifier.clickable {
onAppClick(
app.packageName,
suggestedVersions[app.packageName]
)
},
leadingContent = { leadingContent = {
AppIcon( AppIcon(
app.packageInfo, app.packageInfo,
@ -183,7 +188,12 @@ fun AppSelectorScreen(
key = { it.packageName } key = { it.packageName }
) { app -> ) { app ->
ListItem( ListItem(
modifier = Modifier.clickable { onAppClick(app.packageName) }, modifier = Modifier.clickable {
onAppClick(
app.packageName,
suggestedVersions[app.packageName]
)
},
leadingContent = { AppIcon(app.packageInfo, null, Modifier.size(36.dp)) }, leadingContent = { AppIcon(app.packageInfo, null, Modifier.size(36.dp)) },
headlineContent = { headlineContent = {
AppLabel( AppLabel(

View File

@ -17,6 +17,7 @@ import androidx.compose.material.icons.automirrored.outlined.OpenInNew
import androidx.compose.material.icons.outlined.FileDownload import androidx.compose.material.icons.outlined.FileDownload
import androidx.compose.material.icons.outlined.PostAdd import androidx.compose.material.icons.outlined.PostAdd
import androidx.compose.material.icons.outlined.Save import androidx.compose.material.icons.outlined.Save
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.BottomAppBar import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.ExtendedFloatingActionButton
@ -24,7 +25,9 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
@ -46,6 +49,7 @@ import app.revanced.manager.ui.model.State
import app.revanced.manager.ui.model.StepCategory import app.revanced.manager.ui.model.StepCategory
import app.revanced.manager.ui.viewmodel.PatcherViewModel import app.revanced.manager.ui.viewmodel.PatcherViewModel
import app.revanced.manager.util.APK_MIMETYPE import app.revanced.manager.util.APK_MIMETYPE
import app.revanced.manager.util.IntentContract
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -91,6 +95,36 @@ fun PatcherScreen(
onConfirm = vm::install onConfirm = vm::install
) )
val activityLauncher = rememberLauncherForActivityResult(contract = IntentContract) {
vm.handleActivityResult(it)
}
SideEffect {
vm.launchActivity = activityLauncher::launch
}
if (vm.activeInteractionRequest)
AlertDialog(
onDismissRequest = vm::rejectInteraction,
confirmButton = {
TextButton(
onClick = vm::allowInteraction
) {
Text(stringResource(R.string.continue_))
}
},
dismissButton = {
TextButton(
onClick = vm::rejectInteraction
) {
Text(stringResource(R.string.cancel))
}
},
title = { Text("User interaction required.") },
text = {
Text("User interaction is required to proceed.")
}
)
AppScaffold( AppScaffold(
topBar = { topBar = {
AppTopBar( AppTopBar(

View File

@ -108,7 +108,7 @@ fun SelectedAppInfoScreen(
) )
}, },
onVersionSelectorClick = { onVersionSelectorClick = {
navController.navigate(SelectedAppInfoDestination.VersionSelector) // navController.navigate(SelectedAppInfoDestination.VersionSelector)
}, },
onBackClick = onBackClick, onBackClick = onBackClick,
availablePatchCount = availablePatchCount, availablePatchCount = availablePatchCount,
@ -118,15 +118,6 @@ fun SelectedAppInfoScreen(
packageInfo = vm.selectedAppInfo, packageInfo = vm.selectedAppInfo,
) )
is SelectedAppInfoDestination.VersionSelector -> VersionSelectorScreen(
onBackClick = navController::pop,
onAppClick = {
vm.selectedApp = it
navController.pop()
},
viewModel = koinViewModel { parametersOf(packageName) }
)
is SelectedAppInfoDestination.PatchesSelector -> PatchesSelectorScreen( is SelectedAppInfoDestination.PatchesSelector -> PatchesSelectorScreen(
onSave = { patches, options -> onSave = { patches, options ->
vm.updateConfiguration(patches, options, bundles) vm.updateConfiguration(patches, options, bundles)

View File

@ -1,337 +0,0 @@
package app.revanced.manager.ui.screen
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Download
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemKey
import app.revanced.manager.R
import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.GroupHeader
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.LoadingIndicator
import app.revanced.manager.ui.component.NonSuggestedVersionDialog
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.viewmodel.VersionSelectorViewModel
import app.revanced.manager.util.isScrollingUp
import app.revanced.manager.util.simpleMessage
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VersionSelectorScreen(
onBackClick: () -> Unit,
onAppClick: (SelectedApp) -> Unit,
viewModel: VersionSelectorViewModel
) {
val supportedVersions by viewModel.supportedVersions.collectAsStateWithLifecycle(emptyMap())
val downloadedVersions by viewModel.downloadedVersions.collectAsStateWithLifecycle(emptyList())
val downloadableVersions = viewModel.downloadableApps?.collectAsLazyPagingItems()
val sortedDownloadedVersions by remember {
derivedStateOf {
downloadedVersions
.distinctBy { it.version }
.sortedWith(
compareByDescending<SelectedApp> { supportedVersions[it.version] }.thenByDescending { it.version }
)
}
}
if (viewModel.showNonSuggestedVersionDialog)
NonSuggestedVersionDialog(
suggestedVersion = viewModel.requiredVersion.orEmpty(),
onDismiss = viewModel::dismissNonSuggestedVersionDialog
)
var showDownloaderSelectionDialog by rememberSaveable {
mutableStateOf(false)
}
if (showDownloaderSelectionDialog) {
val plugins by viewModel.downloadersFlow.collectAsStateWithLifecycle(emptyList())
val hasInstalledPlugins by viewModel.hasInstalledPlugins.collectAsStateWithLifecycle(false)
DownloaderSelectionDialog(
plugins = plugins,
hasInstalledPlugins = hasInstalledPlugins,
onConfirm = {
viewModel.selectDownloaderPlugin(it)
showDownloaderSelectionDialog = false
},
onDismiss = { showDownloaderSelectionDialog = false }
)
}
val lazyListState = rememberLazyListState()
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.select_version),
actions = {
IconButton(onClick = { showDownloaderSelectionDialog = true }) {
Icon(Icons.Filled.Download, stringResource(R.string.downloader_select))
}
},
onBackClick = onBackClick,
)
},
floatingActionButton = {
ExtendedFloatingActionButton(
text = { Text(stringResource(R.string.select_version)) },
icon = { Icon(Icons.Default.Check, null) },
expanded = lazyListState.isScrollingUp,
onClick = { viewModel.selectedVersion?.let(onAppClick) }
)
}
) { paddingValues ->
LazyColumnWithScrollbar(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
state = lazyListState
) {
viewModel.installedApp?.let { (packageInfo, installedApp) ->
SelectedApp.Installed(
packageName = viewModel.packageName,
version = packageInfo.versionName
).let {
item {
SelectedAppItem(
selectedApp = it,
selected = viewModel.selectedVersion == it,
onClick = { viewModel.select(it) },
patchCount = supportedVersions[it.version],
enabled =
!(installedApp?.installType == InstallType.ROOT && !viewModel.rootInstaller.hasRootAccess()),
alreadyPatched = installedApp != null && installedApp.installType != InstallType.ROOT
)
}
}
}
if (sortedDownloadedVersions.isNotEmpty()) item {
Row(Modifier.fillMaxWidth()) {
GroupHeader(stringResource(R.string.downloaded_versions))
}
}
items(
items = sortedDownloadedVersions,
key = { it.version }
) {
SelectedAppItem(
selectedApp = it,
selected = viewModel.selectedVersion == it,
onClick = { viewModel.select(it) },
patchCount = supportedVersions[it.version]
)
}
item {
Row(Modifier.fillMaxWidth()) {
GroupHeader(stringResource(R.string.downloadable_versions))
}
}
if (downloadableVersions == null) {
item {
Text(stringResource(R.string.downloader_not_selected))
}
} else {
(downloadableVersions.loadState.prepend as? LoadState.Error)?.let { errorState ->
item {
errorState.Render()
}
}
items(
count = downloadableVersions.itemCount,
key = downloadableVersions.itemKey { it.version }
) {
val item = downloadableVersions[it]!!
SelectedAppItem(
selectedApp = item,
selected = viewModel.selectedVersion == item,
onClick = { viewModel.select(item) },
patchCount = supportedVersions[item.version]
)
}
val loadStates = arrayOf(
downloadableVersions.loadState.append,
downloadableVersions.loadState.refresh
)
if (loadStates.any { it is LoadState.Loading }) {
item {
LoadingIndicator()
}
} else if (downloadableVersions.itemCount == 0) {
item { Text(stringResource(R.string.downloader_no_versions)) }
}
loadStates.firstNotNullOfOrNull { it as? LoadState.Error }?.let { errorState ->
item {
errorState.Render()
}
}
}
}
}
}
@Composable
fun SelectedAppItem(
selectedApp: SelectedApp,
selected: Boolean,
onClick: () -> Unit,
patchCount: Int?,
enabled: Boolean = true,
alreadyPatched: Boolean = false,
) {
ListItem(
leadingContent = { RadioButton(selected, null) },
headlineContent = { Text(selectedApp.version) },
supportingContent = when (selectedApp) {
is SelectedApp.Installed ->
if (alreadyPatched) {
{ Text(stringResource(R.string.already_patched)) }
} else {
{ Text(stringResource(R.string.installed)) }
}
is SelectedApp.Local -> {
{ Text(stringResource(R.string.already_downloaded)) }
}
else -> null
},
trailingContent = patchCount?.let {
{
Text(pluralStringResource(R.plurals.patch_count, it, it))
}
},
modifier = Modifier
.clickable(enabled = !alreadyPatched && enabled, onClick = onClick)
.run {
if (!enabled || alreadyPatched) alpha(0.5f)
else this
}
)
}
@Composable
private fun LoadState.Error.Render() {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
val message =
remember(error) { error.simpleMessage().orEmpty() }
Text(stringResource(R.string.error_occurred))
Text(
text = message,
modifier = Modifier.padding(horizontal = 15.dp)
)
Text(error.stackTraceToString())
}
}
@Composable
private fun DownloaderSelectionDialog(
plugins: List<LoadedDownloaderPlugin>,
hasInstalledPlugins: Boolean,
onConfirm: (LoadedDownloaderPlugin) -> Unit,
onDismiss: () -> Unit
) {
var selectedPackageName: String? by rememberSaveable {
mutableStateOf(null)
}
AlertDialog(
onDismissRequest = onDismiss,
confirmButton = {
TextButton(
enabled = selectedPackageName != null,
onClick = { onConfirm(plugins.single { it.packageName == selectedPackageName }) }
) {
Text(stringResource(R.string.select))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.cancel))
}
},
title = {
Text(stringResource(R.string.downloader_select))
},
icon = {
Icon(Icons.Filled.Download, null)
},
// TODO: fix dialog header centering issue
// textHorizontalPadding = PaddingValues(horizontal = if (plugins.isNotEmpty()) 0.dp else 24.dp),
text = {
LazyColumn {
items(plugins, key = { it.packageName }) {
ListItem(
modifier = Modifier.clickable { selectedPackageName = it.packageName },
headlineContent = { Text(it.name) },
leadingContent = {
RadioButton(
selected = selectedPackageName == it.packageName,
onClick = { selectedPackageName = it.packageName }
)
}
)
}
if (plugins.isEmpty()) {
item {
val resource =
if (hasInstalledPlugins) R.string.downloader_no_plugins_available else R.string.downloader_no_plugins_installed
Text(stringResource(resource))
}
}
}
}
)
}

View File

@ -1,5 +1,6 @@
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
@ -9,6 +10,7 @@ import android.content.pm.PackageInstaller
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@ -29,17 +31,21 @@ 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.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
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.ui.model.Step import app.revanced.manager.ui.model.Step
import app.revanced.manager.ui.model.StepCategory import app.revanced.manager.ui.model.StepCategory
import app.revanced.manager.util.IntentContract
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
import app.revanced.manager.util.simpleMessage import app.revanced.manager.util.simpleMessage
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import app.revanced.manager.util.uiSafe import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -48,7 +54,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.time.withTimeout import kotlinx.coroutines.time.withTimeout
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import java.io.File import java.io.File
@ -74,6 +79,13 @@ class PatcherViewModel(
var isInstalling by mutableStateOf(false) var isInstalling by mutableStateOf(false)
private set private set
private var currentInteractionRequest: CompletableDeferred<ActivityLaunchPermit?>? by mutableStateOf(
null
)
val activeInteractionRequest by derivedStateOf { currentInteractionRequest != null }
private var launchedActivity: CompletableDeferred<IntentContract.Result>? = null
var launchActivity: (Intent) -> Unit = {}
private val tempDir = fs.tempDir.resolve("installer").also { private val tempDir = fs.tempDir.resolve("installer").also {
it.deleteRecursively() it.deleteRecursively()
it.mkdirs() it.mkdirs()
@ -115,6 +127,18 @@ class PatcherViewModel(
downloadProgress, downloadProgress,
patchesProgress, patchesProgress,
setInputFile = { inputFile = it }, setInputFile = { inputFile = it },
handleUserInteractionRequest = {
withContext(Dispatchers.Main) {
if (activeInteractionRequest) throw Exception("Another request is already pending.")
try {
val job = CompletableDeferred<ActivityLaunchPermit?>()
currentInteractionRequest = job
job.await()
} finally {
currentInteractionRequest = null
}
}
},
onProgress = { name, state, message -> onProgress = { name, state, message ->
viewModelScope.launch { viewModelScope.launch {
steps[currentStepIndex] = steps[currentStepIndex].run { steps[currentStepIndex] = steps[currentStepIndex].run {
@ -214,6 +238,35 @@ class PatcherViewModel(
tempDir.deleteRecursively() tempDir.deleteRecursively()
} }
fun rejectInteraction() {
currentInteractionRequest?.complete(null)
currentInteractionRequest = null
}
fun allowInteraction() {
currentInteractionRequest?.complete(ActivityLaunchPermit { intent ->
withContext(Dispatchers.Main) {
if (launchedActivity != null) throw Exception("An activity has already been launched.")
try {
val job = CompletableDeferred<IntentContract.Result>()
launchActivity(intent)
launchedActivity = job
val result = job.await()
if (result.code != Activity.RESULT_OK) throw UserInteractionException.ActivityCancelled()
result.intent
} finally {
launchedActivity = null
}
}
})
currentInteractionRequest = null
}
fun handleActivityResult(result: IntentContract.Result) {
launchedActivity?.complete(result)
}
fun export(uri: Uri?) = viewModelScope.launch { fun export(uri: Uri?) = viewModelScope.launch {
uri?.let { uri?.let {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@ -306,7 +359,8 @@ class PatcherViewModel(
selectedApp: SelectedApp, selectedApp: SelectedApp,
downloadProgress: StateFlow<Pair<Float, Float?>?>? = null downloadProgress: StateFlow<Pair<Float, Float?>?>? = null
): List<Step> { ): List<Step> {
val needsDownload = selectedApp is SelectedApp.Download val needsDownload =
selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Downloadable
return listOfNotNull( return listOfNotNull(
Step( Step(

View File

@ -104,9 +104,9 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
private fun invalidateSelectedAppInfo() = viewModelScope.launch { private fun invalidateSelectedAppInfo() = viewModelScope.launch {
val info = when (val app = selectedApp) { val info = when (val app = selectedApp) {
is SelectedApp.Download -> null
is SelectedApp.Local -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.file) } is SelectedApp.Local -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.file) }
is SelectedApp.Installed -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.packageName) } is SelectedApp.Installed -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.packageName) }
else -> null
} }
selectedAppInfo = info selectedAppInfo = info

View File

@ -1,142 +0,0 @@
package app.revanced.manager.ui.viewmodel
import android.content.pm.PackageInfo
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.cachedIn
import androidx.paging.map
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.domain.repository.DownloadedAppRepository
import app.revanced.manager.domain.repository.DownloaderPluginRepository
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
import app.revanced.manager.network.downloader.ParceledDownloaderApp
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.util.PM
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class VersionSelectorViewModel(
val packageName: String
) : ViewModel(), KoinComponent {
private val downloadedAppRepository: DownloadedAppRepository by inject()
private val installedAppRepository: InstalledAppRepository by inject()
private val patchBundleRepository: PatchBundleRepository by inject()
private val downloaderPluginRepository: DownloaderPluginRepository by inject()
private val pm: PM by inject()
val rootInstaller: RootInstaller by inject()
var installedApp: Pair<PackageInfo, InstalledApp?>? by mutableStateOf(null)
private set
var requiredVersion: String? by mutableStateOf(null)
private set
var selectedVersion: SelectedApp? by mutableStateOf(null)
private set
private var nonSuggestedVersionDialogSubject by mutableStateOf<SelectedApp?>(null)
val showNonSuggestedVersionDialog by derivedStateOf { nonSuggestedVersionDialogSubject != null }
private var suggestedVersion: String? = null
init {
viewModelScope.launch {
val packageInfo = async(Dispatchers.IO) { pm.getPackageInfo(packageName) }
val installedAppDeferred =
async(Dispatchers.IO) { installedAppRepository.get(packageName) }
installedApp =
packageInfo.await()?.let {
it to installedAppDeferred.await()
}
}
viewModelScope.launch {
suggestedVersion = patchBundleRepository.suggestedVersions.first()[packageName]
}
}
val supportedVersions = patchBundleRepository.bundles.map supportedVersions@{ bundles ->
var patchesWithoutVersions = 0
bundles.flatMap { (_, bundle) ->
bundle.patches.flatMap { patch ->
patch.compatiblePackages.orEmpty()
.filter { it.packageName == packageName }
.onEach { if (it.versions == null) patchesWithoutVersions++ }
.flatMap { it.versions.orEmpty() }
}
}.groupingBy { it }
.eachCount()
.toMutableMap()
.apply {
replaceAll { _, count ->
count + patchesWithoutVersions
}
}
}.flowOn(Dispatchers.Default)
val hasInstalledPlugins = downloaderPluginRepository.pluginStates.map { it.isNotEmpty() }
val downloadersFlow = downloaderPluginRepository.loadedPluginsFlow
private var downloaderPlugin: LoadedDownloaderPlugin? by mutableStateOf(null)
val downloadableApps by derivedStateOf {
downloaderPlugin?.let { plugin ->
Pager(
config = plugin.pagingConfig
) {
plugin.createVersionPagingSource(packageName, suggestedVersion)
}.flow.map { pagingData ->
pagingData.map {
SelectedApp.Download(
it.packageName,
it.version,
ParceledDownloaderApp(plugin, it)
)
}
}
}?.flowOn(Dispatchers.Default)?.cachedIn(viewModelScope)
}
val downloadedVersions = downloadedAppRepository.getAll().map { downloadedApps ->
downloadedApps
.filter { it.packageName == packageName }
.map {
SelectedApp.Local(
it.packageName,
it.version,
downloadedAppRepository.getApkFileForApp(it),
false
)
}
}
fun selectDownloaderPlugin(plugin: LoadedDownloaderPlugin) {
downloaderPlugin = plugin
}
fun dismissNonSuggestedVersionDialog() {
nonSuggestedVersionDialogSubject = null
}
fun select(app: SelectedApp) {
if (requiredVersion != null && app.version != requiredVersion) {
nonSuggestedVersionDialogSubject = app
return
}
selectedVersion = app
}
}

View File

@ -0,0 +1,12 @@
package app.revanced.manager.util
import android.content.Context
import android.content.Intent
import androidx.activity.result.contract.ActivityResultContract
object IntentContract : ActivityResultContract<Intent, IntentContract.Result>() {
override fun createIntent(context: Context, input: Intent) = input
override fun parseResult(resultCode: Int, intent: Intent?) = Result(resultCode, intent)
class Result(val code: Int, val intent: Intent?)
}

View File

@ -1,3 +1,7 @@
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 class app/revanced/manager/plugin/downloader/App : android/os/Parcelable {
public static final field CREATOR Landroid/os/Parcelable$Creator; public static final field CREATOR Landroid/os/Parcelable$Creator;
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
@ -18,20 +22,20 @@ public final class app/revanced/manager/plugin/downloader/App$Creator : android/
} }
public abstract interface class app/revanced/manager/plugin/downloader/DownloadScope { public abstract interface class app/revanced/manager/plugin/downloader/DownloadScope {
public abstract fun getSaveLocation ()Ljava/io/File; public abstract fun getTargetFile ()Ljava/io/File;
public abstract fun reportProgress (ILjava/lang/Integer;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public abstract fun reportProgress (ILjava/lang/Integer;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
} }
public final class app/revanced/manager/plugin/downloader/Downloader : app/revanced/manager/plugin/downloader/DownloaderMarker { public final class app/revanced/manager/plugin/downloader/Downloader {
public final fun getDownload ()Lkotlin/jvm/functions/Function3; public final fun getDownload ()Lkotlin/jvm/functions/Function3;
public final fun getGetVersions ()Lkotlin/jvm/functions/Function3; public final fun getGet ()Lkotlin/jvm/functions/Function4;
} }
public final class app/revanced/manager/plugin/downloader/DownloaderBuilder { public final class app/revanced/manager/plugin/downloader/DownloaderBuilder {
public fun <init> ()V public fun <init> ()V
public final fun build ()Lapp/revanced/manager/plugin/downloader/Downloader; public final fun build ()Lapp/revanced/manager/plugin/downloader/Downloader;
public final fun download (Lkotlin/jvm/functions/Function3;)V public final fun download (Lkotlin/jvm/functions/Function3;)V
public final fun getVersions (Lkotlin/jvm/functions/Function3;)V public final fun get (Lkotlin/jvm/functions/Function4;)V
} }
public final class app/revanced/manager/plugin/downloader/DownloaderContext { public final class app/revanced/manager/plugin/downloader/DownloaderContext {
@ -40,28 +44,26 @@ public final class app/revanced/manager/plugin/downloader/DownloaderContext {
public final fun getTempDirectory ()Ljava/io/File; public final fun getTempDirectory ()Ljava/io/File;
} }
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 final class app/revanced/manager/plugin/downloader/DownloaderKt {
public static final fun downloader (Lkotlin/jvm/functions/Function1;)Lapp/revanced/manager/plugin/downloader/Downloader; public static final fun downloader (Lkotlin/jvm/functions/Function1;)Lapp/revanced/manager/plugin/downloader/Downloader;
} }
public abstract interface class app/revanced/manager/plugin/downloader/DownloaderMarker { public abstract interface class app/revanced/manager/plugin/downloader/GetScope {
public abstract fun requestUserInteraction (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
} }
public final class app/revanced/manager/plugin/downloader/PaginatedDownloader : app/revanced/manager/plugin/downloader/DownloaderMarker { public abstract class app/revanced/manager/plugin/downloader/UserInteractionException : java/lang/Exception {
public final fun getDownload ()Lkotlin/jvm/functions/Function3; public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getPagingConfig ()Landroidx/paging/PagingConfig;
public final fun getVersionPager ()Lkotlin/jvm/functions/Function2;
} }
public final class app/revanced/manager/plugin/downloader/PaginatedDownloaderBuilder { public final class app/revanced/manager/plugin/downloader/UserInteractionException$ActivityCancelled : app/revanced/manager/plugin/downloader/UserInteractionException {
public fun <init> ()V public fun <init> ()V
public final fun build ()Lapp/revanced/manager/plugin/downloader/PaginatedDownloader;
public final fun download (Lkotlin/jvm/functions/Function3;)V
public final fun versionPager (Landroidx/paging/PagingConfig;Lkotlin/jvm/functions/Function2;)V
public static synthetic fun versionPager$default (Lapp/revanced/manager/plugin/downloader/PaginatedDownloaderBuilder;Landroidx/paging/PagingConfig;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)V
} }
public final class app/revanced/manager/plugin/downloader/PaginatedDownloaderKt { public final class app/revanced/manager/plugin/downloader/UserInteractionException$RequestDenied : app/revanced/manager/plugin/downloader/UserInteractionException {
public static final fun paginatedDownloader (Lkotlin/jvm/functions/Function1;)Lapp/revanced/manager/plugin/downloader/PaginatedDownloader; public fun <init> ()V
} }

View File

@ -1,15 +0,0 @@
package app.revanced.manager.plugin.downloader
import java.io.File
interface DownloadScope {
/**
* The location where the downloaded APK should be saved.
*/
val targetFile: File
/**
* A callback for reporting download progress
*/
suspend fun reportProgress(bytesReceived: Int, bytesTotal: Int?)
}

View File

@ -1,16 +1,41 @@
package app.revanced.manager.plugin.downloader package app.revanced.manager.plugin.downloader
class Downloader<A : App> internal constructor( import android.content.Intent
val getVersions: suspend (packageName: String, versionHint: String?) -> List<A>, import java.io.File
val download: suspend DownloadScope.(app: A) -> Unit
) : DownloaderMarker
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
@DslMarker
annotation class DownloaderDsl
@DownloaderDsl
interface GetScope {
suspend fun requestUserInteraction(): ActivityLaunchPermit
}
fun interface ActivityLaunchPermit {
suspend fun launch(intent: Intent): Intent?
}
@DownloaderDsl
interface DownloadScope {
/**
* The location where the downloaded APK should be saved.
*/
val targetFile: File
/**
* A callback for reporting download progress
*/
suspend fun reportProgress(bytesReceived: Int, bytesTotal: Int?)
}
@DownloaderDsl
class DownloaderBuilder<A : App> { class DownloaderBuilder<A : App> {
private var getVersions: (suspend (String, String?) -> List<A>)? = null
private var download: (suspend DownloadScope.(A) -> Unit)? = null private var download: (suspend DownloadScope.(A) -> Unit)? = null
private var get: (suspend GetScope.(String, String?) -> A?)? = null
fun getVersions(block: suspend (packageName: String, versionHint: String?) -> List<A>) { fun get(block: suspend GetScope.(packageName: String, version: String?) -> A?) {
getVersions = block get = block
} }
fun download(block: suspend DownloadScope.(app: A) -> Unit) { fun download(block: suspend DownloadScope.(app: A) -> Unit) {
@ -18,10 +43,21 @@ class DownloaderBuilder<A : App> {
} }
fun build() = Downloader( fun build() = Downloader(
getVersions = getVersions ?: error("getVersions was not declared"), download = download ?: error("download was not declared"),
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) -> Unit
)
fun <A : App> downloader(block: DownloaderBuilder<A>.() -> Unit) = fun <A : App> downloader(block: DownloaderBuilder<A>.() -> Unit) =
DownloaderBuilder<A>().apply(block).build() DownloaderBuilder<A>().apply(block).build()
sealed class UserInteractionException(message: String) : Exception(message) {
class RequestDenied : UserInteractionException("Request was denied")
// TODO: can cancelled activities return an intent?
class ActivityCancelled : UserInteractionException("Interaction was cancelled")
}

View File

@ -1,3 +0,0 @@
package app.revanced.manager.plugin.downloader
sealed interface DownloaderMarker

View File

@ -1,37 +0,0 @@
package app.revanced.manager.plugin.downloader
import androidx.paging.PagingConfig
import androidx.paging.PagingSource
class PaginatedDownloader<A : App> internal constructor(
val versionPager: (packageName: String, versionHint: String?) -> PagingSource<*, A>,
val pagingConfig: PagingConfig,
val download: suspend DownloadScope.(app: A) -> Unit
) : DownloaderMarker
class PaginatedDownloaderBuilder<A : App> {
private var versionPager: ((String, String?) -> PagingSource<*, A>)? = null
private var download: (suspend DownloadScope.(A) -> Unit)? = null
private var pagingConfig: PagingConfig? = null
fun versionPager(
pagingConfig: PagingConfig = PagingConfig(pageSize = 5),
block: (packageName: String, versionHint: String?) -> PagingSource<*, A>
) {
versionPager = block
this.pagingConfig = pagingConfig
}
fun download(block: suspend DownloadScope.(app: A) -> Unit) {
download = block
}
fun build() = PaginatedDownloader(
versionPager = versionPager ?: error("versionPager was not declared"),
download = download ?: error("download was not declared"),
pagingConfig = pagingConfig!!
)
}
fun <A : App> paginatedDownloader(block: PaginatedDownloaderBuilder<A>.() -> Unit) =
PaginatedDownloaderBuilder<A>().apply(block).build()

View File

@ -5,15 +5,18 @@ plugins {
} }
android { android {
namespace = "app.revanced.manager.plugin.downloader.example" val packageName = "app.revanced.manager.plugin.downloader.example"
namespace = packageName
compileSdk = 34 compileSdk = 34
defaultConfig { defaultConfig {
applicationId = "app.revanced.manager.plugin.downloader.example" applicationId = packageName
minSdk = 26 minSdk = 26
targetSdk = 34 targetSdk = 34
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
buildConfigField("String", "PLUGIN_PACKAGE_NAME", "\"$packageName\"")
} }
buildTypes { buildTypes {
@ -36,8 +39,19 @@ android {
kotlinOptions { kotlinOptions {
jvmTarget = "17" jvmTarget = "17"
} }
composeOptions.kotlinCompilerExtensionVersion = "1.5.10"
buildFeatures {
compose = true
buildConfig = true
}
} }
dependencies { dependencies {
implementation(libs.compose.activity)
implementation(platform(libs.compose.bom))
implementation(libs.compose.ui)
implementation(libs.compose.ui.tooling)
implementation(libs.compose.material3)
compileOnly(project(":downloader-plugin")) compileOnly(project(":downloader-plugin"))
} }

View File

@ -9,8 +9,14 @@
android:label="@string/app_name" android:label="@string/app_name"
tools:targetApi="34"> tools:targetApi="34">
<activity
android:name=".InteractionActivity"
android:exported="true"
android:theme="@android:style/Theme.DeviceDefault" />
<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.ExamplePluginsKt" />
</application> </application>
</manifest> </manifest>

View File

@ -1,13 +1,13 @@
@file:Suppress("Unused") @file:Suppress("Unused")
package app.revanced.manager.plugin.downloader.example package app.revanced.manager.plugin.downloader.example
import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import androidx.paging.PagingSource
import androidx.paging.PagingState
import app.revanced.manager.plugin.downloader.App import app.revanced.manager.plugin.downloader.App
import app.revanced.manager.plugin.downloader.DownloaderContext import app.revanced.manager.plugin.downloader.DownloaderContext
import app.revanced.manager.plugin.downloader.downloader import app.revanced.manager.plugin.downloader.downloader
import app.revanced.manager.plugin.downloader.paginatedDownloader import app.revanced.manager.plugin.downloader.example.BuildConfig.PLUGIN_PACKAGE_NAME
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.nio.file.Files import java.nio.file.Files
@ -23,68 +23,39 @@ class InstalledApp(
internal val apkPath: String internal val apkPath: String
) : App(packageName, version) ) : App(packageName, version)
private fun installedAppDownloader(context: DownloaderContext) = downloader<InstalledApp> { fun installedAppDownloader(context: DownloaderContext) = downloader<InstalledApp> {
val pm = context.androidContext.packageManager val pm = context.androidContext.packageManager
getVersions { packageName, _ -> get { packageName, version ->
val packageInfo = try { val packageInfo = try {
pm.getPackageInfo(packageName, 0) pm.getPackageInfo(packageName, 0)
} catch (_: PackageManager.NameNotFoundException) { } catch (_: PackageManager.NameNotFoundException) {
return@getVersions emptyList() return@get null
} }
listOf( requestUserInteraction().launch(Intent().apply {
setClassName(
PLUGIN_PACKAGE_NAME,
InteractionActivity::class.java.canonicalName!!
)
})
InstalledApp( InstalledApp(
packageName, packageName,
packageInfo.versionName, packageInfo.versionName,
packageInfo.applicationInfo.sourceDir packageInfo.applicationInfo.sourceDir
) ).takeIf { version == null || it.version == version }
)
} }
download { download {
// Simulate download progress
for (i in 0..5) {
reportProgress(i.megaBytes, 5.megaBytes)
delay(1000L)
}
Files.copy(Path(it.apkPath), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING) Files.copy(Path(it.apkPath), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
} }
} }
private val Int.megaBytes get() = times(1_000_000) private val Int.megaBytes get() = times(1_000_000)
val examplePaginatedDownloader = paginatedDownloader {
versionPager { packageName, versionHint ->
object : PagingSource<Int, App>() {
override fun getRefreshKey(state: PagingState<Int, App>) = state.anchorPosition?.let {
state.closestPageToPosition(it)?.prevKey?.plus(1)
?: state.closestPageToPosition(it)?.nextKey?.minus(1)
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, App> {
val page = params.key ?: 0
if (page == 0 && versionHint != null) return LoadResult.Page(
listOf(
App(
packageName,
versionHint
)
),
prevKey = null,
nextKey = 1
)
return LoadResult.Page(
data = List(params.loadSize) { App(packageName, "fake.$page.$it") },
prevKey = page.minus(1).takeIf { it >= 0 },
nextKey = page.plus(1).takeIf { it < 5 }
)
}
}
}
download {
for (i in 0..5) {
reportProgress(i.megaBytes , 5.megaBytes)
delay(1000L)
}
throw Exception("Download simulation complete")
}
}

View File

@ -0,0 +1,65 @@
package app.revanced.manager.plugin.downloader.example
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.Modifier
class InteractionActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val isDarkTheme = isSystemInDarkTheme()
val colorScheme = if (isDarkTheme) darkColorScheme() else lightColorScheme()
MaterialTheme(colorScheme) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("User interaction example") }
)
}
) { paddingValues ->
Column(modifier = Modifier.padding(paddingValues)) {
Text("This is an example interaction.")
Row {
TextButton(
onClick = {
setResult(RESULT_CANCELED)
finish()
}
) {
Text("Cancel")
}
TextButton(
onClick = {
setResult(RESULT_OK)
finish()
}
) {
Text("Continue")
}
}
}
}
}
}
}
}