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

@ -9,13 +9,13 @@ import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import app.revanced.manager.ui.destination.Destination
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.DashboardScreen
import app.revanced.manager.ui.screen.InstalledAppInfoScreen
import app.revanced.manager.ui.screen.PatcherScreen
import app.revanced.manager.ui.screen.SelectedAppInfoScreen
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.Theme
import app.revanced.manager.ui.viewmodel.MainViewModel
@ -76,12 +76,13 @@ class MainActivity : ComponentActivity() {
is Destination.InstalledApplicationInfo -> InstalledAppInfoScreen(
onPatchClick = { packageName, patchSelection ->
/*
navController.navigate(
Destination.VersionSelector(
packageName,
patchSelection
)
)
)*/
},
onBackClick = { navController.pop() },
viewModel = getComposeViewModel { parametersOf(destination.installedApp) }
@ -93,7 +94,14 @@ class MainActivity : ComponentActivity() {
)
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 = {
navController.navigate(
Destination.SelectedApplicationInfo(
@ -104,24 +112,6 @@ class MainActivity : ComponentActivity() {
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(
onPatchClick = { app, patches, options ->
navController.navigate(

View File

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

View File

@ -5,9 +5,6 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.Signature
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.room.AppDatabase
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.Downloader
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.tag
import dalvik.system.PathClassLoader
@ -115,51 +110,14 @@ class DownloaderPluginRepository(
.loadClass(className)
.getDownloaderImplementation(downloaderContext)
class PluginComponents(
val download: suspend DownloadScope.(App) -> Unit,
val pagingConfig: PagingConfig,
val versionPager: (String, String?) -> PagingSource<*, out App>
)
@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(
LoadedDownloaderPlugin(
packageInfo.packageName,
with(pm) { packageInfo.label() },
packageInfo.versionName,
components.versionPager,
components.download,
components.pagingConfig,
downloader.get,
downloader.download as suspend DownloadScope.(App) -> Unit,
classLoader
)
)
@ -204,19 +162,19 @@ class DownloaderPluginRepository(
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
val Int.isPublicStatic get() = (this and PUBLIC_STATIC) == PUBLIC_STATIC
fun Class<*>.getDownloaderImplementation(context: DownloaderContext) =
declaredMethods
.filter { it.modifiers.isPublicStatic && it.returnType.isDownloaderMarker }
.filter { it.modifiers.isPublicStatic && it.returnType.isDownloader }
.firstNotNullOfOrNull callMethod@{
if (it.parameterTypes contentEquals arrayOf(DownloaderContext::class.java)) return@callMethod it(
null,
context
) as DownloaderMarker
if (it.parameterTypes.isEmpty()) return@callMethod it(null) as DownloaderMarker
) as Downloader<*>
if (it.parameterTypes.isEmpty()) return@callMethod it(null) as Downloader<*>
return@callMethod null
}

View File

@ -1,16 +1,14 @@
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.DownloadScope
import app.revanced.manager.plugin.downloader.GetScope
class LoadedDownloaderPlugin(
val packageName: String,
val name: 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 pagingConfig: PagingConfig,
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.worker.Worker
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.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.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
@ -36,6 +41,7 @@ import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.tag
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@ -57,7 +63,7 @@ class PatcherWorker(
private val installedAppRepository: InstalledAppRepository by inject()
private val rootInstaller: RootInstaller by inject()
data class Args(
class Args(
val input: SelectedApp,
val output: String,
val selectedPatches: PatchSelection,
@ -65,6 +71,7 @@ class PatcherWorker(
val logger: Logger,
val downloadProgress: MutableStateFlow<Pair<Float, Float?>?>,
val patchesProgress: MutableStateFlow<Pair<Int, Int>>,
val handleUserInteractionRequest: suspend () -> ActivityLaunchPermit?,
val setInputFile: (File) -> Unit,
val onProgress: ProgressEventHandler
) {
@ -143,10 +150,7 @@ class PatcherWorker(
}
}
val inputFile = when (val selectedApp = args.input) {
is SelectedApp.Download -> {
val (plugin, app) = downloaderPluginRepository.unwrapParceledApp(selectedApp.app)
suspend fun download(plugin: LoadedDownloaderPlugin, app: DownloaderApp) =
downloadedAppRepository.download(
plugin,
app,
@ -155,6 +159,34 @@ class PatcherWorker(
args.setInputFile(it)
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) }
@ -188,7 +220,10 @@ class PatcherWorker(
Log.i(tag, "Patching succeeded".logFmt())
Result.success()
} 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)
Result.failure()
} catch (e: Exception) {

View File

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

View File

@ -13,7 +13,4 @@ sealed interface SelectedAppInfoDestination : Parcelable {
@Parcelize
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 app.revanced.manager.network.downloader.ParceledDownloaderApp
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import java.io.File
sealed class SelectedApp : Parcelable {
abstract val packageName: String
abstract val version: String
sealed interface SelectedApp : Parcelable {
val packageName: String
val version: String // TODO: make this nullable
@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
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()
data class Downloadable(override val packageName: String, val suggestedVersion: String?) : SelectedApp {
@IgnoredOnParcel
override val version = suggestedVersion.orEmpty()
}
@Parcelize
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)
@Composable
fun AppSelectorScreen(
onAppClick: (packageName: String) -> Unit,
onAppClick: (packageName: String, version: String?) -> Unit,
onStorageClick: (SelectedApp.Local) -> Unit,
onBackClick: () -> Unit,
vm: AppSelectorViewModel = koinViewModel()
@ -90,7 +90,12 @@ fun AppSelectorScreen(
key = { it.packageName }
) { app ->
ListItem(
modifier = Modifier.clickable { onAppClick(app.packageName) },
modifier = Modifier.clickable {
onAppClick(
app.packageName,
suggestedVersions[app.packageName]
)
},
leadingContent = {
AppIcon(
app.packageInfo,
@ -183,7 +188,12 @@ fun AppSelectorScreen(
key = { it.packageName }
) { app ->
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)) },
headlineContent = {
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.PostAdd
import androidx.compose.material.icons.outlined.Save
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
@ -24,7 +25,9 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
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.viewmodel.PatcherViewModel
import app.revanced.manager.util.APK_MIMETYPE
import app.revanced.manager.util.IntentContract
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -91,6 +95,36 @@ fun PatcherScreen(
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(
topBar = {
AppTopBar(

View File

@ -108,7 +108,7 @@ fun SelectedAppInfoScreen(
)
},
onVersionSelectorClick = {
navController.navigate(SelectedAppInfoDestination.VersionSelector)
// navController.navigate(SelectedAppInfoDestination.VersionSelector)
},
onBackClick = onBackClick,
availablePatchCount = availablePatchCount,
@ -118,15 +118,6 @@ fun SelectedAppInfoScreen(
packageInfo = vm.selectedAppInfo,
)
is SelectedAppInfoDestination.VersionSelector -> VersionSelectorScreen(
onBackClick = navController::pop,
onAppClick = {
vm.selectedApp = it
navController.pop()
},
viewModel = koinViewModel { parametersOf(packageName) }
)
is SelectedAppInfoDestination.PatchesSelector -> PatchesSelectorScreen(
onSave = { patches, options ->
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
import android.app.Activity
import android.app.Application
import android.content.BroadcastReceiver
import android.content.Context
@ -9,6 +10,7 @@ import android.content.pm.PackageInstaller
import android.net.Uri
import android.util.Log
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.Logger
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.ui.destination.Destination
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.model.State
import app.revanced.manager.ui.model.Step
import app.revanced.manager.ui.model.StepCategory
import app.revanced.manager.util.IntentContract
import app.revanced.manager.util.PM
import app.revanced.manager.util.simpleMessage
import app.revanced.manager.util.tag
import app.revanced.manager.util.toast
import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
@ -48,7 +54,6 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.time.withTimeout
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File
@ -74,6 +79,13 @@ class PatcherViewModel(
var isInstalling by mutableStateOf(false)
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 {
it.deleteRecursively()
it.mkdirs()
@ -115,6 +127,18 @@ class PatcherViewModel(
downloadProgress,
patchesProgress,
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 ->
viewModelScope.launch {
steps[currentStepIndex] = steps[currentStepIndex].run {
@ -214,6 +238,35 @@ class PatcherViewModel(
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 {
uri?.let {
withContext(Dispatchers.IO) {
@ -306,7 +359,8 @@ class PatcherViewModel(
selectedApp: SelectedApp,
downloadProgress: StateFlow<Pair<Float, Float?>?>? = null
): List<Step> {
val needsDownload = selectedApp is SelectedApp.Download
val needsDownload =
selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Downloadable
return listOfNotNull(
Step(

View File

@ -104,9 +104,9 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
private fun invalidateSelectedAppInfo() = viewModelScope.launch {
val info = when (val app = selectedApp) {
is SelectedApp.Download -> null
is SelectedApp.Local -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.file) }
is SelectedApp.Installed -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.packageName) }
else -> null
}
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 static final field CREATOR Landroid/os/Parcelable$Creator;
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 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 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 getGetVersions ()Lkotlin/jvm/functions/Function3;
public final fun getGet ()Lkotlin/jvm/functions/Function4;
}
public final class app/revanced/manager/plugin/downloader/DownloaderBuilder {
public fun <init> ()V
public final fun build ()Lapp/revanced/manager/plugin/downloader/Downloader;
public final fun download (Lkotlin/jvm/functions/Function3;)V
public final fun getVersions (Lkotlin/jvm/functions/Function3;)V
public final fun get (Lkotlin/jvm/functions/Function4;)V
}
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 abstract interface annotation class app/revanced/manager/plugin/downloader/DownloaderDsl : java/lang/annotation/Annotation {
}
public final class app/revanced/manager/plugin/downloader/DownloaderKt {
public static final fun downloader (Lkotlin/jvm/functions/Function1;)Lapp/revanced/manager/plugin/downloader/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 final fun getDownload ()Lkotlin/jvm/functions/Function3;
public final fun getPagingConfig ()Landroidx/paging/PagingConfig;
public final fun getVersionPager ()Lkotlin/jvm/functions/Function2;
public abstract class app/revanced/manager/plugin/downloader/UserInteractionException : java/lang/Exception {
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
}
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 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 static final fun paginatedDownloader (Lkotlin/jvm/functions/Function1;)Lapp/revanced/manager/plugin/downloader/PaginatedDownloader;
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 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
class Downloader<A : App> internal constructor(
val getVersions: suspend (packageName: String, versionHint: String?) -> List<A>,
val download: suspend DownloadScope.(app: A) -> Unit
) : DownloaderMarker
import android.content.Intent
import java.io.File
@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> {
private var getVersions: (suspend (String, String?) -> List<A>)? = 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>) {
getVersions = block
fun get(block: suspend GetScope.(packageName: String, version: String?) -> A?) {
get = block
}
fun download(block: suspend DownloadScope.(app: A) -> Unit) {
@ -18,10 +43,21 @@ class DownloaderBuilder<A : App> {
}
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) =
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 {
namespace = "app.revanced.manager.plugin.downloader.example"
val packageName = "app.revanced.manager.plugin.downloader.example"
namespace = packageName
compileSdk = 34
defaultConfig {
applicationId = "app.revanced.manager.plugin.downloader.example"
applicationId = packageName
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "1.0"
buildConfigField("String", "PLUGIN_PACKAGE_NAME", "\"$packageName\"")
}
buildTypes {
@ -36,8 +39,19 @@ android {
kotlinOptions {
jvmTarget = "17"
}
composeOptions.kotlinCompilerExtensionVersion = "1.5.10"
buildFeatures {
compose = true
buildConfig = true
}
}
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"))
}

View File

@ -9,8 +9,14 @@
android:label="@string/app_name"
tools:targetApi="34">
<activity
android:name=".InteractionActivity"
android:exported="true"
android:theme="@android:style/Theme.DeviceDefault" />
<meta-data
android:name="app.revanced.manager.plugin.downloader.class"
android:value="app.revanced.manager.plugin.downloader.example.ExamplePluginsKt" />
</application>
</manifest>

View File

@ -1,13 +1,13 @@
@file:Suppress("Unused")
package app.revanced.manager.plugin.downloader.example
import android.content.Intent
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.DownloaderContext
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.parcelize.Parcelize
import java.nio.file.Files
@ -23,68 +23,39 @@ class InstalledApp(
internal val apkPath: String
) : App(packageName, version)
private fun installedAppDownloader(context: DownloaderContext) = downloader<InstalledApp> {
fun installedAppDownloader(context: DownloaderContext) = downloader<InstalledApp> {
val pm = context.androidContext.packageManager
getVersions { packageName, _ ->
get { packageName, version ->
val packageInfo = try {
pm.getPackageInfo(packageName, 0)
} catch (_: PackageManager.NameNotFoundException) {
return@getVersions emptyList()
return@get null
}
listOf(
requestUserInteraction().launch(Intent().apply {
setClassName(
PLUGIN_PACKAGE_NAME,
InteractionActivity::class.java.canonicalName!!
)
})
InstalledApp(
packageName,
packageInfo.versionName,
packageInfo.applicationInfo.sourceDir
)
)
}
download {
Files.copy(Path(it.apkPath), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
}
}
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 }
)
}
}
).takeIf { version == null || it.version == version }
}
download {
// Simulate download progress
for (i in 0..5) {
reportProgress(i.megaBytes, 5.megaBytes)
delay(1000L)
}
throw Exception("Download simulation complete")
Files.copy(Path(it.apkPath), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
}
}
private val Int.megaBytes get() = times(1_000_000)

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