mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-05-01 14:34:24 +02:00
start implementing the new API
This commit is contained in:
parent
7ec3be460b
commit
f9e8d30ff6
@ -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(
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
)
|
)
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
@ -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
|
||||||
@Parcelize
|
override val version = suggestedVersion.orEmpty()
|
||||||
data class Installed(override val packageName: String, override val version: String) : 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
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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(
|
||||||
|
@ -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)
|
||||||
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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?)
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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?)
|
|
||||||
}
|
|
@ -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")
|
||||||
|
}
|
@ -1,3 +0,0 @@
|
|||||||
package app.revanced.manager.plugin.downloader
|
|
||||||
|
|
||||||
sealed interface DownloaderMarker
|
|
@ -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()
|
|
@ -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"))
|
||||||
}
|
}
|
@ -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>
|
@ -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 {
|
|
||||||
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 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
download {
|
download {
|
||||||
|
// Simulate download progress
|
||||||
for (i in 0..5) {
|
for (i in 0..5) {
|
||||||
reportProgress(i.megaBytes, 5.megaBytes)
|
reportProgress(i.megaBytes, 5.megaBytes)
|
||||||
delay(1000L)
|
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)
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user