mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2026-03-13 08:41:57 +08:00
Compare commits
15 Commits
api@1.0.0-
...
feat/impro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c23b58f5c | ||
|
|
5eea987b82 | ||
|
|
9432e57cb0 | ||
|
|
8f13cdaf11 | ||
|
|
02ac195307 | ||
|
|
3911e95746 | ||
|
|
b28e9a15be | ||
|
|
9fc2b4fdef | ||
|
|
08662b2132 | ||
|
|
0169fd2109 | ||
|
|
c53d0462d6 | ||
|
|
af8f2afa36 | ||
|
|
9cdb8eafb3 | ||
|
|
fda0e1697b | ||
|
|
2d98923f50 |
@@ -1,3 +1,10 @@
|
||||
# app [1.26.0-dev.35](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.34...v1.26.0-dev.35) (2026-03-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add support for writing downloader UI with fragments ([#2998](https://github.com/ReVanced/revanced-manager/issues/2998)) ([7471710](https://github.com/ReVanced/revanced-manager/commit/7471710f287b4febb9546a7d7cd59d654b2754e9))
|
||||
|
||||
# app [1.26.0-dev.34](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.33...v1.26.0-dev.34) (2026-03-10)
|
||||
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
version = 1.26.0-dev.34
|
||||
version = 1.26.0-dev.35
|
||||
|
||||
@@ -29,7 +29,7 @@ import app.revanced.manager.ui.model.navigation.ComplexParameter
|
||||
import app.revanced.manager.ui.model.navigation.Dashboard
|
||||
import app.revanced.manager.ui.model.navigation.InstalledApplicationInfo
|
||||
import app.revanced.manager.ui.model.navigation.Patcher
|
||||
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
||||
import app.revanced.manager.ui.model.navigation.SelectedAppInfo
|
||||
import app.revanced.manager.ui.model.navigation.Settings
|
||||
import app.revanced.manager.ui.model.navigation.Update
|
||||
import app.revanced.manager.ui.screen.AnnouncementScreen
|
||||
@@ -42,7 +42,9 @@ import app.revanced.manager.ui.screen.PatchesSelectorScreen
|
||||
import app.revanced.manager.ui.screen.RequiredOptionsScreen
|
||||
import app.revanced.manager.ui.screen.SelectedAppInfoScreen
|
||||
import app.revanced.manager.ui.screen.SettingsScreen
|
||||
import app.revanced.manager.ui.screen.SourceSelectorScreen
|
||||
import app.revanced.manager.ui.screen.UpdateScreen
|
||||
import app.revanced.manager.ui.screen.VersionSelectorScreen
|
||||
import app.revanced.manager.ui.screen.settings.AboutSettingsScreen
|
||||
import app.revanced.manager.ui.screen.settings.AdvancedSettingsScreen
|
||||
import app.revanced.manager.ui.screen.settings.ContributorSettingsScreen
|
||||
@@ -97,8 +99,8 @@ private fun ReVancedManager(vm: MainViewModel) {
|
||||
|
||||
EventEffect(vm.appSelectFlow) { app ->
|
||||
navController.navigateComplex(
|
||||
SelectedApplicationInfo,
|
||||
SelectedApplicationInfo.ViewModelParams(app)
|
||||
SelectedAppInfo,
|
||||
app
|
||||
)
|
||||
}
|
||||
|
||||
@@ -138,7 +140,9 @@ private fun ReVancedManager(vm: MainViewModel) {
|
||||
val data = it.toRoute<InstalledApplicationInfo>()
|
||||
|
||||
InstalledAppInfoScreen(
|
||||
onPatchClick = vm::selectApp,
|
||||
onPatchClick = { packageName ->
|
||||
vm.selectApp(packageName)
|
||||
},
|
||||
onBackClick = navController::popBackStack,
|
||||
viewModel = koinViewModel { parametersOf(data.packageName) }
|
||||
)
|
||||
@@ -146,8 +150,12 @@ private fun ReVancedManager(vm: MainViewModel) {
|
||||
|
||||
composable<AppSelector> {
|
||||
AppSelectorScreen(
|
||||
onSelect = vm::selectApp,
|
||||
onStorageSelect = vm::selectApp,
|
||||
onSelect = { packageName ->
|
||||
vm.selectApp(packageName)
|
||||
},
|
||||
onStorageSelect = { packageName, localPath ->
|
||||
vm.selectApp(packageName, localPath)
|
||||
},
|
||||
onBackClick = navController::popBackStack
|
||||
)
|
||||
}
|
||||
@@ -191,11 +199,11 @@ private fun ReVancedManager(vm: MainViewModel) {
|
||||
)
|
||||
}
|
||||
|
||||
navigation<SelectedApplicationInfo>(startDestination = SelectedApplicationInfo.Main) {
|
||||
composable<SelectedApplicationInfo.Main> {
|
||||
navigation<SelectedAppInfo>(startDestination = SelectedAppInfo.Main) {
|
||||
composable<SelectedAppInfo.Main> {
|
||||
val parentBackStackEntry = navController.navGraphEntry(it)
|
||||
val data =
|
||||
parentBackStackEntry.getComplexArg<SelectedApplicationInfo.ViewModelParams>()
|
||||
parentBackStackEntry.getComplexArg<SelectedAppInfo.ViewModelParams>()
|
||||
val viewModel =
|
||||
koinViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
|
||||
parametersOf(data)
|
||||
@@ -211,23 +219,47 @@ private fun ReVancedManager(vm: MainViewModel) {
|
||||
)
|
||||
}
|
||||
},
|
||||
onPatchSelectorClick = { app, patches, options ->
|
||||
onPatchSelectorClick = { packageName, version, patchSelection, options ->
|
||||
navController.navigateComplex(
|
||||
SelectedApplicationInfo.PatchesSelector,
|
||||
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
|
||||
app,
|
||||
patches,
|
||||
options
|
||||
SelectedAppInfo.PatchesSelector,
|
||||
SelectedAppInfo.PatchesSelector.ViewModelParams(
|
||||
packageName,
|
||||
version,
|
||||
patchSelection,
|
||||
options,
|
||||
)
|
||||
)
|
||||
},
|
||||
onRequiredOptions = { app, patches, options ->
|
||||
onRequiredOptions = { packageName, version, patchSelection, options ->
|
||||
navController.navigateComplex(
|
||||
SelectedApplicationInfo.RequiredOptions,
|
||||
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
|
||||
app,
|
||||
patches,
|
||||
options
|
||||
SelectedAppInfo.RequiredOptions,
|
||||
SelectedAppInfo.PatchesSelector.ViewModelParams(
|
||||
packageName,
|
||||
version,
|
||||
patchSelection,
|
||||
options,
|
||||
)
|
||||
)
|
||||
},
|
||||
onVersionClick = { packageName, patchSelection, selectedVersion, local ->
|
||||
navController.navigateComplex(
|
||||
SelectedAppInfo.VersionSelector,
|
||||
SelectedAppInfo.VersionSelector.ViewModelParams(
|
||||
packageName,
|
||||
patchSelection,
|
||||
selectedVersion,
|
||||
local,
|
||||
)
|
||||
)
|
||||
},
|
||||
onSourceClick = { packageName, version, selectedSource, local ->
|
||||
navController.navigateComplex(
|
||||
SelectedAppInfo.SourceSelector,
|
||||
SelectedAppInfo.SourceSelector.ViewModelParams(
|
||||
packageName,
|
||||
version,
|
||||
selectedSource,
|
||||
local,
|
||||
)
|
||||
)
|
||||
},
|
||||
@@ -235,9 +267,9 @@ private fun ReVancedManager(vm: MainViewModel) {
|
||||
)
|
||||
}
|
||||
|
||||
composable<SelectedApplicationInfo.PatchesSelector> {
|
||||
composable<SelectedAppInfo.PatchesSelector> {
|
||||
val data =
|
||||
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
||||
it.getComplexArg<SelectedAppInfo.PatchesSelector.ViewModelParams>()
|
||||
val selectedAppInfoVm = koinViewModel<SelectedAppInfoViewModel>(
|
||||
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||
)
|
||||
@@ -252,9 +284,43 @@ private fun ReVancedManager(vm: MainViewModel) {
|
||||
)
|
||||
}
|
||||
|
||||
composable<SelectedApplicationInfo.RequiredOptions> {
|
||||
composable<SelectedAppInfo.VersionSelector> {
|
||||
val data =
|
||||
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
||||
it.getComplexArg<SelectedAppInfo.VersionSelector.ViewModelParams>()
|
||||
val selectedAppInfoVm = koinViewModel<SelectedAppInfoViewModel>(
|
||||
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||
)
|
||||
|
||||
VersionSelectorScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
onSave = { version ->
|
||||
selectedAppInfoVm.updateVersion(version)
|
||||
navController.popBackStack()
|
||||
},
|
||||
viewModel = koinViewModel { parametersOf(data) }
|
||||
)
|
||||
}
|
||||
|
||||
composable<SelectedAppInfo.SourceSelector> {
|
||||
val data =
|
||||
it.getComplexArg<SelectedAppInfo.SourceSelector.ViewModelParams>()
|
||||
val selectedAppInfoVm = koinViewModel<SelectedAppInfoViewModel>(
|
||||
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||
)
|
||||
|
||||
SourceSelectorScreen(
|
||||
onBackClick = navController::popBackStack,
|
||||
onSave = { source ->
|
||||
selectedAppInfoVm.updateSource(source)
|
||||
navController.popBackStack()
|
||||
},
|
||||
viewModel = koinViewModel { parametersOf(data) }
|
||||
)
|
||||
}
|
||||
|
||||
composable<SelectedAppInfo.RequiredOptions> {
|
||||
val data =
|
||||
it.getComplexArg<SelectedAppInfo.PatchesSelector.ViewModelParams>()
|
||||
val selectedAppInfoVm = koinViewModel<SelectedAppInfoViewModel>(
|
||||
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||
)
|
||||
@@ -347,4 +413,4 @@ private fun <T : Parcelable, R : ComplexParameter<T>> NavController.navigateComp
|
||||
getBackStackEntry(route).savedStateHandle["args"] = data
|
||||
}
|
||||
|
||||
private fun <T : Parcelable> NavBackStackEntry.getComplexArg() = savedStateHandle.get<T>("args")!!
|
||||
private fun <T : Parcelable> NavBackStackEntry.getComplexArg() = savedStateHandle.get<T>("args")!!
|
||||
|
||||
@@ -2,7 +2,6 @@ package app.revanced.manager.data.room.apps.downloaded
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -12,6 +11,9 @@ interface DownloadedAppDao {
|
||||
@Query("SELECT * FROM downloaded_app")
|
||||
fun getAllApps(): Flow<List<DownloadedApp>>
|
||||
|
||||
@Query("SELECT * FROM downloaded_app WHERE package_name = :packageName")
|
||||
fun get(packageName: String): Flow<List<DownloadedApp>>
|
||||
|
||||
@Query("SELECT * FROM downloaded_app WHERE package_name = :packageName AND version = :version")
|
||||
suspend fun get(packageName: String, version: String): DownloadedApp?
|
||||
|
||||
|
||||
@@ -25,4 +25,6 @@ val viewModelModule = module {
|
||||
viewModelOf(::InstalledAppInfoViewModel)
|
||||
viewModelOf(::UpdatesSettingsViewModel)
|
||||
viewModelOf(::BundleListViewModel)
|
||||
viewModelOf(::VersionSelectorViewModel)
|
||||
viewModelOf(::SourceSelectorViewModel)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ class DownloadedAppRepository(
|
||||
|
||||
fun getAll() = dao.getAllApps().distinctUntilChanged()
|
||||
|
||||
fun get(packageName: String) = dao.get(packageName)
|
||||
|
||||
fun getApkFileForApp(app: DownloadedApp): File =
|
||||
getApkFileForDir(dir.resolve(app.directory))
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.patcher.patch.PatchInfo
|
||||
import app.revanced.manager.patcher.patch.PatchBundle
|
||||
import app.revanced.manager.patcher.patch.PatchBundleInfo
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import app.revanced.manager.util.simpleMessage
|
||||
import app.revanced.manager.util.tag
|
||||
import app.revanced.manager.util.toast
|
||||
@@ -75,6 +76,17 @@ class PatchBundleRepository(
|
||||
|
||||
val patchCountsFlow = bundleInfoFlow.map { it.mapValues { (_, info) -> info.patches.size } }
|
||||
|
||||
fun suggestedVersions(packageName: String, patchSelection: PatchSelection) =
|
||||
bundleInfoFlow.map {
|
||||
val allPatches = patchSelection.flatMap { (uid, patches) ->
|
||||
val bundle = it[uid] ?: return@flatMap emptyList()
|
||||
bundle.patches.filter { patch -> patches.contains(patch.name) }
|
||||
.map(PatchInfo::toPatcherPatch)
|
||||
}.toSet()
|
||||
|
||||
allPatches.mostCommonCompatibleVersions(countUnusedPatches = true)[packageName]
|
||||
}
|
||||
|
||||
val suggestedVersions = bundleInfoFlow.map {
|
||||
val allPatches =
|
||||
it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet()
|
||||
|
||||
@@ -16,7 +16,7 @@ class PatchSelectionRepository(db: AppDatabase) {
|
||||
packageName = packageName
|
||||
).also { dao.createSelection(it) }.uid
|
||||
|
||||
suspend fun getSelection(packageName: String): Map<Int, Set<String>> =
|
||||
suspend fun getSelection(packageName: String): app.revanced.manager.util.PatchSelection =
|
||||
dao.getSelectedPatches(packageName).mapValues { it.value.toSet() }
|
||||
|
||||
suspend fun updateSelection(packageName: String, selection: Map<Int, Set<String>>) =
|
||||
|
||||
@@ -40,7 +40,7 @@ data class PatchInfo(
|
||||
if (pkg.packageName != packageName) return@any false
|
||||
if (pkg.versions == null) return@any true
|
||||
|
||||
versionName != null && versionName in pkg.versions
|
||||
versionName == null || versionName in pkg.versions
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
package app.revanced.manager.patcher.worker
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import android.os.PowerManager
|
||||
import android.util.Log
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.WorkerParameters
|
||||
import app.revanced.manager.MainActivity
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.platform.Filesystem
|
||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||
import app.revanced.manager.domain.installer.RootInstaller
|
||||
import app.revanced.manager.domain.manager.KeystoreManager
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
||||
import app.revanced.manager.domain.repository.DownloaderRepository
|
||||
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.LoadedDownloader
|
||||
import app.revanced.manager.patcher.ProgressEvent
|
||||
import app.revanced.manager.patcher.StepId
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.runStep
|
||||
import app.revanced.manager.patcher.runtime.CoroutineRuntime
|
||||
import app.revanced.manager.patcher.runtime.ProcessRuntime
|
||||
import app.revanced.manager.patcher.toRemoteError
|
||||
import app.revanced.manager.downloader.GetScope
|
||||
import app.revanced.manager.downloader.DownloaderHostApi
|
||||
import app.revanced.manager.downloader.Scope
|
||||
import app.revanced.manager.downloader.UserInteractionException
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import app.revanced.manager.util.tag
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.io.File
|
||||
import android.app.Activity
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import android.os.PowerManager
|
||||
import android.util.Log
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.WorkerParameters
|
||||
import app.revanced.manager.MainActivity
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.platform.Filesystem
|
||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||
import app.revanced.manager.domain.installer.RootInstaller
|
||||
import app.revanced.manager.domain.manager.KeystoreManager
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
||||
import app.revanced.manager.domain.repository.DownloaderRepository
|
||||
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.downloader.DownloaderHostApi
|
||||
import app.revanced.manager.downloader.GetScope
|
||||
import app.revanced.manager.downloader.Scope
|
||||
import app.revanced.manager.downloader.UserInteractionException
|
||||
import app.revanced.manager.network.downloader.LoadedDownloader
|
||||
import app.revanced.manager.patcher.ProgressEvent
|
||||
import app.revanced.manager.patcher.StepId
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.runStep
|
||||
import app.revanced.manager.patcher.runtime.CoroutineRuntime
|
||||
import app.revanced.manager.patcher.runtime.ProcessRuntime
|
||||
import app.revanced.manager.patcher.toRemoteError
|
||||
import app.revanced.manager.ui.model.SelectedSource
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import app.revanced.manager.util.tag
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.io.File
|
||||
|
||||
@OptIn(DownloaderHostApi::class)
|
||||
class PatcherWorker(
|
||||
@@ -68,7 +68,9 @@ class PatcherWorker(
|
||||
private val rootInstaller: RootInstaller by inject()
|
||||
|
||||
class Args(
|
||||
val input: SelectedApp,
|
||||
val packageName: String,
|
||||
val version: String?,
|
||||
val source: SelectedSource,
|
||||
val output: String,
|
||||
val selectedPatches: PatchSelection,
|
||||
val options: Options,
|
||||
@@ -76,9 +78,7 @@ class PatcherWorker(
|
||||
val handleStartActivityRequest: suspend (LoadedDownloader, Intent) -> ActivityResult,
|
||||
val setInputFile: suspend (File) -> Unit,
|
||||
val onEvent: (ProgressEvent) -> Unit,
|
||||
) {
|
||||
val packageName get() = input.packageName
|
||||
}
|
||||
)
|
||||
|
||||
override suspend fun getForegroundInfo() =
|
||||
ForegroundInfo(
|
||||
@@ -143,7 +143,7 @@ class PatcherWorker(
|
||||
val patchedApk = fs.tempDir.resolve("patched.apk")
|
||||
|
||||
return try {
|
||||
if (args.input is SelectedApp.Installed) {
|
||||
if (args.source is SelectedSource.Installed) {
|
||||
installedAppRepository.get(args.packageName)?.let {
|
||||
if (it.installType == InstallType.MOUNT) {
|
||||
rootInstaller.unmount(args.packageName)
|
||||
@@ -156,34 +156,31 @@ class PatcherWorker(
|
||||
downloader,
|
||||
data,
|
||||
args.packageName,
|
||||
args.input.version,
|
||||
args.version,
|
||||
prefs.suggestedVersionSafeguard.get(),
|
||||
!prefs.disablePatchVersionCompatCheck.get(),
|
||||
onDownload = { progress ->
|
||||
args.onEvent(
|
||||
ProgressEvent.Progress(
|
||||
stepId = StepId.DownloadAPK,
|
||||
current = progress.first,
|
||||
total = progress.second
|
||||
)
|
||||
) { progress ->
|
||||
args.onEvent(
|
||||
ProgressEvent.Progress(
|
||||
stepId = StepId.DownloadAPK,
|
||||
current = progress.first,
|
||||
total = progress.second
|
||||
)
|
||||
}
|
||||
).also { args.setInputFile(it) }
|
||||
)
|
||||
}.also { args.setInputFile(it) }
|
||||
|
||||
val inputFile = when (val selectedApp = args.input) {
|
||||
is SelectedApp.Download -> {
|
||||
runStep(StepId.DownloadAPK, args.onEvent) {
|
||||
val (downloader, data) = downloaderRepository.unwrapParceledData(
|
||||
selectedApp.data
|
||||
)
|
||||
|
||||
download(downloader, data)
|
||||
}
|
||||
}
|
||||
|
||||
is SelectedApp.Search -> {
|
||||
val inputFile = when (val source = args.source) {
|
||||
SelectedSource.Auto -> throw Exception("Auto source is not supported in worker.")
|
||||
is SelectedSource.Downloader -> {
|
||||
runStep(StepId.DownloadAPK, args.onEvent) {
|
||||
downloaderRepository.loadedDownloadersFlow.first()
|
||||
.filter { downloader ->
|
||||
(source.packageName == null || downloader.packageName == source.packageName) &&
|
||||
(source.className == null || downloader.className == source.className)
|
||||
}
|
||||
.ifEmpty {
|
||||
throw Exception("No downloader available.")
|
||||
}
|
||||
.firstNotNullOfOrNull { downloader ->
|
||||
try {
|
||||
val getScope = object : GetScope, Scope by downloader.scopeImpl {
|
||||
@@ -200,24 +197,32 @@ class PatcherWorker(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
downloader.impl.get(
|
||||
getScope,
|
||||
selectedApp.packageName,
|
||||
selectedApp.version
|
||||
args.packageName,
|
||||
args.version
|
||||
)
|
||||
}?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
|
||||
}?.takeIf { (_, version) ->
|
||||
args.version == null || version == args.version
|
||||
}
|
||||
} catch (e: UserInteractionException.Activity.NotCompleted) {
|
||||
throw e
|
||||
} catch (_: UserInteractionException) {
|
||||
null
|
||||
}?.let { (data, _) -> download(downloader, data) }
|
||||
}?.let { (data, _) ->
|
||||
download(downloader, data)
|
||||
}
|
||||
} ?: throw Exception("App is not available.")
|
||||
}
|
||||
}
|
||||
|
||||
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
|
||||
is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo!!.sourceDir)
|
||||
is SelectedSource.Downloaded -> File(source.path).also { args.setInputFile(it) }
|
||||
is SelectedSource.Local -> File(source.path).also { args.setInputFile(it) }
|
||||
is SelectedSource.Installed -> File(
|
||||
pm.getPackageInfo(args.packageName)!!.applicationInfo!!.sourceDir
|
||||
).also { args.setInputFile(it) }
|
||||
}
|
||||
|
||||
val runtime = if (prefs.useProcessRuntime.get()) {
|
||||
@@ -265,8 +270,8 @@ class PatcherWorker(
|
||||
Result.failure()
|
||||
} finally {
|
||||
patchedApk.delete()
|
||||
if (args.input is SelectedApp.Local && args.input.temporary) {
|
||||
args.input.file.delete()
|
||||
if (args.source is SelectedSource.Local && args.source.path.startsWith(fs.uiTempDir.path)) {
|
||||
File(args.source.path).delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,4 +280,4 @@ class PatcherWorker(
|
||||
private const val LOG_PREFIX = "[Worker]"
|
||||
private fun String.logFmt() = "$LOG_PREFIX $this"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import app.revanced.manager.R
|
||||
import com.eygraber.compose.placeholder.material3.placeholder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -47,6 +48,8 @@ fun AppLabel(
|
||||
shape = RoundedCornerShape(100)
|
||||
)
|
||||
.then(modifier),
|
||||
style = style
|
||||
style = style,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package app.revanced.manager.ui.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import app.revanced.manager.network.downloader.ParceledDownloaderData
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.io.File
|
||||
|
||||
sealed interface SelectedApp : Parcelable {
|
||||
val packageName: String
|
||||
val version: String?
|
||||
|
||||
@Parcelize
|
||||
data class Download(
|
||||
override val packageName: String,
|
||||
override val version: String?,
|
||||
val data: ParceledDownloaderData
|
||||
) : SelectedApp
|
||||
|
||||
@Parcelize
|
||||
data class Search(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
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package app.revanced.manager.ui.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
sealed class SelectedSource : Parcelable {
|
||||
data object Auto : SelectedSource()
|
||||
data object Installed : SelectedSource()
|
||||
data class Downloaded(val path: String, val version: String) : SelectedSource()
|
||||
data class Local(val path: String) : SelectedSource()
|
||||
data class Downloader(
|
||||
val packageName: String? = null,
|
||||
val className: String? = null
|
||||
) : SelectedSource()
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package app.revanced.manager.ui.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
sealed class SelectedVersion : Parcelable {
|
||||
data object Auto : SelectedVersion()
|
||||
data object Any : SelectedVersion()
|
||||
data class Specific(val version: String) : SelectedVersion()
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
package app.revanced.manager.ui.model.navigation
|
||||
|
||||
import android.os.Parcelable
|
||||
import app.revanced.manager.ui.model.SelectedSource
|
||||
import app.revanced.manager.ui.model.SelectedVersion
|
||||
import app.revanced.manager.network.dto.ReVancedAnnouncement
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import kotlinx.parcelize.Parcelize
|
||||
@@ -30,10 +31,11 @@ data object Announcements
|
||||
data object Announcement : ComplexParameter<ReVancedAnnouncement>
|
||||
|
||||
@Serializable
|
||||
data object SelectedApplicationInfo : ComplexParameter<SelectedApplicationInfo.ViewModelParams> {
|
||||
data object SelectedAppInfo : ComplexParameter<SelectedAppInfo.ViewModelParams> {
|
||||
@Parcelize
|
||||
data class ViewModelParams(
|
||||
val app: SelectedApp,
|
||||
val packageName: String,
|
||||
val localPath: String? = null,
|
||||
val patches: PatchSelection? = null
|
||||
) : Parcelable
|
||||
|
||||
@@ -44,12 +46,35 @@ data object SelectedApplicationInfo : ComplexParameter<SelectedApplicationInfo.V
|
||||
data object PatchesSelector : ComplexParameter<PatchesSelector.ViewModelParams> {
|
||||
@Parcelize
|
||||
data class ViewModelParams(
|
||||
val app: SelectedApp,
|
||||
val currentSelection: PatchSelection?,
|
||||
val packageName: String,
|
||||
val version: String?,
|
||||
val patchSelection: PatchSelection?,
|
||||
val options: @RawValue Options,
|
||||
) : Parcelable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object VersionSelector : ComplexParameter<VersionSelector.ViewModelParams> {
|
||||
@Parcelize
|
||||
data class ViewModelParams(
|
||||
val packageName: String,
|
||||
val patchSelection: PatchSelection,
|
||||
val selectedVersion: SelectedVersion,
|
||||
val localPath: String? = null,
|
||||
) : Parcelable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object SourceSelector : ComplexParameter<SourceSelector.ViewModelParams> {
|
||||
@Parcelize
|
||||
data class ViewModelParams(
|
||||
val packageName: String,
|
||||
val version: String?,
|
||||
val selectedSource: SelectedSource,
|
||||
val localPath: String? = null,
|
||||
) : Parcelable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object RequiredOptions : ComplexParameter<PatchesSelector.ViewModelParams>
|
||||
}
|
||||
@@ -58,7 +83,9 @@ data object SelectedApplicationInfo : ComplexParameter<SelectedApplicationInfo.V
|
||||
data object Patcher : ComplexParameter<Patcher.ViewModelParams> {
|
||||
@Parcelize
|
||||
data class ViewModelParams(
|
||||
val selectedApp: SelectedApp,
|
||||
val packageName: String,
|
||||
val version: String?,
|
||||
val selectedSource: SelectedSource,
|
||||
val selectedPatches: PatchSelection,
|
||||
val options: @RawValue Options
|
||||
) : Parcelable
|
||||
@@ -100,4 +127,4 @@ object Settings {
|
||||
|
||||
@Serializable
|
||||
data object Developer : Destination
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,9 +42,7 @@ import app.revanced.manager.ui.component.AppLabel
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
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.component.SearchView
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.viewmodel.AppSelectorViewModel
|
||||
import app.revanced.manager.util.APK_MIMETYPE
|
||||
import app.revanced.manager.util.EventEffect
|
||||
@@ -54,13 +52,13 @@ import org.koin.androidx.compose.koinViewModel
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AppSelectorScreen(
|
||||
onSelect: (String) -> Unit,
|
||||
onStorageSelect: (SelectedApp.Local) -> Unit,
|
||||
onSelect: (packageName: String) -> Unit,
|
||||
onStorageSelect: (packageName: String, path: String) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
vm: AppSelectorViewModel = koinViewModel()
|
||||
) {
|
||||
EventEffect(flow = vm.storageSelectionFlow) {
|
||||
onStorageSelect(it)
|
||||
onStorageSelect(it.first, it.second)
|
||||
}
|
||||
|
||||
val pickApkLauncher =
|
||||
@@ -68,19 +66,10 @@ fun AppSelectorScreen(
|
||||
uri?.let(vm::handleStorageResult)
|
||||
}
|
||||
|
||||
val suggestedVersions by vm.suggestedAppVersions.collectAsStateWithLifecycle()
|
||||
|
||||
var search by rememberSaveable { mutableStateOf(false) }
|
||||
val appList by vm.apps.collectAsStateWithLifecycle()
|
||||
val appsListFiltered by vm.filteredApps.collectAsStateWithLifecycle()
|
||||
|
||||
vm.nonSuggestedVersionDialogSubject?.let {
|
||||
NonSuggestedVersionDialog(
|
||||
suggestedVersion = suggestedVersions[it.packageName].orEmpty(),
|
||||
onDismiss = vm::dismissNonSuggestedVersionDialog
|
||||
)
|
||||
}
|
||||
|
||||
if (search) {
|
||||
val filterText by vm.filterText.collectAsState()
|
||||
|
||||
@@ -109,18 +98,20 @@ fun AppSelectorScreen(
|
||||
)
|
||||
},
|
||||
headlineContent = { AppLabel(app.packageInfo) },
|
||||
supportingContent = { Text(app.packageName) },
|
||||
trailingContent = app.patches?.let {
|
||||
supportingContent = app.patches?.let {
|
||||
{
|
||||
Text(
|
||||
pluralStringResource(
|
||||
R.plurals.patch_count,
|
||||
R.plurals.available_patch_count,
|
||||
it,
|
||||
it
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
trailingContent = if (app.packageInfo == null) {
|
||||
{ Text(stringResource(R.string.not_installed)) }
|
||||
} else null,
|
||||
colors = transparentListItemColors
|
||||
)
|
||||
}
|
||||
@@ -226,22 +217,20 @@ fun AppSelectorScreen(
|
||||
defaultText = app.packageName
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
suggestedVersions[app.packageName]?.let {
|
||||
Text(stringResource(R.string.suggested_version_info, it))
|
||||
}
|
||||
},
|
||||
trailingContent = app.patches?.let {
|
||||
supportingContent = app.patches?.let {
|
||||
{
|
||||
Text(
|
||||
pluralStringResource(
|
||||
R.plurals.patch_count,
|
||||
R.plurals.available_patch_count,
|
||||
it,
|
||||
it
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
trailingContent = if (app.packageInfo == null) {
|
||||
{ Text(stringResource(R.string.not_installed)) }
|
||||
} else null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
package app.revanced.manager.ui.screen
|
||||
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
||||
import androidx.compose.material.icons.filled.AutoFixHigh
|
||||
@@ -21,82 +17,73 @@ import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.platform.NetworkInfo
|
||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
||||
import app.revanced.manager.network.downloader.LoadedDownloader
|
||||
import app.revanced.manager.ui.component.AlertDialogExtended
|
||||
import app.revanced.manager.ui.component.AppInfo
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.LoadingIndicator
|
||||
import app.revanced.manager.ui.component.NotificationCard
|
||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.model.SelectedSource
|
||||
import app.revanced.manager.ui.model.SelectedVersion
|
||||
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
|
||||
import app.revanced.manager.util.EventEffect
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import app.revanced.manager.util.enabled
|
||||
import app.revanced.manager.util.patchCount
|
||||
import app.revanced.manager.util.toast
|
||||
import app.revanced.manager.util.transparentListItemColors
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SelectedAppInfoScreen(
|
||||
onPatchSelectorClick: (SelectedApp, PatchSelection?, Options) -> Unit,
|
||||
onRequiredOptions: (SelectedApp, PatchSelection?, Options) -> Unit,
|
||||
onPatchSelectorClick: (packageName: String, version: String?, PatchSelection?, Options) -> Unit,
|
||||
onRequiredOptions: (packageName: String, version: String?, PatchSelection?, Options) -> Unit,
|
||||
onPatchClick: () -> Unit,
|
||||
onVersionClick: (packageName: String, patchSelection: PatchSelection, selectedVersion: SelectedVersion, localPath: String?) -> Unit,
|
||||
onSourceClick: (packageName: String, version: String?, SelectedSource, localPath: String?) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
vm: SelectedAppInfoViewModel
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val resources = LocalResources.current
|
||||
val context = androidx.compose.ui.platform.LocalContext.current
|
||||
val networkInfo = koinInject<NetworkInfo>()
|
||||
val networkConnected = remember { networkInfo.isConnected() }
|
||||
val networkMetered = remember { !networkInfo.isUnmetered() }
|
||||
|
||||
val packageName = vm.selectedApp.packageName
|
||||
val version = vm.selectedApp.version
|
||||
val bundles by vm.bundleInfoFlow.collectAsStateWithLifecycle(emptyList())
|
||||
|
||||
val allowIncompatiblePatches by vm.prefs.disablePatchVersionCompatCheck.getAsState()
|
||||
val patches by remember {
|
||||
derivedStateOf {
|
||||
vm.getPatches(bundles, allowIncompatiblePatches)
|
||||
}
|
||||
}
|
||||
val selectedPatchCount = patches.values.sumOf { it.size }
|
||||
|
||||
val launcher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = vm::handleDownloaderActivityResult
|
||||
)
|
||||
EventEffect(flow = vm.launchActivityFlow) { intent ->
|
||||
launcher.launch(intent)
|
||||
}
|
||||
val packageName = vm.packageName
|
||||
val composableScope = rememberCoroutineScope()
|
||||
|
||||
val downloaders by vm.downloaders.collectAsStateWithLifecycle(emptyList())
|
||||
val error by vm.errorFlow.collectAsStateWithLifecycle(null)
|
||||
|
||||
val selectedVersion by vm.selectedVersion.collectAsStateWithLifecycle()
|
||||
val resolvedVersion by vm.resolvedVersion.collectAsStateWithLifecycle(null)
|
||||
val versionSelection by vm.versionPatchSelection.collectAsStateWithLifecycle(emptyMap())
|
||||
|
||||
val selectedSource by vm.selectedSource.collectAsStateWithLifecycle()
|
||||
val resolvedSource by vm.resolvedSource.collectAsStateWithLifecycle(SelectedSource.Downloader())
|
||||
|
||||
val customSelection by vm.customSelection.collectAsStateWithLifecycle(null)
|
||||
val currentPatchSelection by vm.patchSelection.collectAsStateWithLifecycle(emptyMap())
|
||||
val patchCount = currentPatchSelection.patchCount
|
||||
|
||||
val incompatibleCount by vm.incompatiblePatchCount.collectAsStateWithLifecycle(0)
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
@@ -119,22 +106,28 @@ fun SelectedAppInfoScreen(
|
||||
)
|
||||
},
|
||||
onClick = patchClick@{
|
||||
if (selectedPatchCount == 0) {
|
||||
context.toast(resources.getString(R.string.no_patches_selected))
|
||||
|
||||
if (patchCount == 0) {
|
||||
context.toast(context.getString(R.string.no_patches_selected))
|
||||
return@patchClick
|
||||
}
|
||||
|
||||
composableScope.launch {
|
||||
if (!vm.hasSetRequiredOptions(patches)) {
|
||||
if (!vm.hasSetRequiredOptions(currentPatchSelection)) {
|
||||
onRequiredOptions(
|
||||
vm.selectedApp,
|
||||
vm.getCustomPatches(bundles, allowIncompatiblePatches),
|
||||
vm.options
|
||||
packageName,
|
||||
resolvedVersion,
|
||||
customSelection,
|
||||
vm.options,
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
vm.reloadDownloaders()
|
||||
if (vm.errorFlow.first() != null) {
|
||||
context.toast(context.getString(R.string.no_downloader_available))
|
||||
return@launch
|
||||
}
|
||||
|
||||
onPatchClick()
|
||||
}
|
||||
}
|
||||
@@ -142,110 +135,110 @@ fun SelectedAppInfoScreen(
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) { paddingValues ->
|
||||
val downloaders by vm.downloaders.collectAsStateWithLifecycle(emptyList())
|
||||
|
||||
if (vm.showSourceSelector) {
|
||||
val requiredVersion by vm.requiredVersion.collectAsStateWithLifecycle(null)
|
||||
|
||||
AppSourceSelectorDialog(
|
||||
downloaders = downloaders,
|
||||
installedApp = vm.installedAppData,
|
||||
searchApp = SelectedApp.Search(
|
||||
vm.packageName,
|
||||
vm.desiredVersion
|
||||
),
|
||||
activeSearchJob = vm.activeDownloader,
|
||||
hasRoot = vm.hasRoot,
|
||||
onDismissRequest = vm::dismissSourceSelector,
|
||||
onSelectDownloader = vm::searchUsingDownloader,
|
||||
requiredVersion = requiredVersion,
|
||||
onSelect = {
|
||||
vm.selectedApp = it
|
||||
vm.dismissSourceSelector()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
ColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
AppInfo(vm.selectedAppInfo, placeholderLabel = packageName) {
|
||||
Text(
|
||||
version ?: stringResource(R.string.selected_app_meta_any_version),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
vm.selectedAppInfo?.packageName?.let {
|
||||
Text(
|
||||
it,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
PageItem(
|
||||
R.string.patch_selector_item,
|
||||
stringResource(
|
||||
R.string.patch_selector_item_description,
|
||||
selectedPatchCount
|
||||
),
|
||||
title = R.string.patch_selector_item,
|
||||
description = stringResource(R.string.patch_selector_item_description, patchCount),
|
||||
onClick = {
|
||||
onPatchSelectorClick(
|
||||
vm.selectedApp,
|
||||
vm.getCustomPatches(
|
||||
bundles,
|
||||
allowIncompatiblePatches
|
||||
),
|
||||
packageName,
|
||||
resolvedVersion,
|
||||
customSelection,
|
||||
vm.options
|
||||
)
|
||||
}
|
||||
)
|
||||
PageItem(
|
||||
R.string.apk_source_selector_item,
|
||||
when (val app = vm.selectedApp) {
|
||||
is SelectedApp.Search -> stringResource(R.string.apk_source_auto)
|
||||
is SelectedApp.Installed -> stringResource(R.string.apk_source_installed)
|
||||
is SelectedApp.Download -> stringResource(
|
||||
R.string.apk_source_downloader,
|
||||
downloaders.find { it.packageName == app.data.downloaderPackageName && it.name == app.data.downloaderClassName }?.name
|
||||
?: app.data.downloaderPackageName
|
||||
)
|
||||
|
||||
is SelectedApp.Local -> stringResource(R.string.apk_source_local)
|
||||
},
|
||||
onClick = {
|
||||
vm.showSourceSelector()
|
||||
extraDescription = if (incompatibleCount > 0) {
|
||||
{
|
||||
Text(
|
||||
pluralStringResource(
|
||||
R.plurals.version_selector_incompatible_patches,
|
||||
incompatibleCount,
|
||||
incompatibleCount
|
||||
),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
|
||||
PageItem(
|
||||
title = R.string.version_selector_item,
|
||||
description = selectedVersionDescription(selectedVersion, resolvedVersion),
|
||||
onClick = {
|
||||
onVersionClick(
|
||||
packageName,
|
||||
versionSelection,
|
||||
selectedVersion,
|
||||
vm.localPath,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
PageItem(
|
||||
title = R.string.apk_source_selector_item,
|
||||
description = selectedSourceDescription(
|
||||
selectedSource = selectedSource,
|
||||
resolvedSource = resolvedSource,
|
||||
downloaders = downloaders,
|
||||
),
|
||||
onClick = {
|
||||
onSourceClick(
|
||||
packageName,
|
||||
resolvedVersion,
|
||||
selectedSource,
|
||||
vm.localPath,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
error?.let {
|
||||
Text(
|
||||
stringResource(it.resourceId),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(horizontal = 24.dp)
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
val needsInternet =
|
||||
vm.selectedApp.let { it is SelectedApp.Search || it is SelectedApp.Download }
|
||||
if (resolvedSource is SelectedSource.Downloader) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
when {
|
||||
!networkConnected -> {
|
||||
NotificationCard(
|
||||
isWarning = true,
|
||||
icon = Icons.Outlined.WarningAmber,
|
||||
text = stringResource(R.string.network_unavailable_warning),
|
||||
onDismiss = null
|
||||
)
|
||||
}
|
||||
|
||||
when {
|
||||
!needsInternet -> {}
|
||||
!networkConnected -> {
|
||||
NotificationCard(
|
||||
isWarning = true,
|
||||
icon = Icons.Outlined.WarningAmber,
|
||||
text = stringResource(R.string.network_unavailable_warning),
|
||||
onDismiss = null
|
||||
)
|
||||
}
|
||||
|
||||
networkMetered -> {
|
||||
NotificationCard(
|
||||
isWarning = true,
|
||||
icon = Icons.Outlined.WarningAmber,
|
||||
text = stringResource(R.string.network_metered_warning),
|
||||
onDismiss = null
|
||||
)
|
||||
networkMetered -> {
|
||||
NotificationCard(
|
||||
isWarning = true,
|
||||
icon = Icons.Outlined.WarningAmber,
|
||||
text = stringResource(R.string.network_metered_warning),
|
||||
onDismiss = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -254,11 +247,64 @@ fun SelectedAppInfoScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PageItem(@StringRes title: Int, description: String, onClick: () -> Unit) {
|
||||
private fun selectedVersionDescription(
|
||||
selectedVersion: SelectedVersion,
|
||||
resolvedVersion: String?,
|
||||
): String {
|
||||
val resolvedText = resolvedVersion ?: stringResource(R.string.selected_app_meta_any_version)
|
||||
return if (selectedVersion is SelectedVersion.Auto) {
|
||||
stringResource(R.string.selected_app_meta_auto, stringResource(R.string.app_source_dialog_option_auto), resolvedText)
|
||||
} else {
|
||||
resolvedText
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun selectedSourceDescription(
|
||||
selectedSource: SelectedSource,
|
||||
resolvedSource: SelectedSource,
|
||||
downloaders: List<LoadedDownloader>,
|
||||
): String {
|
||||
val resolvedText = when (resolvedSource) {
|
||||
SelectedSource.Auto -> stringResource(R.string.app_source_dialog_option_auto)
|
||||
SelectedSource.Installed -> stringResource(R.string.selected_app_meta_source_installed_apk)
|
||||
is SelectedSource.Downloaded -> stringResource(R.string.selected_app_meta_source_downloaded_apk)
|
||||
is SelectedSource.Local -> stringResource(R.string.selected_app_meta_source_local_apk)
|
||||
is SelectedSource.Downloader -> {
|
||||
if (resolvedSource.packageName == null && resolvedSource.className == null) {
|
||||
stringResource(R.string.selected_app_meta_source_any_downloader)
|
||||
} else {
|
||||
downloaders.firstOrNull { downloader ->
|
||||
downloader.packageName == resolvedSource.packageName &&
|
||||
downloader.className == resolvedSource.className
|
||||
}?.name ?: stringResource(R.string.selected_app_meta_source_any_downloader)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return if (selectedSource is SelectedSource.Auto) {
|
||||
stringResource(
|
||||
R.string.selected_app_meta_auto,
|
||||
stringResource(R.string.app_source_dialog_option_auto),
|
||||
resolvedText
|
||||
)
|
||||
} else {
|
||||
resolvedText
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PageItem(
|
||||
@StringRes title: Int,
|
||||
description: String,
|
||||
onClick: () -> Unit,
|
||||
enabled: Boolean = true,
|
||||
extraDescription: @Composable (ColumnScope.() -> Unit)? = null,
|
||||
) {
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onClick)
|
||||
.padding(start = 8.dp),
|
||||
.clickable(enabled, onClick = onClick)
|
||||
.enabled(enabled),
|
||||
headlineContent = {
|
||||
Text(
|
||||
stringResource(title),
|
||||
@@ -267,105 +313,17 @@ private fun PageItem(@StringRes title: Int, description: String, onClick: () ->
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
description,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
description,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
extraDescription?.invoke(this)
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
Icon(Icons.AutoMirrored.Outlined.ArrowRight, null)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppSourceSelectorDialog(
|
||||
downloaders: List<LoadedDownloader>,
|
||||
installedApp: Pair<SelectedApp.Installed, InstalledApp?>?,
|
||||
searchApp: SelectedApp.Search,
|
||||
activeSearchJob: LoadedDownloader?,
|
||||
hasRoot: Boolean,
|
||||
requiredVersion: String?,
|
||||
onDismissRequest: () -> Unit,
|
||||
onSelectDownloader: (LoadedDownloader) -> Unit,
|
||||
onSelect: (SelectedApp) -> Unit,
|
||||
) {
|
||||
val canSelect = activeSearchJob == null
|
||||
|
||||
AlertDialogExtended(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
title = { Text(stringResource(R.string.app_source_dialog_title)) },
|
||||
textHorizontalPadding = PaddingValues(horizontal = 0.dp),
|
||||
text = {
|
||||
LazyColumn {
|
||||
item(key = "auto") {
|
||||
val hasDownloader = downloaders.isNotEmpty()
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.clickable(enabled = canSelect && hasDownloader) { onSelect(searchApp) }
|
||||
.enabled(hasDownloader),
|
||||
headlineContent = { Text(stringResource(R.string.app_source_dialog_option_auto)) },
|
||||
supportingContent = {
|
||||
Text(
|
||||
if (hasDownloader)
|
||||
stringResource(R.string.app_source_dialog_option_auto_description)
|
||||
else
|
||||
stringResource(R.string.app_source_dialog_option_auto_unavailable)
|
||||
)
|
||||
},
|
||||
colors = transparentListItemColors
|
||||
)
|
||||
}
|
||||
|
||||
installedApp?.let { (app, meta) ->
|
||||
item(key = "installed") {
|
||||
val (usable, text) = when {
|
||||
// Mounted apps must be unpatched before patching, which cannot be done without root access.
|
||||
meta?.installType == InstallType.MOUNT && !hasRoot -> false to stringResource(
|
||||
R.string.app_source_dialog_option_installed_no_root
|
||||
)
|
||||
// Patching already patched apps is not allowed because patches expect unpatched apps.
|
||||
meta?.installType == InstallType.DEFAULT -> false to stringResource(R.string.already_patched)
|
||||
// Version does not match suggested version.
|
||||
requiredVersion != null && app.version != requiredVersion -> false to stringResource(
|
||||
R.string.app_source_dialog_option_installed_version_not_suggested,
|
||||
app.version
|
||||
)
|
||||
|
||||
else -> true to app.version
|
||||
}
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.clickable(enabled = canSelect && usable) { onSelect(app) }
|
||||
.enabled(usable),
|
||||
headlineContent = { Text(stringResource(R.string.installed)) },
|
||||
supportingContent = { Text(text) },
|
||||
colors = transparentListItemColors
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items(
|
||||
downloaders,
|
||||
key = { "downloader_${it.packageName}_${it.className}" }) { downloader ->
|
||||
ListItem(
|
||||
modifier = Modifier.clickable(enabled = canSelect) {
|
||||
onSelectDownloader(
|
||||
downloader
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(downloader.name) },
|
||||
trailingContent = (@Composable { LoadingIndicator() }).takeIf { activeSearchJob == downloader },
|
||||
colors = transparentListItemColors
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package app.revanced.manager.ui.screen
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Save
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.GroupHeader
|
||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||
import app.revanced.manager.ui.model.SelectedSource
|
||||
import app.revanced.manager.ui.viewmodel.SourceSelectorViewModel
|
||||
import app.revanced.manager.util.enabled
|
||||
import app.revanced.manager.util.transparentListItemColors
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SourceSelectorScreen(
|
||||
onBackClick: () -> Unit,
|
||||
onSave: (source: SelectedSource) -> Unit,
|
||||
viewModel: SourceSelectorViewModel,
|
||||
) {
|
||||
val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList())
|
||||
val allDownloaderSections by viewModel.downloaderSections.collectAsStateWithLifecycle(emptyList())
|
||||
val (downloaderSections, unavailableSections) = allDownloaderSections.partition { section ->
|
||||
section.options.all { it.disableReason == null }
|
||||
}
|
||||
val unavailableDownloaders = unavailableSections.flatMap { it.options }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = { Text(stringResource(R.string.app_source_dialog_title)) },
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
HapticExtendedFloatingActionButton(
|
||||
text = { Text(stringResource(R.string.save)) },
|
||||
icon = { Icon(Icons.Outlined.Save, null) },
|
||||
onClick = { onSave(viewModel.selectedSource) },
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
ColumnWithScrollbar(modifier = Modifier.padding(paddingValues)) {
|
||||
SourceOption(
|
||||
isSelected = viewModel.selectedSource == SelectedSource.Auto,
|
||||
onSelect = { viewModel.selectSource(SelectedSource.Auto) },
|
||||
headlineContent = { Text(stringResource(R.string.version_selector_auto_title)) },
|
||||
supportingContent = { Text(stringResource(R.string.source_selector_auto_description)) },
|
||||
)
|
||||
|
||||
viewModel.localApp?.let { option ->
|
||||
GroupHeader(stringResource(R.string.source_selector_category_local))
|
||||
SourceOption(option, viewModel.selectedSource, viewModel::selectSource)
|
||||
}
|
||||
|
||||
viewModel.installedSource?.let { option ->
|
||||
GroupHeader(stringResource(R.string.installed))
|
||||
SourceOption(option, viewModel.selectedSource, viewModel::selectSource)
|
||||
}
|
||||
|
||||
if (downloadedApps.isNotEmpty()) {
|
||||
GroupHeader(stringResource(R.string.source_selector_category_downloaded))
|
||||
downloadedApps.forEach { option ->
|
||||
SourceOption(option, viewModel.selectedSource, viewModel::selectSource)
|
||||
}
|
||||
}
|
||||
|
||||
downloaderSections.forEach { section ->
|
||||
GroupHeader(section.title)
|
||||
section.options.forEach { option ->
|
||||
SourceOption(option, viewModel.selectedSource, viewModel::selectSource)
|
||||
}
|
||||
}
|
||||
|
||||
if (unavailableDownloaders.isNotEmpty()) {
|
||||
GroupHeader(stringResource(R.string.downloaders))
|
||||
unavailableDownloaders.forEach { option ->
|
||||
SourceOption(option, viewModel.selectedSource, viewModel::selectSource)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SourceOption(
|
||||
sourceOption: SourceSelectorViewModel.SourceOption,
|
||||
selectedSource: SelectedSource,
|
||||
onSelect: (SelectedSource) -> Unit,
|
||||
) {
|
||||
SourceOption(
|
||||
isSelected = selectedSource == sourceOption.source,
|
||||
onSelect = { onSelect(sourceOption.source) },
|
||||
headlineContent = { Text(sourceOption.title, maxLines = 1, overflow = TextOverflow.Ellipsis) },
|
||||
supportingContent = sourceOption.disableReason?.let { { Text(stringResource(it.message)) } },
|
||||
enabled = sourceOption.disableReason == null,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SourceOption(
|
||||
isSelected: Boolean,
|
||||
onSelect: () -> Unit,
|
||||
headlineContent: @Composable (() -> Unit),
|
||||
supportingContent: @Composable (() -> Unit)? = null,
|
||||
overlineContent: @Composable (() -> Unit)? = null,
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.clickable(enabled) { onSelect() }
|
||||
.enabled(enabled),
|
||||
leadingContent = {
|
||||
RadioButton(
|
||||
selected = isSelected,
|
||||
onClick = null
|
||||
)
|
||||
},
|
||||
headlineContent = headlineContent,
|
||||
supportingContent = supportingContent,
|
||||
overlineContent = overlineContent,
|
||||
colors = transparentListItemColors
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package app.revanced.manager.ui.screen
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Save
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||
import app.revanced.manager.ui.model.SelectedVersion
|
||||
import app.revanced.manager.ui.viewmodel.VersionSelectorViewModel
|
||||
import app.revanced.manager.util.transparentListItemColors
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun VersionSelectorScreen(
|
||||
onBackClick: () -> Unit,
|
||||
onSave: (version: SelectedVersion) -> Unit,
|
||||
viewModel: VersionSelectorViewModel,
|
||||
) {
|
||||
val versions by viewModel.availableVersions.collectAsStateWithLifecycle(emptyList())
|
||||
val downloadedVersions by viewModel.downloadedVersions.collectAsStateWithLifecycle(emptyList())
|
||||
val localVersion by viewModel.localVersion.collectAsStateWithLifecycle(null)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = { Text(stringResource(R.string.version_selector_title)) },
|
||||
onBackClick = onBackClick
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
HapticExtendedFloatingActionButton(
|
||||
text = { Text(stringResource(R.string.save)) },
|
||||
icon = { Icon(Icons.Outlined.Save, contentDescription = null) },
|
||||
onClick = { onSave(viewModel.selectedVersion) }
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
LazyColumnWithScrollbar(contentPadding = paddingValues) {
|
||||
item {
|
||||
VersionOption(
|
||||
version = SelectedVersion.Auto,
|
||||
isSelected = viewModel.selectedVersion is SelectedVersion.Auto,
|
||||
onSelect = viewModel::selectVersion,
|
||||
headlineContent = { Text(stringResource(R.string.version_selector_auto_title)) },
|
||||
supportingContent = { Text(stringResource(R.string.version_selector_auto_description)) }
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
if (versions.isNotEmpty()) {
|
||||
items(versions, key = { (version, _) -> version.version }) { (version, incompatibleCount) ->
|
||||
val overlineText = when {
|
||||
localVersion == version.version -> stringResource(R.string.version_selector_state_local)
|
||||
version.version in downloadedVersions && viewModel.installedAppVersion == version.version ->
|
||||
stringResource(R.string.version_selector_state_downloaded_installed)
|
||||
version.version in downloadedVersions -> stringResource(R.string.version_selector_state_downloaded)
|
||||
viewModel.installedAppVersion == version.version -> stringResource(R.string.version_selector_state_installed)
|
||||
else -> null
|
||||
}
|
||||
|
||||
VersionOption(
|
||||
version = version,
|
||||
isSelected = viewModel.selectedVersion == version,
|
||||
onSelect = viewModel::selectVersion,
|
||||
headlineContent = { Text(version.version) },
|
||||
supportingContent = {
|
||||
Text(
|
||||
if (incompatibleCount == 0) stringResource(R.string.version_selector_all_patches_compatible)
|
||||
else pluralStringResource(R.plurals.version_selector_incompatible_patches, incompatibleCount, incompatibleCount)
|
||||
)
|
||||
},
|
||||
overlineContent = overlineText?.let { { Text(it) } }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
VersionOption(
|
||||
version = SelectedVersion.Any,
|
||||
isSelected = viewModel.selectedVersion is SelectedVersion.Any,
|
||||
onSelect = viewModel::selectVersion,
|
||||
headlineContent = { Text(stringResource(R.string.version_selector_any_title)) },
|
||||
supportingContent = { Text(stringResource(R.string.version_selector_any_description)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VersionOption(
|
||||
version: SelectedVersion,
|
||||
isSelected: Boolean,
|
||||
onSelect: (SelectedVersion) -> Unit,
|
||||
headlineContent: @Composable (() -> Unit),
|
||||
supportingContent: @Composable (() -> Unit)? = null,
|
||||
overlineContent: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { onSelect(version) },
|
||||
leadingContent = {
|
||||
RadioButton(
|
||||
selected = isSelected,
|
||||
onClick = null
|
||||
)
|
||||
},
|
||||
headlineContent = headlineContent,
|
||||
supportingContent = supportingContent,
|
||||
trailingContent = overlineContent,
|
||||
colors = transparentListItemColors
|
||||
)
|
||||
}
|
||||
@@ -3,9 +3,6 @@ package app.revanced.manager.ui.viewmodel
|
||||
import android.app.Application
|
||||
import android.content.pm.PackageInfo
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
@@ -13,8 +10,6 @@ import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
|
||||
import androidx.lifecycle.viewmodel.compose.saveable
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.platform.Filesystem
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.toast
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -35,7 +30,6 @@ class AppSelectorViewModel(
|
||||
private val app: Application,
|
||||
private val pm: PM,
|
||||
fs: Filesystem,
|
||||
private val patchBundleRepository: PatchBundleRepository,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel() {
|
||||
private val inputFile = savedStateHandle.saveable(key = "inputFile") {
|
||||
@@ -73,28 +67,15 @@ class AppSelectorViewModel(
|
||||
initialValue = null,
|
||||
)
|
||||
|
||||
private val storageSelectionChannel = Channel<SelectedApp.Local>()
|
||||
private val storageSelectionChannel = Channel<Pair<String, String>>()
|
||||
val storageSelectionFlow = storageSelectionChannel.receiveAsFlow()
|
||||
|
||||
val suggestedAppVersions = patchBundleRepository.suggestedVersions.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = emptyMap(),
|
||||
)
|
||||
|
||||
var nonSuggestedVersionDialogSubject by mutableStateOf<SelectedApp.Local?>(null)
|
||||
private set
|
||||
|
||||
fun setFilterText(filter: String) {
|
||||
filterTextFlow.value = filter
|
||||
}
|
||||
|
||||
fun loadLabel(app: PackageInfo?) = with(pm) { app?.label() ?: "Not installed" }
|
||||
|
||||
fun dismissNonSuggestedVersionDialog() {
|
||||
nonSuggestedVersionDialogSubject = null
|
||||
}
|
||||
|
||||
fun handleStorageResult(uri: Uri) = viewModelScope.launch {
|
||||
val selectedApp = withContext(Dispatchers.IO) {
|
||||
loadSelectedFile(uri)
|
||||
@@ -105,11 +86,8 @@ class AppSelectorViewModel(
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (patchBundleRepository.isVersionAllowed(selectedApp.packageName, selectedApp.version)) {
|
||||
storageSelectionChannel.send(selectedApp)
|
||||
} else {
|
||||
nonSuggestedVersionDialogSubject = selectedApp
|
||||
}
|
||||
// TODO: Disallow if 0 patches are compatible
|
||||
storageSelectionChannel.send(selectedApp)
|
||||
}
|
||||
|
||||
private fun loadSelectedFile(uri: Uri) =
|
||||
@@ -119,12 +97,7 @@ class AppSelectorViewModel(
|
||||
Files.copy(stream, toPath())
|
||||
|
||||
pm.getPackageInfo(this)?.let { packageInfo ->
|
||||
SelectedApp.Local(
|
||||
packageName = packageInfo.packageName,
|
||||
version = packageInfo.versionName!!,
|
||||
file = this,
|
||||
temporary = true
|
||||
)
|
||||
Pair(packageInfo.packageName, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,11 @@ import app.revanced.manager.data.room.apps.installed.InstallType
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
|
||||
import app.revanced.manager.domain.manager.KeystoreManager
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
||||
import app.revanced.manager.domain.repository.SerializedSelection
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.model.navigation.SelectedAppInfo
|
||||
import app.revanced.manager.ui.theme.Theme
|
||||
import app.revanced.manager.util.tag
|
||||
import app.revanced.manager.util.toast
|
||||
@@ -36,41 +35,17 @@ private const val LEGACY_LIST_PREFIX = "VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu
|
||||
class MainViewModel(
|
||||
private val patchBundleRepository: PatchBundleRepository,
|
||||
private val patchSelectionRepository: PatchSelectionRepository,
|
||||
private val downloadedAppRepository: DownloadedAppRepository,
|
||||
private val installedAppRepository: InstalledAppRepository,
|
||||
private val keystoreManager: KeystoreManager,
|
||||
private val app: Application,
|
||||
val prefs: PreferencesManager,
|
||||
private val json: Json
|
||||
) : ViewModel() {
|
||||
private val appSelectChannel = Channel<SelectedApp>()
|
||||
private val appSelectChannel = Channel<SelectedAppInfo.ViewModelParams>()
|
||||
val appSelectFlow = appSelectChannel.receiveAsFlow()
|
||||
|
||||
private suspend fun suggestedVersion(packageName: String) =
|
||||
patchBundleRepository.suggestedVersions.first()[packageName]
|
||||
|
||||
private suspend fun findDownloadedApp(app: SelectedApp): SelectedApp.Local? {
|
||||
if (app !is SelectedApp.Search) return null
|
||||
|
||||
val suggestedVersion = suggestedVersion(app.packageName) ?: return null
|
||||
|
||||
val downloadedApp =
|
||||
downloadedAppRepository.get(app.packageName, suggestedVersion, markUsed = true)
|
||||
?: return null
|
||||
return SelectedApp.Local(
|
||||
downloadedApp.packageName,
|
||||
downloadedApp.version,
|
||||
downloadedAppRepository.getApkFileForApp(downloadedApp),
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
fun selectApp(app: SelectedApp) = viewModelScope.launch {
|
||||
appSelectChannel.send(findDownloadedApp(app) ?: app)
|
||||
}
|
||||
|
||||
fun selectApp(packageName: String) = viewModelScope.launch {
|
||||
selectApp(SelectedApp.Search(packageName, suggestedVersion(packageName)))
|
||||
fun selectApp(packageName: String, localPath: String? = null) = viewModelScope.launch {
|
||||
appSelectChannel.send(SelectedAppInfo.ViewModelParams(packageName, localPath))
|
||||
}
|
||||
|
||||
init {
|
||||
|
||||
@@ -37,7 +37,7 @@ 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.ui.model.InstallerModel
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.model.SelectedSource
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.ui.model.Step
|
||||
import app.revanced.manager.ui.model.StepCategory
|
||||
@@ -96,9 +96,8 @@ class PatcherViewModel(
|
||||
private val ackpineInstaller: PackageInstaller = get()
|
||||
|
||||
private var installedApp: InstalledApp? = null
|
||||
private val selectedApp = input.selectedApp
|
||||
val packageName = selectedApp.packageName
|
||||
val version = selectedApp.version
|
||||
val packageName = input.packageName
|
||||
val version = input.version
|
||||
|
||||
var installedPackageName by savedStateHandle.saveable(
|
||||
key = "installedPackageName",
|
||||
@@ -163,7 +162,7 @@ class PatcherViewModel(
|
||||
}
|
||||
|
||||
val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) {
|
||||
generateSteps(app, input.selectedApp, input.selectedPatches).toMutableStateList()
|
||||
generateSteps(app, input.selectedSource, input.selectedPatches).toMutableStateList()
|
||||
}
|
||||
|
||||
val progress by derivedStateOf {
|
||||
@@ -181,7 +180,9 @@ class PatcherViewModel(
|
||||
ParcelUuid(
|
||||
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
||||
"patching", PatcherWorker.Args(
|
||||
input.selectedApp,
|
||||
input.packageName,
|
||||
input.version,
|
||||
input.selectedSource,
|
||||
outputFile.path,
|
||||
input.selectedPatches,
|
||||
input.options,
|
||||
@@ -260,7 +261,7 @@ class PatcherViewModel(
|
||||
super.onCleared()
|
||||
workManager.cancelWorkById(patcherWorkerId.uuid)
|
||||
|
||||
if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) {
|
||||
if (input.selectedSource is SelectedSource.Installed && installedApp?.installType == InstallType.MOUNT) {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
uiSafe(app, R.string.failed_to_mount, "Failed to mount") {
|
||||
withTimeout(Duration.ofMinutes(1L)) {
|
||||
@@ -385,7 +386,7 @@ class PatcherViewModel(
|
||||
installedAppRepository.addOrUpdate(
|
||||
installerPkgName,
|
||||
packageName,
|
||||
input.selectedApp.version
|
||||
input.version
|
||||
?: withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile)?.versionName!! },
|
||||
InstallType.DEFAULT,
|
||||
input.selectedPatches,
|
||||
@@ -439,9 +440,20 @@ class PatcherViewModel(
|
||||
currentPackageInfo.label()
|
||||
}
|
||||
|
||||
val inputVersion = input.selectedApp.version
|
||||
?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName }
|
||||
?: throw Exception("Failed to determine input APK version")
|
||||
val existingPackageInfo =
|
||||
withContext(Dispatchers.IO) { pm.getPackageInfo(currentPackageInfo.packageName) }
|
||||
if (existingPackageInfo == null && currentPackageInfo.splitNames.isNotEmpty()) {
|
||||
packageInstallerStatus =
|
||||
AndroidPackageInstaller.STATUS_FAILURE_INVALID
|
||||
return@launch
|
||||
}
|
||||
|
||||
val inputVersion = when (val source = input.selectedSource) {
|
||||
is SelectedSource.Downloaded -> source.version
|
||||
else -> input.version
|
||||
?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName }
|
||||
?: throw Exception("Failed to determine input APK version")
|
||||
}
|
||||
|
||||
needsRootUninstall = true
|
||||
// Install as root
|
||||
@@ -533,10 +545,10 @@ class PatcherViewModel(
|
||||
|
||||
fun generateSteps(
|
||||
context: Context,
|
||||
selectedApp: SelectedApp,
|
||||
selectedSource: SelectedSource,
|
||||
selectedPatches: PatchSelection
|
||||
): List<Step> = buildList {
|
||||
if (selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search)
|
||||
if (selectedSource is SelectedSource.Downloader)
|
||||
add(
|
||||
Step(
|
||||
StepId.DownloadAPK,
|
||||
@@ -594,4 +606,4 @@ class PatcherViewModel(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.patcher.patch.PatchBundleInfo
|
||||
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection
|
||||
import app.revanced.manager.patcher.patch.PatchInfo
|
||||
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
||||
import app.revanced.manager.ui.model.navigation.SelectedAppInfo
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import app.revanced.manager.util.saver.Nullable
|
||||
@@ -45,14 +45,14 @@ import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
|
||||
@OptIn(SavedStateHandleSaveableApi::class)
|
||||
class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.ViewModelParams) :
|
||||
class PatchesSelectorViewModel(input: SelectedAppInfo.PatchesSelector.ViewModelParams) :
|
||||
ViewModel(), KoinComponent {
|
||||
private val app: Application = get()
|
||||
private val savedStateHandle: SavedStateHandle = get()
|
||||
private val prefs: PreferencesManager = get()
|
||||
|
||||
private val packageName = input.app.packageName
|
||||
val appVersion = input.app.version
|
||||
private val packageName = input.packageName
|
||||
val appVersion = input.version
|
||||
|
||||
var selectionWarningEnabled by mutableStateOf(true)
|
||||
private set
|
||||
@@ -62,7 +62,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
||||
val allowIncompatiblePatches =
|
||||
get<PreferencesManager>().disablePatchVersionCompatCheck.getBlocking()
|
||||
val bundlesFlow =
|
||||
get<PatchBundleRepository>().scopedBundleInfoFlow(packageName, input.app.version)
|
||||
get<PatchBundleRepository>().scopedBundleInfoFlow(packageName, input.version)
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
@@ -88,7 +88,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
||||
key = "selection",
|
||||
stateSaver = selectionSaver,
|
||||
) {
|
||||
mutableStateOf(input.currentSelection?.toPersistentPatchSelection())
|
||||
mutableStateOf(input.patchSelection?.toPersistentPatchSelection())
|
||||
}
|
||||
|
||||
private val patchOptions: PersistentOptions by savedStateHandle.saveable(
|
||||
|
||||
@@ -1,137 +1,143 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInfo
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
|
||||
import androidx.lifecycle.viewmodel.compose.saveable
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||
import app.revanced.manager.domain.installer.RootInstaller
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
||||
import app.revanced.manager.domain.repository.DownloaderRepository
|
||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.domain.repository.PatchOptionsRepository
|
||||
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
||||
import app.revanced.manager.patcher.patch.PatchBundleInfo
|
||||
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection
|
||||
import app.revanced.manager.network.downloader.LoadedDownloader
|
||||
import app.revanced.manager.network.downloader.ParceledDownloaderData
|
||||
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.requiredOptionsSet
|
||||
import app.revanced.manager.downloader.GetScope
|
||||
import app.revanced.manager.downloader.DownloaderHostApi
|
||||
import app.revanced.manager.downloader.Scope
|
||||
import app.revanced.manager.downloader.UserInteractionException
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection
|
||||
import app.revanced.manager.ui.model.SelectedSource
|
||||
import app.revanced.manager.ui.model.SelectedVersion
|
||||
import app.revanced.manager.ui.model.navigation.Patcher
|
||||
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
||||
import app.revanced.manager.ui.model.navigation.SelectedAppInfo
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import app.revanced.manager.util.simpleMessage
|
||||
import app.revanced.manager.util.tag
|
||||
import app.revanced.manager.util.toast
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import app.revanced.manager.util.patchCount
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import java.io.File
|
||||
|
||||
@OptIn(SavedStateHandleSaveableApi::class, DownloaderHostApi::class)
|
||||
@OptIn(SavedStateHandleSaveableApi::class)
|
||||
class SelectedAppInfoViewModel(
|
||||
input: SelectedApplicationInfo.ViewModelParams
|
||||
input: SelectedAppInfo.ViewModelParams
|
||||
) : ViewModel(), KoinComponent {
|
||||
private val app: Application = get()
|
||||
private val bundleRepository: PatchBundleRepository = get()
|
||||
private val selectionRepository: PatchSelectionRepository = get()
|
||||
private val optionsRepository: PatchOptionsRepository = get()
|
||||
private val downloaderRepository: DownloaderRepository = get()
|
||||
private val installedAppRepository: InstalledAppRepository = get()
|
||||
private val downloadedAppRepository: DownloadedAppRepository = get()
|
||||
private val rootInstaller: RootInstaller = get()
|
||||
private val pm: PM = get()
|
||||
private val savedStateHandle: SavedStateHandle = get()
|
||||
val prefs: PreferencesManager = get()
|
||||
val downloaders = downloaderRepository.loadedDownloadersFlow
|
||||
val desiredVersion = input.app.version
|
||||
val packageName = input.app.packageName
|
||||
|
||||
val downloaders = downloaderRepository.loadedDownloadersFlow
|
||||
val packageName = input.packageName
|
||||
val localPath = input.localPath
|
||||
private val persistConfiguration = input.patches == null
|
||||
|
||||
val hasRoot = rootInstaller.hasRootAccess()
|
||||
var installedAppData: Pair<SelectedApp.Installed, InstalledApp?>? by mutableStateOf(null)
|
||||
private set
|
||||
private val selectionFlow = MutableStateFlow(
|
||||
input.patches?.let(SelectionState::Customized) ?: SelectionState.Default
|
||||
)
|
||||
|
||||
private var _selectedApp by savedStateHandle.saveable {
|
||||
mutableStateOf(input.app)
|
||||
private val _selectedVersion = MutableStateFlow<SelectedVersion>(SelectedVersion.Auto)
|
||||
val selectedVersion: StateFlow<SelectedVersion> = _selectedVersion
|
||||
|
||||
private val _selectedSource = MutableStateFlow<SelectedSource>(SelectedSource.Auto)
|
||||
val selectedSource: StateFlow<SelectedSource> = _selectedSource
|
||||
|
||||
private val unscopedBundles = bundleRepository.scopedBundleInfoFlow(packageName, null)
|
||||
|
||||
val versionPatchSelection = combine(selectionFlow, unscopedBundles) { selection, bundleInfo ->
|
||||
selection.patches(bundleInfo, allowIncompatible = true)
|
||||
}
|
||||
|
||||
var selectedAppInfo: PackageInfo? by mutableStateOf(null)
|
||||
private set
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val mostCompatibleVersions = versionPatchSelection.flatMapLatest { selection ->
|
||||
bundleRepository.suggestedVersions(packageName, selection)
|
||||
}
|
||||
|
||||
var selectedApp
|
||||
get() = _selectedApp
|
||||
set(value) {
|
||||
_selectedApp = value
|
||||
invalidateSelectedAppInfo()
|
||||
}
|
||||
|
||||
init {
|
||||
invalidateSelectedAppInfo()
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
val packageInfo = async(Dispatchers.IO) { pm.getPackageInfo(packageName) }
|
||||
val installedAppDeferred =
|
||||
async(Dispatchers.IO) { installedAppRepository.get(packageName) }
|
||||
|
||||
installedAppData =
|
||||
packageInfo.await()?.let {
|
||||
SelectedApp.Installed(
|
||||
packageName,
|
||||
it.versionName!!
|
||||
) to installedAppDeferred.await()
|
||||
}
|
||||
val resolvedVersion = combine(
|
||||
_selectedVersion,
|
||||
mostCompatibleVersions,
|
||||
) { selected, mostCompatible ->
|
||||
when (selected) {
|
||||
is SelectedVersion.Specific -> selected.version
|
||||
is SelectedVersion.Any -> null
|
||||
is SelectedVersion.Auto -> mostCompatible?.maxWithOrNull(
|
||||
compareBy<Map.Entry<String, Int>> { it.value }
|
||||
.thenBy { it.key }
|
||||
)?.key
|
||||
}
|
||||
}
|
||||
|
||||
val requiredVersion = combine(
|
||||
prefs.suggestedVersionSafeguard.flow,
|
||||
bundleRepository.suggestedVersions
|
||||
) { suggestedVersionSafeguard, suggestedVersions ->
|
||||
if (!suggestedVersionSafeguard) return@combine null
|
||||
|
||||
suggestedVersions[input.app.packageName]
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val scopedBundles = resolvedVersion.flatMapLatest { version ->
|
||||
bundleRepository.scopedBundleInfoFlow(packageName, version)
|
||||
}
|
||||
|
||||
val bundleInfoFlow by derivedStateOf {
|
||||
bundleRepository.scopedBundleInfoFlow(packageName, selectedApp.version)
|
||||
val patchSelection = combine(selectionFlow, scopedBundles) { selection, bundleInfo ->
|
||||
selection.patches(bundleInfo, allowIncompatible = true)
|
||||
}
|
||||
|
||||
val customSelection = combine(selectionFlow, scopedBundles) { selection, bundleInfo ->
|
||||
(selection as? SelectionState.Customized)?.patches(bundleInfo, allowIncompatible = true)
|
||||
}
|
||||
|
||||
val incompatiblePatchCount = combine(selectionFlow, scopedBundles) { selection, bundleInfo ->
|
||||
val selectedPatches = selection.patches(bundleInfo, allowIncompatible = true)
|
||||
bundleInfo.sumOf { bundle ->
|
||||
bundle.incompatible.count { patch ->
|
||||
patch.name in selectedPatches[bundle.uid].orEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val resolvedSource = combine(
|
||||
_selectedSource,
|
||||
resolvedVersion,
|
||||
) { source, version ->
|
||||
when {
|
||||
source == SelectedSource.Auto -> resolveAutoSource(version)
|
||||
isSourceValid(source, version) -> source
|
||||
else -> resolveAutoSource(version)
|
||||
}
|
||||
}
|
||||
|
||||
var options: Options by savedStateHandle.saveable {
|
||||
viewModelScope.launch {
|
||||
if (!persistConfiguration) return@launch // TODO: save options for patched apps.
|
||||
val bundlePatches = bundleInfoFlow.first()
|
||||
val bundlePatches = unscopedBundles.first()
|
||||
.associate { it.uid to it.patches.associateBy { patch -> patch.name } }
|
||||
|
||||
options = withContext(Dispatchers.Default) {
|
||||
@@ -143,165 +149,106 @@ class SelectedAppInfoViewModel(
|
||||
}
|
||||
private set
|
||||
|
||||
private var selectionState: SelectionState by savedStateHandle.saveable {
|
||||
if (input.patches != null)
|
||||
return@saveable mutableStateOf(SelectionState.Customized(input.patches))
|
||||
|
||||
// Try to get the previous selection if customization is enabled.
|
||||
viewModelScope.launch {
|
||||
if (!prefs.disableSelectionWarning.get()) return@launch
|
||||
|
||||
val previous = selectionRepository.getSelection(packageName)
|
||||
if (previous.values.sumOf { it.size } == 0) return@launch
|
||||
selectionState = SelectionState.Customized(previous)
|
||||
}
|
||||
|
||||
mutableStateOf(SelectionState.Default)
|
||||
}
|
||||
|
||||
var showSourceSelector by mutableStateOf(false)
|
||||
private set
|
||||
private var downloaderAction: Pair<LoadedDownloader, Job>? by mutableStateOf(null)
|
||||
val activeDownloader get() = downloaderAction?.first
|
||||
private var launchedActivity by mutableStateOf<CompletableDeferred<ActivityResult>?>(null)
|
||||
private val launchActivityChannel = Channel<Intent>()
|
||||
val launchActivityFlow = launchActivityChannel.receiveAsFlow()
|
||||
|
||||
val errorFlow = combine(downloaders, snapshotFlow { selectedApp }) { downloaderList, app ->
|
||||
val errorFlow = combine(downloaders, resolvedSource) { downloaderList, source ->
|
||||
when {
|
||||
app is SelectedApp.Search && downloaderList.isEmpty() -> Error.NoDownloaders
|
||||
source is SelectedSource.Downloader && matchingDownloaders(
|
||||
source,
|
||||
downloaderList
|
||||
).isEmpty() -> Error.NoDownloaders
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun showSourceSelector() {
|
||||
dismissSourceSelector()
|
||||
showSourceSelector = true
|
||||
var selectedAppInfo: PackageInfo? by mutableStateOf(null)
|
||||
private set
|
||||
|
||||
fun updateVersion(version: SelectedVersion) {
|
||||
_selectedVersion.value = version
|
||||
}
|
||||
|
||||
private fun cancelDownloaderAction() {
|
||||
downloaderAction?.second?.cancel()
|
||||
downloaderAction = null
|
||||
fun updateSource(source: SelectedSource) {
|
||||
_selectedSource.value = source
|
||||
}
|
||||
|
||||
fun dismissSourceSelector() {
|
||||
cancelDownloaderAction()
|
||||
showSourceSelector = false
|
||||
}
|
||||
|
||||
fun searchUsingDownloader(downloader: LoadedDownloader) {
|
||||
cancelDownloaderAction()
|
||||
downloaderAction = downloader to viewModelScope.launch {
|
||||
try {
|
||||
val scope = object : GetScope, Scope by downloader.scopeImpl {
|
||||
override suspend fun requestStartActivity(intent: Intent) =
|
||||
withContext(Dispatchers.Main) {
|
||||
if (launchedActivity != null) error("Previous activity has not finished")
|
||||
try {
|
||||
val result = with(CompletableDeferred<ActivityResult>()) {
|
||||
launchedActivity = this
|
||||
launchActivityChannel.send(intent)
|
||||
await()
|
||||
}
|
||||
when (result.resultCode) {
|
||||
Activity.RESULT_OK -> result.data
|
||||
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
|
||||
else -> throw UserInteractionException.Activity.NotCompleted(
|
||||
result.resultCode,
|
||||
result.data
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
launchedActivity = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
downloader.impl.get(scope, packageName, desiredVersion)
|
||||
}?.let { (data, version) ->
|
||||
if (desiredVersion != null && version != desiredVersion) {
|
||||
app.toast(app.getString(R.string.downloader_invalid_version))
|
||||
return@launch
|
||||
}
|
||||
selectedApp = SelectedApp.Download(
|
||||
packageName,
|
||||
version,
|
||||
ParceledDownloaderData(downloader, data)
|
||||
)
|
||||
} ?: app.toast(app.getString(R.string.downloader_app_not_found))
|
||||
} catch (e: UserInteractionException.Activity) {
|
||||
app.toast(e.message!!)
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
app.toast(app.getString(R.string.downloader_error, e.simpleMessage()))
|
||||
Log.e(tag, "Downloader.get threw an exception", e)
|
||||
} finally {
|
||||
downloaderAction = null
|
||||
dismissSourceSelector()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleDownloaderActivityResult(result: ActivityResult) {
|
||||
launchedActivity?.complete(result)
|
||||
}
|
||||
|
||||
private fun invalidateSelectedAppInfo() = viewModelScope.launch {
|
||||
val info = when (val app = selectedApp) {
|
||||
is SelectedApp.Local -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.file) }
|
||||
is SelectedApp.Installed -> withContext(Dispatchers.IO) { pm.getPackageInfo(app.packageName) }
|
||||
else -> null
|
||||
}
|
||||
|
||||
selectedAppInfo = info
|
||||
suspend fun reloadDownloaders() {
|
||||
downloaderRepository.reload()
|
||||
}
|
||||
|
||||
fun getOptionsFiltered(bundles: List<PatchBundleInfo.Scoped>) = options.filtered(bundles)
|
||||
suspend fun hasSetRequiredOptions(patchSelection: PatchSelection) = bundleInfoFlow
|
||||
.first()
|
||||
.requiredOptionsSet(
|
||||
allowIncompatible = prefs.disablePatchVersionCompatCheck.get(),
|
||||
isSelected = { bundle, patch -> patch.name in patchSelection[bundle.uid]!! },
|
||||
suspend fun hasSetRequiredOptions(patchSelection: PatchSelection): Boolean {
|
||||
val allowIncompatible = prefs.disablePatchVersionCompatCheck.get()
|
||||
val bundles = scopedBundles.first()
|
||||
|
||||
return bundles.requiredOptionsSet(
|
||||
allowIncompatible = allowIncompatible,
|
||||
isSelected = { bundle, patch -> patch.name in patchSelection[bundle.uid].orEmpty() },
|
||||
optionsForPatch = { bundle, patch -> options[bundle.uid]?.get(patch.name) },
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getPatcherParams(): Patcher.ViewModelParams {
|
||||
val allowIncompatible = prefs.disablePatchVersionCompatCheck.get()
|
||||
val bundles = bundleInfoFlow.first()
|
||||
val bundles = scopedBundles.first()
|
||||
|
||||
return Patcher.ViewModelParams(
|
||||
selectedApp,
|
||||
getPatches(bundles, allowIncompatible),
|
||||
packageName,
|
||||
resolvedVersion.first(),
|
||||
resolvedSource.first(),
|
||||
selectionFlow.value.patches(bundles, allowIncompatible),
|
||||
getOptionsFiltered(bundles)
|
||||
)
|
||||
}
|
||||
|
||||
fun getPatches(bundles: List<PatchBundleInfo.Scoped>, allowIncompatible: Boolean) =
|
||||
selectionState.patches(bundles, allowIncompatible)
|
||||
|
||||
fun getCustomPatches(
|
||||
bundles: List<PatchBundleInfo.Scoped>,
|
||||
allowIncompatible: Boolean
|
||||
): PatchSelection? =
|
||||
(selectionState as? SelectionState.Customized)?.patches(bundles, allowIncompatible)
|
||||
|
||||
|
||||
fun updateConfiguration(
|
||||
selection: PatchSelection?,
|
||||
options: Options
|
||||
selectedOptions: Options
|
||||
) = viewModelScope.launch {
|
||||
selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default
|
||||
selectionFlow.value = selection?.let(SelectionState::Customized) ?: SelectionState.Default
|
||||
|
||||
val filteredOptions = options.filtered(bundleInfoFlow.first())
|
||||
this@SelectedAppInfoViewModel.options = filteredOptions
|
||||
val filteredOptions = selectedOptions.filtered(scopedBundles.first())
|
||||
options = filteredOptions
|
||||
|
||||
if (!persistConfiguration) return@launch
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
selection?.let { selectionRepository.updateSelection(packageName, it) }
|
||||
?: selectionRepository.resetSelectionForPackage(packageName)
|
||||
|
||||
optionsRepository.saveOptions(packageName, filteredOptions)
|
||||
selection?.let { selectionRepository.updateSelection(packageName, it) }
|
||||
?: selectionRepository.resetSelectionForPackage(packageName)
|
||||
|
||||
optionsRepository.saveOptions(packageName, filteredOptions)
|
||||
}
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
reloadDownloaders()
|
||||
invalidateSelectedAppInfo()
|
||||
}
|
||||
|
||||
localPath?.let { local ->
|
||||
viewModelScope.launch {
|
||||
val packageInfo = withContext(Dispatchers.IO) { pm.getPackageInfo(File(local)) }
|
||||
?: return@launch
|
||||
|
||||
_selectedVersion.value = SelectedVersion.Specific(packageInfo.versionName!!)
|
||||
_selectedSource.value = SelectedSource.Local(local)
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
if (prefs.disableSelectionWarning.get()) {
|
||||
val previous = selectionRepository.getSelection(packageName)
|
||||
if (previous.patchCount != 0) {
|
||||
selectionFlow.value = SelectionState.Customized(previous)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
combine(_selectedSource, resolvedVersion) { source, version -> source to version }
|
||||
.collect { (source, version) ->
|
||||
if (source != SelectedSource.Auto && !isSourceValid(source, version)) {
|
||||
_selectedSource.value = SelectedSource.Auto
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,6 +256,70 @@ class SelectedAppInfoViewModel(
|
||||
NoDownloaders(R.string.no_downloader_available)
|
||||
}
|
||||
|
||||
private suspend fun invalidateSelectedAppInfo() {
|
||||
selectedAppInfo = withContext(Dispatchers.IO) {
|
||||
pm.getPackageInfo(packageName) ?: localPath?.let { pm.getPackageInfo(File(it)) }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun resolveAutoSource(version: String?): SelectedSource {
|
||||
val installedPackage = pm.getPackageInfo(packageName)
|
||||
val installedApp = installedAppRepository.get(packageName)
|
||||
val installType = installedApp?.installType
|
||||
val isInstalledVersionMatch = version == null || installedPackage?.versionName == version
|
||||
|
||||
if (
|
||||
installedPackage != null &&
|
||||
isInstalledVersionMatch &&
|
||||
installType != InstallType.DEFAULT &&
|
||||
!(installType == InstallType.MOUNT && !rootInstaller.hasRootAccess()) &&
|
||||
installedPackage.applicationInfo?.splitSourceDirs.isNullOrEmpty()
|
||||
) {
|
||||
return SelectedSource.Installed
|
||||
}
|
||||
|
||||
if (version != null) {
|
||||
downloadedAppRepository.get(packageName, version)?.let { app ->
|
||||
return SelectedSource.Downloaded(
|
||||
path = downloadedAppRepository.getApkFileForApp(app).path,
|
||||
version = version
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return SelectedSource.Downloader()
|
||||
}
|
||||
|
||||
private suspend fun isSourceValid(source: SelectedSource, version: String?): Boolean =
|
||||
when (source) {
|
||||
SelectedSource.Auto -> true
|
||||
SelectedSource.Installed -> {
|
||||
val installedPackage = pm.getPackageInfo(packageName) ?: return false
|
||||
val installedApp = installedAppRepository.get(packageName)
|
||||
|
||||
installedApp?.installType != InstallType.DEFAULT &&
|
||||
!(installedApp?.installType == InstallType.MOUNT && !rootInstaller.hasRootAccess()) &&
|
||||
(version == null || installedPackage.versionName == version)
|
||||
}
|
||||
|
||||
is SelectedSource.Downloaded -> version == null || source.version == version
|
||||
is SelectedSource.Local -> {
|
||||
val packageInfo = withContext(Dispatchers.IO) { pm.getPackageInfo(File(source.path)) }
|
||||
?: return false
|
||||
version == null || packageInfo.versionName == version
|
||||
}
|
||||
|
||||
is SelectedSource.Downloader -> true
|
||||
}
|
||||
|
||||
private fun matchingDownloaders(
|
||||
source: SelectedSource.Downloader,
|
||||
downloaderList: List<app.revanced.manager.network.downloader.LoadedDownloader>
|
||||
) = downloaderList.filter { downloader ->
|
||||
(source.packageName == null || downloader.packageName == source.packageName) &&
|
||||
(source.className == null || downloader.className == source.className)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
/**
|
||||
* Returns a copy with all nonexistent options removed.
|
||||
@@ -342,9 +353,7 @@ private sealed interface SelectionState : Parcelable {
|
||||
@Parcelize
|
||||
data class Customized(val patchSelection: PatchSelection) : SelectionState {
|
||||
override fun patches(bundles: List<PatchBundleInfo.Scoped>, allowIncompatible: Boolean) =
|
||||
bundles.toPatchSelection(
|
||||
allowIncompatible
|
||||
) { uid, patch ->
|
||||
bundles.toPatchSelection(allowIncompatible) { uid, patch ->
|
||||
patchSelection[uid]?.contains(patch.name) ?: false
|
||||
}
|
||||
}
|
||||
@@ -355,4 +364,3 @@ private sealed interface SelectionState : Parcelable {
|
||||
bundles.toPatchSelection(allowIncompatible) { _, patch -> patch.include }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||
import app.revanced.manager.domain.installer.RootInstaller
|
||||
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
||||
import app.revanced.manager.domain.repository.DownloaderRepository
|
||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||
import app.revanced.manager.network.downloader.DownloaderPackageState
|
||||
import app.revanced.manager.ui.model.SelectedSource
|
||||
import app.revanced.manager.ui.model.navigation.SelectedAppInfo
|
||||
import app.revanced.manager.util.PM
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import java.io.File
|
||||
|
||||
class SourceSelectorViewModel(
|
||||
val input: SelectedAppInfo.SourceSelector.ViewModelParams
|
||||
) : ViewModel(), KoinComponent {
|
||||
private val downloadedAppRepository: DownloadedAppRepository = get()
|
||||
private val downloaderRepository: DownloaderRepository = get()
|
||||
private val installedAppRepository: InstalledAppRepository = get()
|
||||
private val rootInstaller: RootInstaller = get()
|
||||
private val pm: PM = get()
|
||||
|
||||
var selectedSource by mutableStateOf(input.selectedSource)
|
||||
private set
|
||||
|
||||
var localApp by mutableStateOf<SourceOption?>(null)
|
||||
private set
|
||||
|
||||
var installedSource by mutableStateOf<SourceOption?>(null)
|
||||
private set
|
||||
|
||||
fun selectSource(source: SelectedSource) {
|
||||
selectedSource = source
|
||||
}
|
||||
|
||||
private fun versionMismatchReason(version: String?) =
|
||||
DisableReason.VERSION_NOT_MATCHING.takeIf { input.version != null && version != input.version }
|
||||
|
||||
val downloadedApps = downloadedAppRepository.get(input.packageName)
|
||||
.map { apps ->
|
||||
apps.sortedByDescending { it.version }.map { app ->
|
||||
SourceOption(
|
||||
source = SelectedSource.Downloaded(
|
||||
path = downloadedAppRepository.getApkFileForApp(app).path,
|
||||
version = app.version
|
||||
),
|
||||
title = app.version,
|
||||
key = app.version,
|
||||
disableReason = versionMismatchReason(app.version)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val downloaderSections = downloaderRepository.downloaderPackageStates.map { packageStates ->
|
||||
packageStates
|
||||
.filter { (_, state) -> state !is DownloaderPackageState.Loaded || state.downloaders.isNotEmpty() }
|
||||
.map { (packageName, state) ->
|
||||
when (state) {
|
||||
is DownloaderPackageState.Loaded -> DownloaderSection(
|
||||
title = state.name.ifBlank { packageName },
|
||||
key = packageName,
|
||||
options = state.downloaders.map { downloader ->
|
||||
SourceOption(
|
||||
source = SelectedSource.Downloader(
|
||||
packageName,
|
||||
downloader.className
|
||||
),
|
||||
title = downloader.name,
|
||||
key = "${packageName}:${downloader.className}",
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
DownloaderPackageState.Untrusted,
|
||||
is DownloaderPackageState.Failed -> {
|
||||
val title =
|
||||
with(pm) { pm.getPackageInfo(packageName)?.label() ?: packageName }
|
||||
DownloaderSection(
|
||||
title = title,
|
||||
key = "unavailable_$packageName",
|
||||
options = listOf(
|
||||
SourceOption(
|
||||
source = SelectedSource.Downloader(packageName),
|
||||
title = title,
|
||||
key = "unavailable_$packageName",
|
||||
disableReason = when (state) {
|
||||
DownloaderPackageState.Untrusted -> DisableReason.NOT_TRUSTED
|
||||
else -> DisableReason.FAILED_TO_LOAD
|
||||
},
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
pm.getPackageInfo(input.packageName)?.let { packageInfo ->
|
||||
val installedApp = installedAppRepository.get(input.packageName)
|
||||
|
||||
installedSource = SourceOption(
|
||||
source = SelectedSource.Installed,
|
||||
title = packageInfo.versionName.toString(),
|
||||
key = input.packageName,
|
||||
disableReason = when {
|
||||
installedApp?.installType == InstallType.DEFAULT -> DisableReason.ALREADY_PATCHED
|
||||
installedApp?.installType == InstallType.MOUNT && !rootInstaller.hasRootAccess() ->
|
||||
DisableReason.NO_ROOT
|
||||
|
||||
else -> versionMismatchReason(packageInfo.versionName)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
input.localPath?.let { local ->
|
||||
pm.getPackageInfo(File(local))?.let { packageInfo ->
|
||||
localApp = SourceOption(
|
||||
source = SelectedSource.Local(local),
|
||||
title = packageInfo.versionName.toString(),
|
||||
key = "local",
|
||||
disableReason = versionMismatchReason(packageInfo.versionName)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class DisableReason(@param:StringRes val message: Int) {
|
||||
VERSION_NOT_MATCHING(R.string.source_selector_disable_reason_version_not_matching),
|
||||
ALREADY_PATCHED(R.string.already_patched),
|
||||
NO_ROOT(R.string.app_source_dialog_option_installed_no_root),
|
||||
NOT_TRUSTED(R.string.source_selector_disable_reason_not_trusted),
|
||||
FAILED_TO_LOAD(R.string.source_selector_disable_reason_failed_to_load),
|
||||
}
|
||||
|
||||
data class SourceOption(
|
||||
val source: SelectedSource,
|
||||
val title: String,
|
||||
val key: String,
|
||||
val disableReason: DisableReason? = null
|
||||
)
|
||||
|
||||
data class DownloaderSection(
|
||||
val title: String,
|
||||
val key: String,
|
||||
val options: List<SourceOption>,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.ui.model.SelectedVersion
|
||||
import app.revanced.manager.ui.model.navigation.SelectedAppInfo
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.patchCount
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import java.io.File
|
||||
|
||||
class VersionSelectorViewModel(
|
||||
val input: SelectedAppInfo.VersionSelector.ViewModelParams
|
||||
) : ViewModel(), KoinComponent {
|
||||
private val patchBundleRepository: PatchBundleRepository = get()
|
||||
private val downloadedAppsRepository: DownloadedAppRepository = get()
|
||||
private val installedAppRepository: InstalledAppRepository = get()
|
||||
private val pm: PM = get()
|
||||
|
||||
val patchCount = input.patchSelection.patchCount
|
||||
|
||||
var selectedVersion by mutableStateOf(input.selectedVersion)
|
||||
private set
|
||||
|
||||
var installedAppVersion by mutableStateOf<String?>(null)
|
||||
private set
|
||||
|
||||
fun selectVersion(version: SelectedVersion) {
|
||||
selectedVersion = version
|
||||
}
|
||||
|
||||
private val _localVersion = MutableStateFlow<String?>(null)
|
||||
val localVersion: StateFlow<String?> = _localVersion
|
||||
|
||||
val downloadedVersions = downloadedAppsRepository.get(input.packageName)
|
||||
.map { apps -> apps.map { it.version } }
|
||||
|
||||
val availableVersions = combine(
|
||||
patchBundleRepository.suggestedVersions(input.packageName, input.patchSelection),
|
||||
_localVersion,
|
||||
) { versions, local ->
|
||||
val allVersions = versions.orEmpty().toMutableMap().apply {
|
||||
if (local != null) putIfAbsent(local, 0)
|
||||
}
|
||||
|
||||
allVersions
|
||||
.map { (version, supported) -> SelectedVersion.Specific(version) to patchCount - supported }
|
||||
.sortedWith(compareBy<Pair<SelectedVersion.Specific, Int>> { it.second }.thenByDescending { it.first.version })
|
||||
}
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
val patchedApp = installedAppRepository.get(input.packageName)
|
||||
if (patchedApp?.installType != InstallType.DEFAULT) {
|
||||
installedAppVersion = pm.getPackageInfo(input.packageName)?.versionName
|
||||
}
|
||||
|
||||
input.localPath?.let { local ->
|
||||
_localVersion.value = pm.getPackageInfo(File(local))?.versionName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,9 @@ import kotlin.time.Clock
|
||||
typealias PatchSelection = Map<Int, Set<String>>
|
||||
typealias Options = Map<Int, Map<String, Map<String, Any?>>>
|
||||
|
||||
val PatchSelection.patchCount
|
||||
get() = this.values.sumOf { it.size }
|
||||
|
||||
val Context.isDebuggable get() = 0 != applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
|
||||
|
||||
fun Context.openUrl(url: String) {
|
||||
|
||||
@@ -53,22 +53,45 @@ Second \"item\" text"</string>
|
||||
<string name="no_network_toast">No internet connection available</string>
|
||||
|
||||
<string name="selected_app_meta_any_version">Any available version</string>
|
||||
<string name="selected_app_meta_auto">%1$s (%2$s)</string>
|
||||
<string name="selected_app_meta_source_installed_apk">Installed APK</string>
|
||||
<string name="selected_app_meta_source_downloaded_apk">Downloaded APK</string>
|
||||
<string name="selected_app_meta_source_local_apk">Local APK</string>
|
||||
<string name="selected_app_meta_source_any_downloader">Any available downloader</string>
|
||||
<string name="version_selector_title">Select version</string>
|
||||
<string name="version_selector_auto_title">Auto (Recommended)</string>
|
||||
<string name="version_selector_auto_description">Automatically select the best available version</string>
|
||||
<string name="version_selector_all_patches_compatible">All patches are compatible</string>
|
||||
<string name="version_selector_state_local">Local</string>
|
||||
<string name="version_selector_state_downloaded_installed">Downloaded, Installed</string>
|
||||
<string name="version_selector_state_downloaded">Downloaded</string>
|
||||
<string name="version_selector_state_installed">Installed</string>
|
||||
<string name="version_selector_any_title">Any available version</string>
|
||||
<string name="version_selector_any_description">Use any available version regardless of compatibility</string>
|
||||
<string name="app_source_dialog_title">Select source</string>
|
||||
<string name="app_source_dialog_option_auto">Auto</string>
|
||||
<string name="app_source_dialog_option_auto_description">Use all available downloader to download the app</string>
|
||||
<string name="app_source_dialog_option_auto_unavailable">No downloader available</string>
|
||||
<string name="app_source_dialog_option_installed_no_root">Mounted apps cannot be patched again without root access</string>
|
||||
<string name="app_source_dialog_option_installed_version_not_suggested">Version %s does not match the suggested version</string>
|
||||
<string name="source_selector_auto_description">Automatically select the best available source</string>
|
||||
<string name="source_selector_any_available_downloader">Any available downloader</string>
|
||||
<string name="source_selector_category_downloaded">Downloaded</string>
|
||||
<string name="source_selector_category_local">Local</string>
|
||||
<string name="source_selector_disable_reason_version_not_matching">Does not match the selected version</string>
|
||||
<string name="source_selector_disable_reason_not_trusted">Not trusted</string>
|
||||
<string name="source_selector_disable_reason_failed_to_load">Failed to load</string>
|
||||
|
||||
<string name="patch_item_description">Start patching the application</string>
|
||||
<string name="patch_selector_item">Select patches</string>
|
||||
<string name="patch_selector_item_description">%d patches selected</string>
|
||||
<string name="patch_selector_item">Patches</string>
|
||||
<string name="patch_selector_item_description">%d selected</string>
|
||||
<string name="no_patches_selected">No patches selected</string>
|
||||
<string name="version_selector_item">Version</string>
|
||||
|
||||
<string name="network_unavailable_warning">Your device is not connected to the internet. Downloading will fail later.</string>
|
||||
<string name="network_metered_warning">You are currently on a metered connection. Data charges from your service provider may apply.</string>
|
||||
|
||||
<string name="apk_source_selector_item">Select APK source</string>
|
||||
<string name="apk_source_selector_item">APK source</string>
|
||||
<string name="apk_source_auto">Using all APK downloaders</string>
|
||||
<string name="apk_source_downloader">Using %s</string>
|
||||
<string name="apk_source_installed">Using installed APK</string>
|
||||
@@ -208,7 +231,7 @@ You will not be able to update the previously installed apps from this source.</
|
||||
<string name="share">Share</string>
|
||||
<string name="patch">Patch</string>
|
||||
<string name="select_from_storage">Select from storage</string>
|
||||
<string name="select_from_storage_description">Select an APK file from storage using file picker</string>
|
||||
<string name="select_from_storage_description">Select an APK file from storage</string>
|
||||
<string name="suggested_version_info">Suggested version: %s</string>
|
||||
<string name="type_anything">Type anything to continue</string>
|
||||
<string name="search">Search patches…</string>
|
||||
@@ -528,4 +551,12 @@ Click on the patches to see more details.</string>
|
||||
<plurals name="selected_count">
|
||||
<item quantity="other">%d selected</item>
|
||||
</plurals>
|
||||
<plurals name="version_selector_incompatible_patches">
|
||||
<item quantity="one">%d incompatible patch</item>
|
||||
<item quantity="other">%d incompatible patches</item>
|
||||
</plurals>
|
||||
<plurals name="available_patch_count">
|
||||
<item quantity="one">%d patch available</item>
|
||||
<item quantity="other">%d patches available</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user