mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2026-01-11 21:56:17 +00:00
Compare commits
1 Commits
feat/impro
...
feat/prere
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5b24b6d5b |
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,7 +1,3 @@
|
||||
name: ⭐ Feature request
|
||||
description: Create a detailed request for a new feature.
|
||||
title: 'feat: '
|
||||
labels: ['Feature request']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
110
app/CHANGELOG.md
110
app/CHANGELOG.md
@@ -1,113 +1,3 @@
|
||||
# app [1.26.0-dev.16](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.15...v1.26.0-dev.16) (2025-12-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Show patches as individual steps in patcher screen ([#2889](https://github.com/ReVanced/revanced-manager/issues/2889)) ([11dd6e4](https://github.com/ReVanced/revanced-manager/commit/11dd6e4064099427a8c9bc6f225a19412e5c70e2))
|
||||
|
||||
# app [1.26.0-dev.15](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.14...v1.26.0-dev.15) (2025-12-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* install dialog getting stuck ([#2900](https://github.com/ReVanced/revanced-manager/issues/2900)) ([18a4df9](https://github.com/ReVanced/revanced-manager/commit/18a4df9af9cac120fdb8e4ff7aadd2e2a8d5c1a6))
|
||||
|
||||
# app [1.26.0-dev.14](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.13...v1.26.0-dev.14) (2025-12-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Update selected patch count when SelectionState changes ([#2896](https://github.com/ReVanced/revanced-manager/issues/2896)) ([0d26df0](https://github.com/ReVanced/revanced-manager/commit/0d26df03f463195dae550240c7f652680763079c))
|
||||
|
||||
# app [1.26.0-dev.13](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.12...v1.26.0-dev.13) (2025-12-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Make patcher screen design more consistent with inspiration ([#2805](https://github.com/ReVanced/revanced-manager/issues/2805)) ([dbb6c01](https://github.com/ReVanced/revanced-manager/commit/dbb6c01e89a5e710185ff4304de0ac9e19bed053))
|
||||
|
||||
# app [1.26.0-dev.12](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.11...v1.26.0-dev.12) (2025-12-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Improve trust plugin dialog design ([#2420](https://github.com/ReVanced/revanced-manager/issues/2420)) ([0300da9](https://github.com/ReVanced/revanced-manager/commit/0300da9eac6c0fc29dbbb66622c0d52f4cf68934))
|
||||
|
||||
# app [1.26.0-dev.11](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.10...v1.26.0-dev.11) (2025-10-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add pure black theme ([#2824](https://github.com/ReVanced/revanced-manager/issues/2824)) ([3d75ffe](https://github.com/ReVanced/revanced-manager/commit/3d75ffe6a7a39efdebe13dbd07c937c1de409ead))
|
||||
|
||||
# app [1.26.0-dev.10](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.9...v1.26.0-dev.10) (2025-10-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* prevent back presses during installation ([2ff7072](https://github.com/ReVanced/revanced-manager/commit/2ff70728b490b92f212a82dcf599bc0c23f589e7))
|
||||
|
||||
# app [1.26.0-dev.9](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.8...v1.26.0-dev.9) (2025-10-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Instantly re-fetch patch bundle on pre-release preference update ([d5671db](https://github.com/ReVanced/revanced-manager/commit/d5671db3a77541c07bbbb4c3baca02f3ba0703f2)), closes [#2784](https://github.com/ReVanced/revanced-manager/issues/2784)
|
||||
|
||||
# app [1.26.0-dev.8](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.7...v1.26.0-dev.8) (2025-10-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Offcenter loading indicator in AppSelector ([12d92ba](https://github.com/ReVanced/revanced-manager/commit/12d92ba8110f5d1ac78aeecfa575444b5c53f561))
|
||||
|
||||
# app [1.26.0-dev.7](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.6...v1.26.0-dev.7) (2025-10-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Improve consistency between pre-release toggles ([e1b768c](https://github.com/ReVanced/revanced-manager/commit/e1b768c4679ecae8bff8007bdab56ff6544b12b6))
|
||||
|
||||
# app [1.26.0-dev.6](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.5...v1.26.0-dev.6) (2025-10-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Broken version comparison ([c327857](https://github.com/ReVanced/revanced-manager/commit/c3278578237dcddd9e7ab79ee80a02fdeef9604d))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Open contributor's GitHub profile when clicked ([#2775](https://github.com/ReVanced/revanced-manager/issues/2775)) ([2571cb8](https://github.com/ReVanced/revanced-manager/commit/2571cb8c1108e9c1ed84950f17692c09d66e0556))
|
||||
|
||||
# app [1.26.0-dev.5](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.4...v1.26.0-dev.5) (2025-10-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Toggle to use pre-release versions of ReVanced Patches ([08cec67](https://github.com/ReVanced/revanced-manager/commit/08cec674bbbe5297090ac5ee6039569975fbe9e7))
|
||||
|
||||
# app [1.26.0-dev.4](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.3...v1.26.0-dev.4) (2025-10-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add newlines to debug logs ([4753873](https://github.com/ReVanced/revanced-manager/commit/4753873866b575e2dcb160020df63f63862c8f33))
|
||||
|
||||
# app [1.26.0-dev.3](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.2...v1.26.0-dev.3) (2025-10-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Toggle to use pre-release versions of ReVanced Manager ([#2773](https://github.com/ReVanced/revanced-manager/issues/2773)) ([d758964](https://github.com/ReVanced/revanced-manager/commit/d7589647426b3d3438161a2f0b59bf4f154ac34b))
|
||||
|
||||
# app [1.26.0-dev.2](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.1...v1.26.0-dev.2) (2025-10-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Migration of keystore, by fixing mislabeling of alias as cn ([#2769](https://github.com/ReVanced/revanced-manager/issues/2769)) ([aeab639](https://github.com/ReVanced/revanced-manager/commit/aeab639b2b09e8bbd2478cfbf5a518586405c0f7))
|
||||
|
||||
# app [1.26.0-dev.1](https://github.com/ReVanced/revanced-manager/compare/v1.25.1...v1.26.0-dev.1) (2025-10-02)
|
||||
|
||||
|
||||
|
||||
@@ -108,10 +108,6 @@ dependencies {
|
||||
|
||||
// Compose Icons
|
||||
implementation(libs.compose.icons.fontawesome)
|
||||
|
||||
// Ackpine
|
||||
implementation(libs.ackpine.core)
|
||||
implementation(libs.ackpine.ktx)
|
||||
}
|
||||
|
||||
buildscript {
|
||||
|
||||
@@ -1 +1 @@
|
||||
version = 1.26.0-dev.16
|
||||
version = 1.26.0-dev.1
|
||||
|
||||
@@ -51,6 +51,9 @@
|
||||
|
||||
<activity android:name=".plugin.downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" />
|
||||
|
||||
<service android:name=".service.InstallService" />
|
||||
<service android:name=".service.UninstallService" />
|
||||
|
||||
<service
|
||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||
android:foregroundServiceType="specialUse"
|
||||
@@ -72,15 +75,5 @@
|
||||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false"
|
||||
tools:node="merge">
|
||||
<meta-data
|
||||
android:name="ru.solrudev.ackpine.AckpineInitializer"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -1,4 +0,0 @@
|
||||
// ProgressEventParcel.aidl
|
||||
package app.revanced.manager.patcher;
|
||||
|
||||
parcelable ProgressEventParcel;
|
||||
@@ -1,12 +1,11 @@
|
||||
// IPatcherEvents.aidl
|
||||
package app.revanced.manager.patcher.runtime.process;
|
||||
|
||||
import app.revanced.manager.patcher.ProgressEventParcel;
|
||||
|
||||
// Interface for sending events back to the main app process.
|
||||
oneway interface IPatcherEvents {
|
||||
void log(String level, String msg);
|
||||
void event(in ProgressEventParcel event);
|
||||
void patchSucceeded();
|
||||
void progress(String name, String state, String msg);
|
||||
// The patching process has ended. The exceptionStackTrace is null if it finished successfully.
|
||||
void finished(String exceptionStackTrace);
|
||||
}
|
||||
@@ -30,7 +30,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.SelectedAppInfo
|
||||
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
||||
import app.revanced.manager.ui.model.navigation.Settings
|
||||
import app.revanced.manager.ui.model.navigation.Update
|
||||
import app.revanced.manager.ui.screen.AppSelectorScreen
|
||||
@@ -41,9 +41,7 @@ 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
|
||||
@@ -83,7 +81,6 @@ class MainActivity : ComponentActivity() {
|
||||
)
|
||||
val theme by vm.prefs.theme.getAsState()
|
||||
val dynamicColor by vm.prefs.dynamicColor.getAsState()
|
||||
val pureBlackTheme by vm.prefs.pureBlackTheme.getAsState()
|
||||
|
||||
EventEffect(vm.legacyImportActivityFlow) {
|
||||
try {
|
||||
@@ -94,19 +91,25 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
ReVancedManagerTheme(
|
||||
darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK,
|
||||
dynamicColor = dynamicColor,
|
||||
pureBlackTheme = pureBlackTheme
|
||||
dynamicColor = dynamicColor
|
||||
) {
|
||||
ReVancedManager()
|
||||
ReVancedManager(vm)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReVancedManager() {
|
||||
private fun ReVancedManager(vm: MainViewModel) {
|
||||
val navController = rememberNavController()
|
||||
|
||||
EventEffect(vm.appSelectFlow) { app ->
|
||||
navController.navigateComplex(
|
||||
SelectedApplicationInfo,
|
||||
SelectedApplicationInfo.ViewModelParams(app)
|
||||
)
|
||||
}
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Dashboard,
|
||||
@@ -137,12 +140,7 @@ private fun ReVancedManager() {
|
||||
val data = it.toRoute<InstalledApplicationInfo>()
|
||||
|
||||
InstalledAppInfoScreen(
|
||||
onPatchClick = { packageName ->
|
||||
navController.navigateComplex(
|
||||
SelectedAppInfo,
|
||||
SelectedAppInfo.ViewModelParams(packageName)
|
||||
)
|
||||
},
|
||||
onPatchClick = vm::selectApp,
|
||||
onBackClick = navController::popBackStack,
|
||||
viewModel = koinViewModel { parametersOf(data.packageName) }
|
||||
)
|
||||
@@ -150,20 +148,8 @@ private fun ReVancedManager() {
|
||||
|
||||
composable<AppSelector> {
|
||||
AppSelectorScreen(
|
||||
onSelect = { packageName ->
|
||||
navController.navigateComplex(
|
||||
SelectedAppInfo,
|
||||
SelectedAppInfo.ViewModelParams(packageName)
|
||||
)
|
||||
},
|
||||
onStorageSelect = { packageName, localPath ->
|
||||
navController.navigateComplex(
|
||||
SelectedAppInfo,
|
||||
SelectedAppInfo.ViewModelParams(
|
||||
packageName, localPath
|
||||
)
|
||||
)
|
||||
},
|
||||
onSelect = vm::selectApp,
|
||||
onStorageSelect = vm::selectApp,
|
||||
onBackClick = navController::popBackStack
|
||||
)
|
||||
}
|
||||
@@ -191,11 +177,11 @@ private fun ReVancedManager() {
|
||||
)
|
||||
}
|
||||
|
||||
navigation<SelectedAppInfo>(startDestination = SelectedAppInfo.Main) {
|
||||
composable<SelectedAppInfo.Main> {
|
||||
navigation<SelectedApplicationInfo>(startDestination = SelectedApplicationInfo.Main) {
|
||||
composable<SelectedApplicationInfo.Main> {
|
||||
val parentBackStackEntry = navController.navGraphEntry(it)
|
||||
val data =
|
||||
parentBackStackEntry.getComplexArg<SelectedAppInfo.ViewModelParams>()
|
||||
parentBackStackEntry.getComplexArg<SelectedApplicationInfo.ViewModelParams>()
|
||||
val viewModel =
|
||||
koinNavViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
|
||||
parametersOf(data)
|
||||
@@ -211,47 +197,23 @@ private fun ReVancedManager() {
|
||||
)
|
||||
}
|
||||
},
|
||||
onPatchSelectorClick = { packageName, version, patchSelection, options ->
|
||||
onPatchSelectorClick = { app, patches, options ->
|
||||
navController.navigateComplex(
|
||||
SelectedAppInfo.PatchesSelector,
|
||||
SelectedAppInfo.PatchesSelector.ViewModelParams(
|
||||
packageName,
|
||||
version,
|
||||
patchSelection,
|
||||
options,
|
||||
SelectedApplicationInfo.PatchesSelector,
|
||||
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
|
||||
app,
|
||||
patches,
|
||||
options
|
||||
)
|
||||
)
|
||||
},
|
||||
onRequiredOptions = { packageName, version, patchSelection, options ->
|
||||
onRequiredOptions = { app, patches, options ->
|
||||
navController.navigateComplex(
|
||||
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,
|
||||
SelectedApplicationInfo.RequiredOptions,
|
||||
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
|
||||
app,
|
||||
patches,
|
||||
options
|
||||
)
|
||||
)
|
||||
},
|
||||
@@ -259,9 +221,9 @@ private fun ReVancedManager() {
|
||||
)
|
||||
}
|
||||
|
||||
composable<SelectedAppInfo.PatchesSelector> {
|
||||
composable<SelectedApplicationInfo.PatchesSelector> {
|
||||
val data =
|
||||
it.getComplexArg<SelectedAppInfo.PatchesSelector.ViewModelParams>()
|
||||
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
||||
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
||||
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||
)
|
||||
@@ -276,43 +238,9 @@ private fun ReVancedManager() {
|
||||
)
|
||||
}
|
||||
|
||||
composable<SelectedAppInfo.VersionSelector> {
|
||||
composable<SelectedApplicationInfo.RequiredOptions> {
|
||||
val data =
|
||||
it.getComplexArg<SelectedAppInfo.VersionSelector.ViewModelParams>()
|
||||
val selectedAppInfoVm = koinNavViewModel<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 = koinNavViewModel<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>()
|
||||
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
||||
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
||||
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||
)
|
||||
|
||||
@@ -48,8 +48,7 @@ class ManagerApplication : Application() {
|
||||
workerModule,
|
||||
viewModelModule,
|
||||
databaseModule,
|
||||
rootModule,
|
||||
ackpineModule
|
||||
rootModule
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ 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
|
||||
@@ -11,9 +12,6 @@ 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?
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package app.revanced.manager.di
|
||||
|
||||
import android.content.Context
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
import ru.solrudev.ackpine.installer.PackageInstaller
|
||||
import ru.solrudev.ackpine.uninstaller.PackageUninstaller
|
||||
|
||||
val ackpineModule = module {
|
||||
fun provideInstaller(context: Context) = PackageInstaller.getInstance(context)
|
||||
fun provideUninstaller(context: Context) = PackageUninstaller.getInstance(context)
|
||||
|
||||
single {
|
||||
provideInstaller(androidContext())
|
||||
}
|
||||
single {
|
||||
provideUninstaller(androidContext())
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,4 @@ val viewModelModule = module {
|
||||
viewModelOf(::InstalledAppInfoViewModel)
|
||||
viewModelOf(::UpdatesSettingsViewModel)
|
||||
viewModelOf(::BundleListViewModel)
|
||||
viewModelOf(::VersionSelectorViewModel)
|
||||
viewModelOf(::SourceSelectorViewModel)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ class PreferencesManager(
|
||||
context: Context
|
||||
) : BasePreferencesManager(context, "settings") {
|
||||
val dynamicColor = booleanPreference("dynamic_color", true)
|
||||
val pureBlackTheme = booleanPreference("pure_black_theme", false)
|
||||
val theme = enumPreference("theme", Theme.SYSTEM)
|
||||
|
||||
val api = stringPreference("api_url", "https://api.revanced.app")
|
||||
@@ -24,7 +23,6 @@ class PreferencesManager(
|
||||
val managerAutoUpdates = booleanPreference("manager_auto_updates", false)
|
||||
val showManagerUpdateDialogOnLaunch = booleanPreference("show_manager_update_dialog_on_launch", true)
|
||||
val useManagerPrereleases = booleanPreference("manager_prereleases", false)
|
||||
val usePatchesPrereleases = booleanPreference("patches_prereleases", false)
|
||||
|
||||
val disablePatchVersionCompatCheck = booleanPreference("disable_patch_version_compatibility_check", false)
|
||||
val disableSelectionWarning = booleanPreference("disable_selection_warning", false)
|
||||
|
||||
@@ -30,8 +30,6 @@ 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,7 +26,6 @@ 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,17 +74,6 @@ 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): app.revanced.manager.util.PatchSelection =
|
||||
suspend fun getSelection(packageName: String): Map<Int, Set<String>> =
|
||||
dao.getSelectedPatches(packageName).mapValues { it.value.toSet() }
|
||||
|
||||
suspend fun updateSelection(packageName: String, selection: Map<Int, Set<String>>) =
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package app.revanced.manager.network.api
|
||||
|
||||
import app.revanced.manager.BuildConfig
|
||||
import android.os.Build
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.network.dto.ReVancedAsset
|
||||
import app.revanced.manager.network.dto.ReVancedGitRepository
|
||||
@@ -30,12 +30,12 @@ class ReVancedAPI(
|
||||
private suspend inline fun <reified T> request(route: String) = request<T>(apiUrl(), route)
|
||||
|
||||
suspend fun getAppUpdate() =
|
||||
getLatestAppInfo().getOrThrow().takeIf { it.version.removePrefix("v") != BuildConfig.VERSION_NAME }
|
||||
getLatestAppInfo().getOrThrow().takeIf { it.version != Build.VERSION.RELEASE }
|
||||
|
||||
suspend fun getLatestAppInfo() =
|
||||
request<ReVancedAsset>("manager?prerelease=${prefs.useManagerPrereleases.get()}")
|
||||
|
||||
suspend fun getPatchesUpdate() = request<ReVancedAsset>("patches?prerelease=${prefs.usePatchesPrereleases.get()}")
|
||||
suspend fun getPatchesUpdate() = request<ReVancedAsset>("patches")
|
||||
|
||||
suspend fun getContributors() = request<List<ReVancedGitRepository>>("contributors")
|
||||
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
package app.revanced.manager.patcher
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
|
||||
@Parcelize
|
||||
sealed class ProgressEvent : Parcelable {
|
||||
abstract val stepId: StepId?
|
||||
|
||||
data class Started(override val stepId: StepId) : ProgressEvent()
|
||||
|
||||
data class Progress(
|
||||
override val stepId: StepId,
|
||||
val current: Long? = null,
|
||||
val total: Long? = null,
|
||||
val message: String? = null,
|
||||
) : ProgressEvent()
|
||||
|
||||
data class Completed(
|
||||
override val stepId: StepId,
|
||||
) : ProgressEvent()
|
||||
|
||||
data class Failed(
|
||||
override val stepId: StepId?,
|
||||
val error: RemoteError,
|
||||
) : ProgressEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parcelable wrapper for [ProgressEvent].
|
||||
*
|
||||
* Required because AIDL does not support sealed classes.
|
||||
*/
|
||||
@Parcelize
|
||||
data class ProgressEventParcel(val event: ProgressEvent) : Parcelable
|
||||
|
||||
fun ProgressEventParcel.toEvent(): ProgressEvent = event
|
||||
fun ProgressEvent.toParcel(): ProgressEventParcel = ProgressEventParcel(this)
|
||||
|
||||
@Parcelize
|
||||
sealed class StepId : Parcelable {
|
||||
data object DownloadAPK : StepId()
|
||||
data object LoadPatches : StepId()
|
||||
data object ReadAPK : StepId()
|
||||
data object ExecutePatches : StepId()
|
||||
data class ExecutePatch(val index: Int) : StepId()
|
||||
data object WriteAPK : StepId()
|
||||
data object SignAPK : StepId()
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class RemoteError(
|
||||
val type: String,
|
||||
val message: String?,
|
||||
val stackTrace: String,
|
||||
) : Parcelable
|
||||
|
||||
fun Exception.toRemoteError() = RemoteError(
|
||||
type = this::class.java.name,
|
||||
message = this.message,
|
||||
stackTrace = this.stackTraceToString(),
|
||||
)
|
||||
|
||||
|
||||
inline fun <T> runStep(
|
||||
stepId: StepId,
|
||||
onEvent: (ProgressEvent) -> Unit,
|
||||
block: () -> T,
|
||||
): T = try {
|
||||
onEvent(ProgressEvent.Started(stepId))
|
||||
val value = block()
|
||||
onEvent(ProgressEvent.Completed(stepId))
|
||||
value
|
||||
} catch (error: Exception) {
|
||||
onEvent(ProgressEvent.Failed(stepId, error.toRemoteError()))
|
||||
throw error
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
package app.revanced.manager.patcher
|
||||
|
||||
import android.content.Context
|
||||
import app.revanced.library.ApkUtils.applyTo
|
||||
import app.revanced.manager.patcher.Session.Companion.component1
|
||||
import app.revanced.manager.patcher.Session.Companion.component2
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.patcher.Patcher
|
||||
import app.revanced.patcher.PatcherConfig
|
||||
import app.revanced.patcher.patch.Patch
|
||||
@@ -21,10 +22,15 @@ class Session(
|
||||
cacheDir: String,
|
||||
frameworkDir: String,
|
||||
aaptPath: String,
|
||||
private val androidContext: Context,
|
||||
private val logger: Logger,
|
||||
private val input: File,
|
||||
private val onEvent: (ProgressEvent) -> Unit,
|
||||
private val onPatchCompleted: suspend () -> Unit,
|
||||
private val onProgress: (name: String?, state: State?, message: String?) -> Unit
|
||||
) : Closeable {
|
||||
private fun updateProgress(name: String? = null, state: State? = null, message: String? = null) =
|
||||
onProgress(name, state, message)
|
||||
|
||||
private val tempDir = File(cacheDir).resolve("patcher").also { it.mkdirs() }
|
||||
private val patcher = Patcher(
|
||||
PatcherConfig(
|
||||
@@ -36,68 +42,86 @@ class Session(
|
||||
)
|
||||
|
||||
private suspend fun Patcher.applyPatchesVerbose(selectedPatches: PatchList) {
|
||||
var nextPatchIndex = 0
|
||||
|
||||
updateProgress(
|
||||
name = androidContext.getString(R.string.executing_patch, selectedPatches[nextPatchIndex]),
|
||||
state = State.RUNNING
|
||||
)
|
||||
|
||||
this().collect { (patch, exception) ->
|
||||
val index = selectedPatches.indexOf(patch)
|
||||
if (index == -1) return@collect
|
||||
if (patch !in selectedPatches) return@collect
|
||||
|
||||
if (exception != null) {
|
||||
onEvent(
|
||||
ProgressEvent.Failed(
|
||||
StepId.ExecutePatch(index),
|
||||
exception.toRemoteError(),
|
||||
)
|
||||
updateProgress(
|
||||
name = androidContext.getString(R.string.failed_to_execute_patch, patch.name),
|
||||
state = State.FAILED,
|
||||
message = exception.stackTraceToString()
|
||||
)
|
||||
|
||||
logger.error("${patch.name} failed:")
|
||||
logger.error(exception.stackTraceToString())
|
||||
throw exception
|
||||
}
|
||||
|
||||
onEvent(
|
||||
ProgressEvent.Completed(
|
||||
StepId.ExecutePatch(index),
|
||||
nextPatchIndex++
|
||||
|
||||
onPatchCompleted()
|
||||
|
||||
selectedPatches.getOrNull(nextPatchIndex)?.let { nextPatch ->
|
||||
updateProgress(
|
||||
name = androidContext.getString(R.string.executing_patch, nextPatch.name)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
logger.info("${patch.name} succeeded")
|
||||
}
|
||||
|
||||
updateProgress(
|
||||
state = State.COMPLETED,
|
||||
name = androidContext.resources.getQuantityString(
|
||||
R.plurals.patches_executed,
|
||||
selectedPatches.size,
|
||||
selectedPatches.size
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun run(output: File, selectedPatches: PatchList) {
|
||||
runStep(StepId.ExecutePatches, onEvent) {
|
||||
java.util.logging.Logger.getLogger("").apply {
|
||||
handlers.forEach {
|
||||
it.close()
|
||||
removeHandler(it)
|
||||
}
|
||||
updateProgress(state = State.COMPLETED) // Unpacking
|
||||
|
||||
addHandler(logger.handler)
|
||||
java.util.logging.Logger.getLogger("").apply {
|
||||
handlers.forEach {
|
||||
it.close()
|
||||
removeHandler(it)
|
||||
}
|
||||
|
||||
with(patcher) {
|
||||
logger.info("Merging integrations")
|
||||
this += selectedPatches.toSet()
|
||||
|
||||
logger.info("Applying patches...")
|
||||
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
|
||||
}
|
||||
addHandler(logger.handler)
|
||||
}
|
||||
|
||||
runStep(StepId.WriteAPK, onEvent) {
|
||||
logger.info("Writing patched files...")
|
||||
val result = patcher.get()
|
||||
with(patcher) {
|
||||
logger.info("Merging integrations")
|
||||
this += selectedPatches.toSet()
|
||||
|
||||
val patched = tempDir.resolve("result.apk")
|
||||
withContext(Dispatchers.IO) {
|
||||
Files.copy(input.toPath(), patched.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
result.applyTo(patched)
|
||||
|
||||
logger.info("Patched apk saved to $patched")
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
Files.move(patched.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
logger.info("Applying patches...")
|
||||
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
|
||||
}
|
||||
|
||||
logger.info("Writing patched files...")
|
||||
val result = patcher.get()
|
||||
|
||||
val patched = tempDir.resolve("result.apk")
|
||||
withContext(Dispatchers.IO) {
|
||||
Files.copy(input.toPath(), patched.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
result.applyTo(patched)
|
||||
|
||||
logger.info("Patched apk saved to $patched")
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
Files.move(patched.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
updateProgress(state = State.COMPLETED) // Saving
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
|
||||
@@ -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,12 +1,11 @@
|
||||
package app.revanced.manager.patcher.runtime
|
||||
|
||||
import android.content.Context
|
||||
import app.revanced.manager.patcher.ProgressEvent
|
||||
import app.revanced.manager.patcher.Session
|
||||
import app.revanced.manager.patcher.StepId
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.patch.PatchBundle
|
||||
import app.revanced.manager.patcher.runStep
|
||||
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import java.io.File
|
||||
@@ -14,7 +13,7 @@ import java.io.File
|
||||
/**
|
||||
* Simple [Runtime] implementation that runs the patcher using coroutines.
|
||||
*/
|
||||
class CoroutineRuntime(context: Context) : Runtime(context) {
|
||||
class CoroutineRuntime(private val context: Context) : Runtime(context) {
|
||||
override suspend fun execute(
|
||||
inputFile: String,
|
||||
outputFile: String,
|
||||
@@ -22,50 +21,47 @@ class CoroutineRuntime(context: Context) : Runtime(context) {
|
||||
selectedPatches: PatchSelection,
|
||||
options: Options,
|
||||
logger: Logger,
|
||||
onEvent: (ProgressEvent) -> Unit,
|
||||
onPatchCompleted: suspend () -> Unit,
|
||||
onProgress: ProgressEventHandler,
|
||||
) {
|
||||
val patchList = runStep(StepId.LoadPatches, onEvent) {
|
||||
val selectedBundles = selectedPatches.keys
|
||||
val bundles = bundles()
|
||||
val uids = bundles.entries.associate { (key, value) -> value to key }
|
||||
val selectedBundles = selectedPatches.keys
|
||||
val bundles = bundles()
|
||||
val uids = bundles.entries.associate { (key, value) -> value to key }
|
||||
|
||||
val allPatches =
|
||||
PatchBundle.Loader.patches(bundles.values, packageName)
|
||||
.mapKeys { (b, _) -> uids[b]!! }
|
||||
.filterKeys { it in selectedBundles }
|
||||
val allPatches =
|
||||
PatchBundle.Loader.patches(bundles.values, packageName)
|
||||
.mapKeys { (b, _) -> uids[b]!! }
|
||||
.filterKeys { it in selectedBundles }
|
||||
|
||||
val patchList = selectedPatches.flatMap { (bundle, selected) ->
|
||||
allPatches[bundle]?.filter { it.name in selected }
|
||||
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
||||
}
|
||||
val patchList = selectedPatches.flatMap { (bundle, selected) ->
|
||||
allPatches[bundle]?.filter { it.name in selected }
|
||||
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
||||
}
|
||||
|
||||
// Set all patch options.
|
||||
options.forEach { (bundle, bundlePatchOptions) ->
|
||||
val patches = allPatches[bundle] ?: return@forEach
|
||||
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
|
||||
val patchOptions = patches.single { it.name == patchName }.options
|
||||
configuredPatchOptions.forEach { (key, value) ->
|
||||
patchOptions[key] = value
|
||||
}
|
||||
// Set all patch options.
|
||||
options.forEach { (bundle, bundlePatchOptions) ->
|
||||
val patches = allPatches[bundle] ?: return@forEach
|
||||
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
|
||||
val patchOptions = patches.single { it.name == patchName }.options
|
||||
configuredPatchOptions.forEach { (key, value) ->
|
||||
patchOptions[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
patchList
|
||||
}
|
||||
|
||||
val session = runStep(StepId.ReadAPK, onEvent) {
|
||||
Session(
|
||||
cacheDir,
|
||||
frameworkPath,
|
||||
aaptPath,
|
||||
logger,
|
||||
File(inputFile),
|
||||
onEvent,
|
||||
)
|
||||
}
|
||||
onProgress(null, State.COMPLETED, null) // Loading patches
|
||||
|
||||
session.use { s ->
|
||||
s.run(
|
||||
Session(
|
||||
cacheDir,
|
||||
frameworkPath,
|
||||
aaptPath,
|
||||
context,
|
||||
logger,
|
||||
File(inputFile),
|
||||
onPatchCompleted = onPatchCompleted,
|
||||
onProgress
|
||||
).use { session ->
|
||||
session.run(
|
||||
File(outputFile),
|
||||
patchList
|
||||
)
|
||||
|
||||
@@ -10,13 +10,12 @@ import app.revanced.manager.BuildConfig
|
||||
import app.revanced.manager.patcher.runtime.process.IPatcherEvents
|
||||
import app.revanced.manager.patcher.runtime.process.IPatcherProcess
|
||||
import app.revanced.manager.patcher.LibraryResolver
|
||||
import app.revanced.manager.patcher.ProgressEvent
|
||||
import app.revanced.manager.patcher.ProgressEventParcel
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.runtime.process.Parameters
|
||||
import app.revanced.manager.patcher.runtime.process.PatchConfiguration
|
||||
import app.revanced.manager.patcher.runtime.process.PatcherProcess
|
||||
import app.revanced.manager.patcher.toEvent
|
||||
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
@@ -67,7 +66,8 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
||||
selectedPatches: PatchSelection,
|
||||
options: Options,
|
||||
logger: Logger,
|
||||
onEvent: (ProgressEvent) -> Unit,
|
||||
onPatchCompleted: suspend () -> Unit,
|
||||
onProgress: ProgressEventHandler,
|
||||
) = coroutineScope {
|
||||
// Get the location of our own Apk.
|
||||
val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo!!.sourceDir
|
||||
@@ -111,6 +111,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
||||
}
|
||||
|
||||
val patching = CompletableDeferred<Unit>()
|
||||
val scope = this
|
||||
|
||||
launch(Dispatchers.IO) {
|
||||
val binder = awaitBinderConnection()
|
||||
@@ -123,10 +124,13 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
||||
val eventHandler = object : IPatcherEvents.Stub() {
|
||||
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg)
|
||||
|
||||
override fun event(event: ProgressEventParcel?) {
|
||||
event?.let { onEvent(it.toEvent()) }
|
||||
override fun patchSucceeded() {
|
||||
scope.launch { onPatchCompleted() }
|
||||
}
|
||||
|
||||
override fun progress(name: String?, state: String?, msg: String?) =
|
||||
onProgress(name, state?.let { enumValueOf<State>(it) }, msg)
|
||||
|
||||
override fun finished(exceptionStackTrace: String?) {
|
||||
binder.exit()
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import android.content.Context
|
||||
import app.revanced.manager.data.platform.Filesystem
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.patcher.ProgressEvent
|
||||
import app.revanced.manager.patcher.aapt.Aapt
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import kotlinx.coroutines.flow.first
|
||||
@@ -34,6 +34,7 @@ sealed class Runtime(context: Context) : KoinComponent {
|
||||
selectedPatches: PatchSelection,
|
||||
options: Options,
|
||||
logger: Logger,
|
||||
onEvent: (ProgressEvent) -> Unit,
|
||||
onPatchCompleted: suspend () -> Unit,
|
||||
onProgress: ProgressEventHandler,
|
||||
)
|
||||
}
|
||||
@@ -8,15 +8,12 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Looper
|
||||
import app.revanced.manager.BuildConfig
|
||||
import app.revanced.manager.patcher.ProgressEvent
|
||||
import app.revanced.manager.patcher.Session
|
||||
import app.revanced.manager.patcher.StepId
|
||||
import app.revanced.manager.patcher.logger.LogLevel
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.patch.PatchBundle
|
||||
import app.revanced.manager.patcher.runStep
|
||||
import app.revanced.manager.patcher.runtime.ProcessRuntime
|
||||
import app.revanced.manager.patcher.toParcel
|
||||
import app.revanced.manager.ui.model.State
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -27,7 +24,7 @@ import kotlin.system.exitProcess
|
||||
/**
|
||||
* The main class that runs inside the runner process launched by [ProcessRuntime].
|
||||
*/
|
||||
class PatcherProcess() : IPatcherProcess.Stub() {
|
||||
class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
||||
private var eventBinder: IPatcherEvents? = null
|
||||
|
||||
private val scope =
|
||||
@@ -49,8 +46,6 @@ class PatcherProcess() : IPatcherProcess.Stub() {
|
||||
override fun exit() = exitProcess(0)
|
||||
|
||||
override fun start(parameters: Parameters, events: IPatcherEvents) {
|
||||
fun onEvent(event: ProgressEvent) = events.event(event.toParcel())
|
||||
|
||||
eventBinder = events
|
||||
|
||||
scope.launch {
|
||||
@@ -61,42 +56,38 @@ class PatcherProcess() : IPatcherProcess.Stub() {
|
||||
|
||||
logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
|
||||
|
||||
val patchList = runStep(StepId.LoadPatches, ::onEvent) {
|
||||
val allPatches = PatchBundle.Loader.patches(
|
||||
parameters.configurations.map { it.bundle },
|
||||
parameters.packageName
|
||||
)
|
||||
|
||||
parameters.configurations.flatMap { config ->
|
||||
val patches = (allPatches[config.bundle] ?: return@flatMap emptyList())
|
||||
val allPatches = PatchBundle.Loader.patches(parameters.configurations.map { it.bundle }, parameters.packageName)
|
||||
val patchList = parameters.configurations.flatMap { config ->
|
||||
val patches = (allPatches[config.bundle] ?: return@flatMap emptyList())
|
||||
.filter { it.name in config.patches }
|
||||
.associateBy { it.name }
|
||||
|
||||
config.options.forEach { (patchName, opts) ->
|
||||
val patchOptions = patches[patchName]?.options
|
||||
?: throw Exception("Patch with name $patchName does not exist.")
|
||||
config.options.forEach { (patchName, opts) ->
|
||||
val patchOptions = patches[patchName]?.options
|
||||
?: throw Exception("Patch with name $patchName does not exist.")
|
||||
|
||||
opts.forEach { (key, value) ->
|
||||
patchOptions[key] = value
|
||||
}
|
||||
opts.forEach { (key, value) ->
|
||||
patchOptions[key] = value
|
||||
}
|
||||
|
||||
patches.values
|
||||
}
|
||||
|
||||
patches.values
|
||||
}
|
||||
|
||||
val session = runStep(StepId.ReadAPK, ::onEvent) {
|
||||
Session(
|
||||
cacheDir = parameters.cacheDir,
|
||||
aaptPath = parameters.aaptPath,
|
||||
frameworkDir = parameters.frameworkDir,
|
||||
logger = logger,
|
||||
input = File(parameters.inputFile),
|
||||
onEvent = ::onEvent,
|
||||
)
|
||||
}
|
||||
events.progress(null, State.COMPLETED.name, null) // Loading patches
|
||||
|
||||
session.use {
|
||||
Session(
|
||||
cacheDir = parameters.cacheDir,
|
||||
aaptPath = parameters.aaptPath,
|
||||
frameworkDir = parameters.frameworkDir,
|
||||
androidContext = context,
|
||||
logger = logger,
|
||||
input = File(parameters.inputFile),
|
||||
onPatchCompleted = { events.patchSucceeded() },
|
||||
onProgress = { name, state, message ->
|
||||
events.progress(name, state?.name, message)
|
||||
}
|
||||
).use {
|
||||
it.run(File(parameters.outputFile), patchList)
|
||||
}
|
||||
|
||||
@@ -128,7 +119,7 @@ class PatcherProcess() : IPatcherProcess.Stub() {
|
||||
}
|
||||
}
|
||||
|
||||
val ipcInterface = PatcherProcess()
|
||||
val ipcInterface = PatcherProcess(appContext)
|
||||
|
||||
appContext.sendBroadcast(Intent().apply {
|
||||
action = ProcessRuntime.CONNECT_TO_APP_ACTION
|
||||
|
||||
@@ -29,17 +29,14 @@ import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||
import app.revanced.manager.domain.worker.Worker
|
||||
import app.revanced.manager.domain.worker.WorkerRepository
|
||||
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||
import app.revanced.manager.patcher.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.plugin.downloader.GetScope
|
||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||
import app.revanced.manager.plugin.downloader.UserInteractionException
|
||||
import app.revanced.manager.ui.model.SelectedSource
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
@@ -51,6 +48,8 @@ import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.io.File
|
||||
|
||||
typealias ProgressEventHandler = (name: String?, state: State?, message: String?) -> Unit
|
||||
|
||||
@OptIn(PluginHostApi::class)
|
||||
class PatcherWorker(
|
||||
context: Context,
|
||||
@@ -67,17 +66,19 @@ class PatcherWorker(
|
||||
private val rootInstaller: RootInstaller by inject()
|
||||
|
||||
class Args(
|
||||
val packageName: String,
|
||||
val version: String?,
|
||||
val source: SelectedSource,
|
||||
val input: SelectedApp,
|
||||
val output: String,
|
||||
val selectedPatches: PatchSelection,
|
||||
val options: Options,
|
||||
val logger: Logger,
|
||||
val onDownloadProgress: suspend (Pair<Long, Long?>?) -> Unit,
|
||||
val onPatchCompleted: suspend () -> Unit,
|
||||
val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult,
|
||||
val setInputFile: suspend (File) -> Unit,
|
||||
val onEvent: (ProgressEvent) -> Unit,
|
||||
)
|
||||
val onProgress: ProgressEventHandler
|
||||
) {
|
||||
val packageName get() = input.packageName
|
||||
}
|
||||
|
||||
override suspend fun getForegroundInfo() =
|
||||
ForegroundInfo(
|
||||
@@ -139,10 +140,14 @@ class PatcherWorker(
|
||||
}
|
||||
|
||||
private suspend fun runPatcher(args: Args): Result {
|
||||
|
||||
fun updateProgress(name: String? = null, state: State? = null, message: String? = null) =
|
||||
args.onProgress(name, state, message)
|
||||
|
||||
val patchedApk = fs.tempDir.resolve("patched.apk")
|
||||
|
||||
return try {
|
||||
if (args.source is SelectedSource.Installed) {
|
||||
if (args.input is SelectedApp.Installed) {
|
||||
installedAppRepository.get(args.packageName)?.let {
|
||||
if (it.installType == InstallType.MOUNT) {
|
||||
rootInstaller.unmount(args.packageName)
|
||||
@@ -155,65 +160,58 @@ class PatcherWorker(
|
||||
plugin,
|
||||
data,
|
||||
args.packageName,
|
||||
args.version,
|
||||
args.input.version,
|
||||
prefs.suggestedVersionSafeguard.get(),
|
||||
!prefs.disablePatchVersionCompatCheck.get(),
|
||||
) { progress ->
|
||||
args.onEvent(
|
||||
ProgressEvent.Progress(
|
||||
stepId = StepId.DownloadAPK,
|
||||
current = progress.first,
|
||||
total = progress.second
|
||||
)
|
||||
)
|
||||
}.also { args.setInputFile(it) }
|
||||
|
||||
val inputFile = when (val source = args.source) {
|
||||
is SelectedSource.Auto -> throw Exception("Auto source is not supported in worker.")
|
||||
|
||||
is SelectedSource.Plugin -> {
|
||||
runStep(StepId.DownloadAPK, args.onEvent) {
|
||||
downloaderPluginRepository.loadedPluginsFlow.first()
|
||||
.firstNotNullOfOrNull { plugin ->
|
||||
try {
|
||||
val getScope = object : GetScope {
|
||||
override val pluginPackageName = plugin.packageName
|
||||
override val hostPackageName =
|
||||
applicationContext.packageName
|
||||
|
||||
override suspend fun requestStartActivity(intent: Intent): Intent? {
|
||||
val result =
|
||||
args.handleStartActivityRequest(plugin, intent)
|
||||
return when (result.resultCode) {
|
||||
Activity.RESULT_OK -> result.data
|
||||
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
|
||||
else -> throw UserInteractionException.Activity.NotCompleted(
|
||||
result.resultCode,
|
||||
result.data
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
plugin.get(
|
||||
getScope,
|
||||
args.packageName,
|
||||
args.version
|
||||
)
|
||||
}?.takeIf { (_, version) -> args.version == null || version == args.version }
|
||||
} catch (e: UserInteractionException.Activity.NotCompleted) {
|
||||
throw e
|
||||
} catch (_: UserInteractionException) {
|
||||
null
|
||||
}?.let { (data, _) -> download(plugin, data) }
|
||||
} ?: throw Exception("App is not available.")
|
||||
}
|
||||
onDownload = args.onDownloadProgress
|
||||
).also {
|
||||
args.setInputFile(it)
|
||||
updateProgress(state = State.COMPLETED) // Download APK
|
||||
}
|
||||
|
||||
is SelectedSource.Downloaded -> File(source.path)
|
||||
is SelectedSource.Local -> File(source.path)
|
||||
val inputFile = when (val selectedApp = args.input) {
|
||||
is SelectedApp.Download -> {
|
||||
val (plugin, data) = downloaderPluginRepository.unwrapParceledData(selectedApp.data)
|
||||
|
||||
is SelectedSource.Installed -> File(pm.getPackageInfo(args.packageName)!!.applicationInfo!!.sourceDir)
|
||||
download(plugin, data)
|
||||
}
|
||||
|
||||
is SelectedApp.Search -> {
|
||||
downloaderPluginRepository.loadedPluginsFlow.first()
|
||||
.firstNotNullOfOrNull { plugin ->
|
||||
try {
|
||||
val getScope = object : GetScope {
|
||||
override val pluginPackageName = plugin.packageName
|
||||
override val hostPackageName = applicationContext.packageName
|
||||
override suspend fun requestStartActivity(intent: Intent): Intent? {
|
||||
val result = args.handleStartActivityRequest(plugin, intent)
|
||||
return when (result.resultCode) {
|
||||
Activity.RESULT_OK -> result.data
|
||||
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
|
||||
else -> throw UserInteractionException.Activity.NotCompleted(
|
||||
result.resultCode,
|
||||
result.data
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
plugin.get(
|
||||
getScope,
|
||||
selectedApp.packageName,
|
||||
selectedApp.version
|
||||
)
|
||||
}?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
|
||||
} catch (e: UserInteractionException.Activity.NotCompleted) {
|
||||
throw e
|
||||
} catch (_: UserInteractionException) {
|
||||
null
|
||||
}?.let { (data, _) -> download(plugin, 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)
|
||||
}
|
||||
|
||||
val runtime = if (prefs.useProcessRuntime.get()) {
|
||||
@@ -229,12 +227,12 @@ class PatcherWorker(
|
||||
args.selectedPatches,
|
||||
args.options,
|
||||
args.logger,
|
||||
args.onEvent,
|
||||
args.onPatchCompleted,
|
||||
args.onProgress
|
||||
)
|
||||
|
||||
runStep(StepId.SignAPK, args.onEvent) {
|
||||
keystoreManager.sign(patchedApk, File(args.output))
|
||||
}
|
||||
keystoreManager.sign(patchedApk, File(args.output))
|
||||
updateProgress(state = State.COMPLETED) // Signing
|
||||
|
||||
Log.i(tag, "Patching succeeded".logFmt())
|
||||
Result.success()
|
||||
@@ -243,15 +241,17 @@ class PatcherWorker(
|
||||
tag,
|
||||
"An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt()
|
||||
)
|
||||
args.onEvent(ProgressEvent.Failed(null, e.toRemoteError())) // Fallback if exception doesn't occur within step
|
||||
updateProgress(state = State.FAILED, message = e.originalStackTrace)
|
||||
Result.failure()
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "An exception occurred while patching".logFmt(), e)
|
||||
args.onEvent(ProgressEvent.Failed(null, e.toRemoteError())) // Fallback if exception doesn't occur within step
|
||||
updateProgress(state = State.FAILED, message = e.stackTraceToString())
|
||||
Result.failure()
|
||||
} finally {
|
||||
patchedApk.delete()
|
||||
if (args.source is SelectedSource.Local) File(args.source.path).delete()
|
||||
if (args.input is SelectedApp.Local && args.input.temporary) {
|
||||
args.input.file.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package app.revanced.manager.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
class InstallService : Service() {
|
||||
|
||||
override fun onStartCommand(
|
||||
intent: Intent, flags: Int, startId: Int
|
||||
): Int {
|
||||
val extraStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)
|
||||
val extraStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
||||
val extraPackageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
|
||||
when (extraStatus) {
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
startActivity(if (Build.VERSION.SDK_INT >= 33) {
|
||||
intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
|
||||
} else {
|
||||
intent.getParcelableExtra(Intent.EXTRA_INTENT)
|
||||
}.apply {
|
||||
this?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
})
|
||||
}
|
||||
|
||||
else -> {
|
||||
sendBroadcast(Intent().apply {
|
||||
action = APP_INSTALL_ACTION
|
||||
`package` = packageName
|
||||
putExtra(EXTRA_INSTALL_STATUS, extraStatus)
|
||||
putExtra(EXTRA_INSTALL_STATUS_MESSAGE, extraStatusMessage)
|
||||
putExtra(EXTRA_PACKAGE_NAME, extraPackageName)
|
||||
})
|
||||
}
|
||||
}
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
companion object {
|
||||
const val APP_INSTALL_ACTION = "APP_INSTALL_ACTION"
|
||||
|
||||
const val EXTRA_INSTALL_STATUS = "EXTRA_INSTALL_STATUS"
|
||||
const val EXTRA_INSTALL_STATUS_MESSAGE = "EXTRA_INSTALL_STATUS_MESSAGE"
|
||||
const val EXTRA_PACKAGE_NAME = "EXTRA_PACKAGE_NAME"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,7 +15,6 @@ 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 io.github.fornewid.placeholder.material3.placeholder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -48,8 +47,6 @@ fun AppLabel(
|
||||
shape = RoundedCornerShape(100)
|
||||
)
|
||||
.then(modifier),
|
||||
style = style,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = style
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package app.revanced.manager.ui.component
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageInstaller
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.StringRes
|
||||
@@ -80,7 +79,7 @@ private fun installerStatusDialogButton(
|
||||
enum class DialogKind(
|
||||
val flag: Int,
|
||||
val title: Int,
|
||||
@param:StringRes val contentStringResId: Int,
|
||||
@StringRes val contentStringResId: Int,
|
||||
val icon: ImageVector = Icons.Outlined.ErrorOutline,
|
||||
val confirmButton: InstallerStatusDialogButton = installerStatusDialogButton(R.string.ok),
|
||||
val dismissButton: InstallerStatusDialogButton? = null,
|
||||
@@ -134,8 +133,10 @@ enum class DialogKind(
|
||||
title = R.string.installation_storage_issue_dialog_title,
|
||||
contentStringResId = R.string.installation_storage_issue_description,
|
||||
),
|
||||
|
||||
@RequiresApi(34)
|
||||
FAILURE_TIMEOUT(
|
||||
flag = @SuppressLint("InlinedApi") PackageInstaller.STATUS_FAILURE_TIMEOUT,
|
||||
flag = PackageInstaller.STATUS_FAILURE_TIMEOUT,
|
||||
title = R.string.installation_timeout_dialog_title,
|
||||
contentStringResId = R.string.installation_timeout_description,
|
||||
confirmButton = installerStatusDialogButton(R.string.install_app) { model ->
|
||||
|
||||
@@ -45,7 +45,6 @@ import app.revanced.manager.domain.bundles.LocalPatchBundle
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
|
||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefault
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.ExceptionViewerDialog
|
||||
@@ -66,14 +65,12 @@ fun BundleInformationDialog(
|
||||
) {
|
||||
val bundleRepo = koinInject<PatchBundleRepository>()
|
||||
val networkInfo = koinInject<NetworkInfo>()
|
||||
val prefs = koinInject<PreferencesManager>()
|
||||
val hasNetwork = remember { networkInfo.isConnected() }
|
||||
val composableScope = rememberCoroutineScope()
|
||||
var viewCurrentBundlePatches by remember { mutableStateOf(false) }
|
||||
val isLocal = src is LocalPatchBundle
|
||||
val bundleManifestAttributes = src.patchBundle?.manifestAttributes
|
||||
val (autoUpdate, endpoint) = src.asRemoteOrNull?.let { it.autoUpdate to it.endpoint }
|
||||
?: (null to null)
|
||||
val (autoUpdate, endpoint) = src.asRemoteOrNull?.let { it.autoUpdate to it.endpoint } ?: (null to null)
|
||||
|
||||
fun onAutoUpdateChange(new: Boolean) = composableScope.launch {
|
||||
with(bundleRepo) {
|
||||
@@ -176,34 +173,6 @@ fun BundleInformationDialog(
|
||||
)
|
||||
}
|
||||
|
||||
if (src.isDefault) {
|
||||
val useBundlePrerelease by prefs.usePatchesPrereleases.getAsState()
|
||||
|
||||
BundleListItem(
|
||||
headlineText = stringResource(R.string.patches_prereleases),
|
||||
supportingText = stringResource(R.string.patches_prereleases_description, src.name),
|
||||
trailingContent = {
|
||||
HapticSwitch(
|
||||
checked = useBundlePrerelease,
|
||||
onCheckedChange = {
|
||||
composableScope.launch {
|
||||
prefs.usePatchesPrereleases.update(
|
||||
it
|
||||
)
|
||||
onUpdate()
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable {
|
||||
composableScope.launch {
|
||||
prefs.usePatchesPrereleases.update(!useBundlePrerelease)
|
||||
onUpdate()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
endpoint?.takeUnless { src.isDefault }?.let { url ->
|
||||
var showUrlInputDialog by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package app.revanced.manager.ui.component.patcher
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Cancel
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
@@ -20,7 +21,6 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -39,9 +39,11 @@ import androidx.compose.ui.unit.dp
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.ArrowButton
|
||||
import app.revanced.manager.ui.component.LoadingIndicator
|
||||
import app.revanced.manager.ui.model.ProgressKey
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.ui.model.StepCategory
|
||||
import app.revanced.manager.ui.model.Step
|
||||
import app.revanced.manager.ui.model.StepCategory
|
||||
import app.revanced.manager.ui.model.StepProgressProvider
|
||||
import java.util.Locale
|
||||
import kotlin.math.floor
|
||||
|
||||
@@ -50,10 +52,21 @@ import kotlin.math.floor
|
||||
fun Steps(
|
||||
category: StepCategory,
|
||||
steps: List<Step>,
|
||||
isExpanded: Boolean = false,
|
||||
onExpand: () -> Unit,
|
||||
onClick: () -> Unit
|
||||
stepCount: Pair<Int, Int>? = null,
|
||||
stepProgressProvider: StepProgressProvider
|
||||
) {
|
||||
var expanded by rememberSaveable { mutableStateOf(true) }
|
||||
|
||||
val categoryColor by animateColorAsState(
|
||||
if (expanded) MaterialTheme.colorScheme.surfaceContainerHigh else Color.Transparent,
|
||||
label = "category"
|
||||
)
|
||||
|
||||
val cardColor by animateColorAsState(
|
||||
if (expanded) MaterialTheme.colorScheme.surfaceContainer else Color.Transparent,
|
||||
label = "card"
|
||||
)
|
||||
|
||||
val state = remember(steps) {
|
||||
when {
|
||||
steps.all { it.state == State.COMPLETED } -> State.COMPLETED
|
||||
@@ -63,69 +76,62 @@ fun Steps(
|
||||
}
|
||||
}
|
||||
|
||||
val filteredSteps = remember(steps) {
|
||||
val failedCount = steps.count { it.state == State.FAILED }
|
||||
|
||||
steps.filter { step ->
|
||||
// Show hidden steps if it's the only failed step.
|
||||
!step.hide || (step.state == State.FAILED && failedCount == 1)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(state) {
|
||||
if (state == State.RUNNING || state == State.FAILED)
|
||||
onExpand()
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||
.background(cardColor)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||
modifier = Modifier
|
||||
.clickable(true, onClick = onClick)
|
||||
.fillMaxWidth()
|
||||
.padding(20.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.clickable { expanded = !expanded }
|
||||
.background(categoryColor)
|
||||
) {
|
||||
StepIcon(state = state, size = 24.dp)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
StepIcon(state = state, size = 24.dp)
|
||||
|
||||
Text(stringResource(category.displayName))
|
||||
Text(stringResource(category.displayName))
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Text(
|
||||
text = "${filteredSteps.count { it.state == State.COMPLETED }}/${filteredSteps.size}",
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
val stepProgress = remember(stepCount, steps) {
|
||||
stepCount?.let { (current, total) -> "$current/$total" }
|
||||
?: "${steps.count { it.state == State.COMPLETED }}/${steps.size}"
|
||||
}
|
||||
|
||||
ArrowButton(modifier = Modifier.size(24.dp), expanded = isExpanded, onClick = null)
|
||||
Text(
|
||||
text = stepProgress,
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
|
||||
ArrowButton(modifier = Modifier.size(24.dp), expanded = expanded, onClick = null)
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(visible = isExpanded) {
|
||||
AnimatedVisibility(visible = expanded) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.background.copy(0.6f))
|
||||
.fillMaxWidth()
|
||||
.padding(top = 10.dp)
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
filteredSteps.forEachIndexed { index, step ->
|
||||
val (progress, progressText) = step.progress?.let { (current, total) ->
|
||||
if (total != null) current.toFloat() / total.toFloat() to "${current.megaBytes}/${total.megaBytes} MB"
|
||||
else null to "${current.megaBytes} MB"
|
||||
steps.forEach { step ->
|
||||
val (progress, progressText) = when (step.progressKey) {
|
||||
null -> null
|
||||
ProgressKey.DOWNLOAD -> stepProgressProvider.downloadProgress?.let { (downloaded, total) ->
|
||||
if (total != null) downloaded.toFloat() / total.toFloat() to "${downloaded.megaBytes}/${total.megaBytes} MB"
|
||||
else null to "${downloaded.megaBytes} MB"
|
||||
}
|
||||
} ?: (null to null)
|
||||
|
||||
SubStep(
|
||||
name = step.title,
|
||||
name = step.name,
|
||||
state = step.state,
|
||||
message = step.message,
|
||||
progress = progress,
|
||||
progressText = progressText,
|
||||
isFirst = index == 0,
|
||||
isLast = index == filteredSteps.lastIndex,
|
||||
progressText = progressText
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -139,9 +145,7 @@ fun SubStep(
|
||||
state: State,
|
||||
message: String? = null,
|
||||
progress: Float? = null,
|
||||
progressText: String? = null,
|
||||
isFirst: Boolean = false,
|
||||
isLast: Boolean = false,
|
||||
progressText: String? = null
|
||||
) {
|
||||
var messageExpanded by rememberSaveable { mutableStateOf(true) }
|
||||
|
||||
@@ -152,22 +156,22 @@ fun SubStep(
|
||||
clickable { messageExpanded = !messageExpanded }
|
||||
else this
|
||||
}
|
||||
.padding(top = if (isFirst) 10.dp else 8.dp, bottom = if (isLast) 20.dp else 8.dp)
|
||||
.padding(horizontal = 20.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
StepIcon(
|
||||
size = 18.dp,
|
||||
state = state,
|
||||
progress = progress,
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier.size(24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
StepIcon(state, progress, size = 20.dp)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = name,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f, true),
|
||||
@@ -197,7 +201,7 @@ fun SubStep(
|
||||
text = message.orEmpty(),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier.padding(horizontal = 36.dp, vertical = 8.dp)
|
||||
modifier = Modifier.padding(horizontal = 52.dp, vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -207,44 +211,40 @@ fun SubStep(
|
||||
fun StepIcon(state: State, progress: Float? = null, size: Dp) {
|
||||
val strokeWidth = Dp(floor(size.value / 10) + 1)
|
||||
|
||||
Crossfade(targetState = state, label = "State CrossFade") { state ->
|
||||
when (state) {
|
||||
State.COMPLETED -> Icon(
|
||||
Icons.Filled.CheckCircle,
|
||||
contentDescription = stringResource(R.string.step_completed),
|
||||
tint = Color(0xFF59B463),
|
||||
modifier = Modifier.size(size)
|
||||
when (state) {
|
||||
State.COMPLETED -> Icon(
|
||||
Icons.Filled.CheckCircle,
|
||||
contentDescription = stringResource(R.string.step_completed),
|
||||
tint = MaterialTheme.colorScheme.surfaceTint,
|
||||
modifier = Modifier.size(size)
|
||||
)
|
||||
|
||||
State.FAILED -> Icon(
|
||||
Icons.Filled.Cancel,
|
||||
contentDescription = stringResource(R.string.step_failed),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(size)
|
||||
)
|
||||
|
||||
State.WAITING -> Icon(
|
||||
Icons.Outlined.Circle,
|
||||
contentDescription = stringResource(R.string.step_waiting),
|
||||
tint = MaterialTheme.colorScheme.surfaceVariant,
|
||||
modifier = Modifier.size(size)
|
||||
)
|
||||
|
||||
State.RUNNING ->
|
||||
LoadingIndicator(
|
||||
modifier = stringResource(R.string.step_running).let { description ->
|
||||
Modifier
|
||||
.size(size)
|
||||
.semantics {
|
||||
contentDescription = description
|
||||
}
|
||||
},
|
||||
progress = { progress },
|
||||
strokeWidth = strokeWidth
|
||||
)
|
||||
|
||||
State.FAILED -> Icon(
|
||||
Icons.Filled.Cancel,
|
||||
contentDescription = stringResource(R.string.step_failed),
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(size)
|
||||
)
|
||||
|
||||
State.WAITING -> Icon(
|
||||
Icons.Outlined.Circle,
|
||||
contentDescription = stringResource(R.string.step_waiting),
|
||||
tint = MaterialTheme.colorScheme.onSurface.copy(.2f),
|
||||
modifier = Modifier.size(size)
|
||||
)
|
||||
|
||||
State.RUNNING -> {
|
||||
LoadingIndicator(
|
||||
modifier = stringResource(R.string.step_running).let { description ->
|
||||
Modifier
|
||||
.size(size)
|
||||
.semantics {
|
||||
contentDescription = description
|
||||
}
|
||||
},
|
||||
|
||||
progress = { progress },
|
||||
strokeWidth = strokeWidth
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package app.revanced.manager.ui.model
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.StringRes
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.patcher.StepId
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
enum class StepCategory(@StringRes val displayName: Int) {
|
||||
@@ -16,20 +15,19 @@ enum class State {
|
||||
WAITING, RUNNING, FAILED, COMPLETED
|
||||
}
|
||||
|
||||
enum class ProgressKey {
|
||||
DOWNLOAD
|
||||
}
|
||||
|
||||
interface StepProgressProvider {
|
||||
val downloadProgress: Pair<Long, Long?>?
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Step(
|
||||
val id: StepId,
|
||||
val title: String,
|
||||
val name: String,
|
||||
val category: StepCategory,
|
||||
val state: State = State.WAITING,
|
||||
val message: String? = null,
|
||||
val progress: Pair<Long, Long?>? = null,
|
||||
val hide: Boolean = false,
|
||||
) : Parcelable
|
||||
|
||||
|
||||
fun Step.withState(
|
||||
state: State = this.state,
|
||||
message: String? = this.message,
|
||||
progress: Pair<Long, Long?>? = this.progress
|
||||
) = copy(state = state, message = message, progress = progress)
|
||||
val progressKey: ProgressKey? = null
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,35 @@
|
||||
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
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
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 Plugin(val packageName: String?) : SelectedSource()
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
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,7 @@
|
||||
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.ui.model.SelectedApp
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import kotlinx.parcelize.Parcelize
|
||||
@@ -24,11 +23,10 @@ data class InstalledApplicationInfo(val packageName: String)
|
||||
data class Update(val downloadOnScreenEntry: Boolean = false)
|
||||
|
||||
@Serializable
|
||||
data object SelectedAppInfo : ComplexParameter<SelectedAppInfo.ViewModelParams> {
|
||||
data object SelectedApplicationInfo : ComplexParameter<SelectedApplicationInfo.ViewModelParams> {
|
||||
@Parcelize
|
||||
data class ViewModelParams(
|
||||
val packageName: String,
|
||||
val localPath: String? = null,
|
||||
val app: SelectedApp,
|
||||
val patches: PatchSelection? = null
|
||||
) : Parcelable
|
||||
|
||||
@@ -39,35 +37,12 @@ data object SelectedAppInfo : ComplexParameter<SelectedAppInfo.ViewModelParams>
|
||||
data object PatchesSelector : ComplexParameter<PatchesSelector.ViewModelParams> {
|
||||
@Parcelize
|
||||
data class ViewModelParams(
|
||||
val packageName: String,
|
||||
val version: String?,
|
||||
val patchSelection: PatchSelection?,
|
||||
val app: SelectedApp,
|
||||
val currentSelection: 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>
|
||||
}
|
||||
@@ -76,9 +51,7 @@ data object SelectedAppInfo : ComplexParameter<SelectedAppInfo.ViewModelParams>
|
||||
data object Patcher : ComplexParameter<Patcher.ViewModelParams> {
|
||||
@Parcelize
|
||||
data class ViewModelParams(
|
||||
val packageName: String,
|
||||
val version: String?,
|
||||
val selectedSource: SelectedSource,
|
||||
val selectedApp: SelectedApp,
|
||||
val selectedPatches: PatchSelection,
|
||||
val options: @RawValue Options
|
||||
) : Parcelable
|
||||
|
||||
@@ -44,6 +44,7 @@ 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
|
||||
@@ -53,13 +54,13 @@ import org.koin.androidx.compose.koinViewModel
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AppSelectorScreen(
|
||||
onSelect: (packageName: String) -> Unit,
|
||||
onStorageSelect: (packageName: String, path: String) -> Unit,
|
||||
onSelect: (String) -> Unit,
|
||||
onStorageSelect: (SelectedApp.Local) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
vm: AppSelectorViewModel = koinViewModel()
|
||||
) {
|
||||
EventEffect(flow = vm.storageSelectionFlow) {
|
||||
onStorageSelect(it.first, it.second)
|
||||
onStorageSelect(it)
|
||||
}
|
||||
|
||||
val pickApkLauncher =
|
||||
@@ -82,12 +83,12 @@ fun AppSelectorScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// vm.nonSuggestedVersionDialogSubject?.let {
|
||||
// NonSuggestedVersionDialog(
|
||||
// suggestedVersion = suggestedVersions[it.packageName].orEmpty(),
|
||||
// onDismiss = vm::dismissNonSuggestedVersionDialog
|
||||
// )
|
||||
// }
|
||||
vm.nonSuggestedVersionDialogSubject?.let {
|
||||
NonSuggestedVersionDialog(
|
||||
suggestedVersion = suggestedVersions[it.packageName].orEmpty(),
|
||||
onDismiss = vm::dismissNonSuggestedVersionDialog
|
||||
)
|
||||
}
|
||||
|
||||
if (search)
|
||||
SearchView(
|
||||
@@ -114,7 +115,8 @@ fun AppSelectorScreen(
|
||||
)
|
||||
},
|
||||
headlineContent = { AppLabel(app.packageInfo) },
|
||||
supportingContent = app.patches?.let {
|
||||
supportingContent = { Text(app.packageName) },
|
||||
trailingContent = app.patches?.let {
|
||||
{
|
||||
Text(
|
||||
pluralStringResource(
|
||||
@@ -212,7 +214,12 @@ fun AppSelectorScreen(
|
||||
defaultText = app.packageName
|
||||
)
|
||||
},
|
||||
supportingContent = app.patches?.let {
|
||||
supportingContent = {
|
||||
suggestedVersions[app.packageName]?.let {
|
||||
Text(stringResource(R.string.suggested_version_info, it))
|
||||
}
|
||||
},
|
||||
trailingContent = app.patches?.let {
|
||||
{
|
||||
Text(
|
||||
pluralStringResource(
|
||||
@@ -227,13 +234,7 @@ fun AppSelectorScreen(
|
||||
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier.fillParentMaxSize(), contentAlignment = Alignment.Center
|
||||
) {
|
||||
LoadingIndicator()
|
||||
}
|
||||
}
|
||||
item { LoadingIndicator() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,6 @@ import app.revanced.manager.ui.model.StepCategory
|
||||
import app.revanced.manager.ui.viewmodel.PatcherViewModel
|
||||
import app.revanced.manager.util.APK_MIMETYPE
|
||||
import app.revanced.manager.util.EventEffect
|
||||
import app.revanced.manager.util.toast
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -77,17 +76,18 @@ fun PatcherScreen(
|
||||
var showInstallPicker by rememberSaveable { mutableStateOf(false) }
|
||||
var showDismissConfirmationDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
fun onPageBack() = when {
|
||||
patcherSucceeded == null -> showDismissConfirmationDialog = true
|
||||
viewModel.isInstalling -> context.toast(context.getString(R.string.patcher_install_in_progress))
|
||||
else -> onLeave()
|
||||
fun onPageBack() {
|
||||
if(patcherSucceeded == null)
|
||||
showDismissConfirmationDialog = true
|
||||
else
|
||||
onLeave()
|
||||
}
|
||||
|
||||
BackHandler(onBack = ::onPageBack)
|
||||
|
||||
val steps by remember {
|
||||
derivedStateOf {
|
||||
viewModel.steps.groupBy { it.category }.toList()
|
||||
viewModel.steps.groupBy { it.category }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,12 +213,6 @@ fun PatcherScreen(
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
var expandedCategory by rememberSaveable { mutableStateOf<StepCategory?>(null) }
|
||||
|
||||
val expandCategory: (StepCategory?) -> Unit = { category ->
|
||||
expandedCategory = category
|
||||
}
|
||||
|
||||
LinearProgressIndicator(
|
||||
progress = { viewModel.progress },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
@@ -230,17 +224,14 @@ fun PatcherScreen(
|
||||
contentPadding = PaddingValues(16.dp)
|
||||
) {
|
||||
items(
|
||||
items = steps,
|
||||
items = steps.toList(),
|
||||
key = { it.first }
|
||||
) { (category, steps) ->
|
||||
Steps(
|
||||
category = category,
|
||||
steps = steps,
|
||||
isExpanded = expandedCategory == category,
|
||||
onExpand = { expandCategory(category) },
|
||||
onClick = {
|
||||
expandCategory(if (expandedCategory == category) null else category)
|
||||
}
|
||||
stepCount = if (category == StepCategory.PATCHING) viewModel.patchesProgress else null,
|
||||
stepProgressProvider = viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,7 +498,7 @@ private fun PatchItem(
|
||||
leadingContent = {
|
||||
HapticCheckbox(
|
||||
checked = selected,
|
||||
onCheckedChange = null,
|
||||
onCheckedChange = { onToggle() },
|
||||
enabled = compatible
|
||||
)
|
||||
},
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
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.ColumnScope
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
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
|
||||
@@ -17,6 +21,7 @@ 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
|
||||
@@ -31,30 +36,33 @@ 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.LoadedDownloaderPlugin
|
||||
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.SelectedSource
|
||||
import app.revanced.manager.ui.model.SelectedVersion
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
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.launch
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SelectedAppInfoScreen(
|
||||
onPatchSelectorClick: (packageName: String, version: String?, PatchSelection?, Options) -> Unit,
|
||||
onRequiredOptions: (packageName: String, version: String?, PatchSelection?, Options) -> Unit,
|
||||
onPatchSelectorClick: (SelectedApp, PatchSelection?, Options) -> Unit,
|
||||
onRequiredOptions: (SelectedApp, 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
|
||||
) {
|
||||
@@ -63,23 +71,29 @@ fun SelectedAppInfoScreen(
|
||||
val networkConnected = remember { networkInfo.isConnected() }
|
||||
val networkMetered = remember { !networkInfo.isUnmetered() }
|
||||
|
||||
val packageName = vm.packageName
|
||||
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 = remember(bundles, allowIncompatiblePatches) {
|
||||
vm.getPatches(bundles, allowIncompatiblePatches)
|
||||
}
|
||||
val selectedPatchCount = remember(patches) {
|
||||
patches.values.sumOf { it.size }
|
||||
}
|
||||
|
||||
val launcher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = vm::handlePluginActivityResult
|
||||
)
|
||||
EventEffect(flow = vm.launchActivityFlow) { intent ->
|
||||
launcher.launch(intent)
|
||||
}
|
||||
val composableScope = rememberCoroutineScope()
|
||||
|
||||
val error by vm.errorFlow.collectAsStateWithLifecycle(null)
|
||||
|
||||
val selectedVersion by vm.selectedVersion.collectAsStateWithLifecycle()
|
||||
val resolvedVersion by vm.resolvedVersion.collectAsStateWithLifecycle(null)
|
||||
|
||||
val selectedSource by vm.selectedSource.collectAsStateWithLifecycle()
|
||||
val resolvedSource by vm.resolvedSource.collectAsStateWithLifecycle(null)
|
||||
|
||||
val customSelection by vm.customSelection.collectAsStateWithLifecycle(null)
|
||||
val fullPatchSelection by vm.patchSelection.collectAsStateWithLifecycle(emptyMap())
|
||||
val patchCount = fullPatchSelection.patchCount
|
||||
|
||||
val incompatibleCount by vm.incompatiblePatchCount.collectAsStateWithLifecycle(0)
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
@@ -102,18 +116,18 @@ fun SelectedAppInfoScreen(
|
||||
)
|
||||
},
|
||||
onClick = patchClick@{
|
||||
if (patchCount == 0) {
|
||||
if (selectedPatchCount == 0) {
|
||||
context.toast(context.getString(R.string.no_patches_selected))
|
||||
|
||||
return@patchClick
|
||||
}
|
||||
|
||||
composableScope.launch {
|
||||
if (!vm.hasSetRequiredOptions(fullPatchSelection)) {
|
||||
if (!vm.hasSetRequiredOptions(patches)) {
|
||||
onRequiredOptions(
|
||||
vm.packageName,
|
||||
resolvedVersion,
|
||||
customSelection,
|
||||
vm.options,
|
||||
vm.selectedApp,
|
||||
vm.getCustomPatches(bundles, allowIncompatiblePatches),
|
||||
vm.options
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
@@ -125,96 +139,94 @@ fun SelectedAppInfoScreen(
|
||||
},
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) { paddingValues ->
|
||||
val plugins by vm.plugins.collectAsStateWithLifecycle(emptyList())
|
||||
|
||||
if (vm.showSourceSelector) {
|
||||
val requiredVersion by vm.requiredVersion.collectAsStateWithLifecycle(null)
|
||||
|
||||
AppSourceSelectorDialog(
|
||||
plugins = plugins,
|
||||
installedApp = vm.installedAppData,
|
||||
searchApp = SelectedApp.Search(
|
||||
vm.packageName,
|
||||
vm.desiredVersion
|
||||
),
|
||||
activeSearchJob = vm.activePluginAction,
|
||||
hasRoot = vm.hasRoot,
|
||||
onDismissRequest = vm::dismissSourceSelector,
|
||||
onSelectPlugin = vm::searchUsingPlugin,
|
||||
requiredVersion = requiredVersion,
|
||||
onSelect = {
|
||||
vm.selectedApp = it
|
||||
vm.dismissSourceSelector()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
ColumnWithScrollbar(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
AppInfo(vm.selectedAppInfo, placeholderLabel = packageName) {
|
||||
vm.selectedAppInfo?.let {
|
||||
Text(
|
||||
it.packageName,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
version ?: stringResource(R.string.selected_app_meta_any_version),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
|
||||
PageItem(
|
||||
R.string.patch_selector_item,
|
||||
stringResource(R.string.patch_selector_item_description, patchCount),
|
||||
stringResource(
|
||||
R.string.patch_selector_item_description,
|
||||
selectedPatchCount
|
||||
),
|
||||
onClick = {
|
||||
onPatchSelectorClick(
|
||||
vm.packageName,
|
||||
resolvedVersion,
|
||||
customSelection,
|
||||
vm.selectedApp,
|
||||
vm.getCustomPatches(
|
||||
bundles,
|
||||
allowIncompatiblePatches
|
||||
),
|
||||
vm.options
|
||||
)
|
||||
},
|
||||
extraDescription = if (incompatibleCount > 0) { {
|
||||
Text(
|
||||
"$incompatibleCount incompatible",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
} } else null,
|
||||
)
|
||||
|
||||
val versionText = resolvedVersion ?: "Any available version"
|
||||
val versionDescription = if (selectedVersion is SelectedVersion.Auto)
|
||||
"Auto ($versionText)" // stringResource(R.string.selected_app_meta_auto_version, actualVersion)
|
||||
else versionText
|
||||
|
||||
PageItem(
|
||||
R.string.version_selector_item,
|
||||
versionDescription,
|
||||
onClick = {
|
||||
onVersionClick(
|
||||
packageName,
|
||||
fullPatchSelection,
|
||||
selectedVersion,
|
||||
vm.localPath,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
val sourceText = when (val source = resolvedSource) {
|
||||
is SelectedSource.Installed -> "Installed APK"
|
||||
is SelectedSource.Downloaded -> "Downloaded APK"
|
||||
is SelectedSource.Local -> "Local APK"
|
||||
is SelectedSource.Plugin -> {
|
||||
source.packageName ?: "Any available downloader"
|
||||
}
|
||||
else -> "Auto"
|
||||
}
|
||||
val sourceDescription = if (selectedSource is SelectedSource.Auto)
|
||||
"Auto ($sourceText)" // stringResource(R.string.selected_app_meta_auto_version, actualVersion)
|
||||
else sourceText
|
||||
|
||||
)
|
||||
PageItem(
|
||||
R.string.apk_source_selector_item,
|
||||
sourceDescription,
|
||||
onClick = { onSourceClick(
|
||||
packageName,
|
||||
resolvedVersion,
|
||||
selectedSource,
|
||||
vm.localPath,
|
||||
) },
|
||||
)
|
||||
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,
|
||||
plugins.find { it.packageName == app.data.pluginPackageName }?.name
|
||||
?: app.data.pluginPackageName
|
||||
)
|
||||
|
||||
is SelectedApp.Local -> stringResource(R.string.apk_source_local)
|
||||
},
|
||||
onClick = {
|
||||
vm.showSourceSelector()
|
||||
}
|
||||
)
|
||||
error?.let {
|
||||
Text(
|
||||
stringResource(it.resourceId),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
modifier = Modifier.padding(horizontal = 24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (resolvedSource is SelectedSource.Plugin) Column(
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 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 }
|
||||
|
||||
when {
|
||||
!needsInternet -> {}
|
||||
!networkConnected -> {
|
||||
NotificationCard(
|
||||
isWarning = true,
|
||||
@@ -223,6 +235,7 @@ fun SelectedAppInfoScreen(
|
||||
onDismiss = null
|
||||
)
|
||||
}
|
||||
|
||||
networkMetered -> {
|
||||
NotificationCard(
|
||||
isWarning = true,
|
||||
@@ -238,17 +251,11 @@ fun SelectedAppInfoScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PageItem(
|
||||
@StringRes title: Int,
|
||||
description: String,
|
||||
onClick: () -> Unit,
|
||||
enabled: Boolean = true,
|
||||
extraDescription: @Composable (ColumnScope.() -> Unit)? = null,
|
||||
) {
|
||||
private fun PageItem(@StringRes title: Int, description: String, onClick: () -> Unit) {
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.clickable(enabled, onClick = onClick)
|
||||
.enabled(enabled),
|
||||
.clickable(onClick = onClick)
|
||||
.padding(start = 8.dp),
|
||||
headlineContent = {
|
||||
Text(
|
||||
stringResource(title),
|
||||
@@ -257,17 +264,99 @@ private fun PageItem(
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Column {
|
||||
Text(
|
||||
description,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
extraDescription?.invoke(this)
|
||||
}
|
||||
Text(
|
||||
description,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Icon(Icons.AutoMirrored.Outlined.ArrowRight, null)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppSourceSelectorDialog(
|
||||
plugins: List<LoadedDownloaderPlugin>,
|
||||
installedApp: Pair<SelectedApp.Installed, InstalledApp?>?,
|
||||
searchApp: SelectedApp.Search,
|
||||
activeSearchJob: String?,
|
||||
hasRoot: Boolean,
|
||||
requiredVersion: String?,
|
||||
onDismissRequest: () -> Unit,
|
||||
onSelectPlugin: (LoadedDownloaderPlugin) -> 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 hasPlugins = plugins.isNotEmpty()
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.clickable(enabled = canSelect && hasPlugins) { onSelect(searchApp) }
|
||||
.enabled(hasPlugins),
|
||||
headlineContent = { Text(stringResource(R.string.app_source_dialog_option_auto)) },
|
||||
supportingContent = {
|
||||
Text(
|
||||
if (hasPlugins)
|
||||
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(plugins, key = { "plugin_${it.packageName}" }) { plugin ->
|
||||
ListItem(
|
||||
modifier = Modifier.clickable(enabled = canSelect) { onSelectPlugin(plugin) },
|
||||
headlineContent = { Text(plugin.name) },
|
||||
trailingContent = (@Composable { LoadingIndicator() }).takeIf { activeSearchJob == plugin.packageName },
|
||||
colors = transparentListItemColors
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
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.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.LazyColumnWithScrollbar
|
||||
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 plugins by viewModel.plugins.collectAsStateWithLifecycle(emptyList())
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = { Text("Select source") },
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
HapticExtendedFloatingActionButton(
|
||||
text = { Text(stringResource(R.string.save)) },
|
||||
icon = { Icon(Icons.Outlined.Save, null) },
|
||||
onClick = { onSave(viewModel.selectedSource) },
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
LazyColumnWithScrollbar (
|
||||
contentPadding = paddingValues,
|
||||
) {
|
||||
item {
|
||||
SourceOption(
|
||||
isSelected = viewModel.selectedSource == SelectedSource.Auto,
|
||||
onSelect = { viewModel.selectSource(SelectedSource.Auto) },
|
||||
headlineContent = { Text("Auto (Recommended)") },
|
||||
supportingContent = { Text("Automatically select the best available source") }
|
||||
)
|
||||
}
|
||||
item {
|
||||
SourceOption(
|
||||
isSelected = viewModel.selectedSource == SelectedSource.Plugin(null),
|
||||
onSelect = { viewModel.selectSource(SelectedSource.Plugin(null)) },
|
||||
headlineContent = { Text("Any available downloader") },
|
||||
)
|
||||
}
|
||||
|
||||
viewModel.localApp?.let { option ->
|
||||
item {
|
||||
HorizontalDivider()
|
||||
SourceOption(
|
||||
sourceOption = option,
|
||||
isSelected = viewModel.selectedSource == option.source,
|
||||
onSelect = viewModel::selectSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.installedSource?.let { option ->
|
||||
item {
|
||||
HorizontalDivider()
|
||||
SourceOption(
|
||||
sourceOption = option,
|
||||
isSelected = viewModel.selectedSource == option.source,
|
||||
onSelect = viewModel::selectSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadedApps.isNotEmpty()) item { HorizontalDivider() }
|
||||
items(downloadedApps, key = { it.key }) { option ->
|
||||
SourceOption(
|
||||
sourceOption = option,
|
||||
isSelected = viewModel.selectedSource == option.source,
|
||||
onSelect = viewModel::selectSource,
|
||||
)
|
||||
}
|
||||
|
||||
if (plugins.isNotEmpty()) item { HorizontalDivider() }
|
||||
items(plugins, key = { it.key }) { option ->
|
||||
SourceOption(
|
||||
sourceOption = option,
|
||||
isSelected = viewModel.selectedSource == option.source,
|
||||
onSelect = viewModel::selectSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SourceOption(
|
||||
sourceOption: SourceSelectorViewModel.SourceOption,
|
||||
isSelected: Boolean,
|
||||
onSelect: (SelectedSource) -> Unit,
|
||||
) = SourceOption(
|
||||
isSelected = isSelected,
|
||||
onSelect = { onSelect(sourceOption.source) },
|
||||
overlineContent = sourceOption.category?.let {{ Text(it) }},
|
||||
headlineContent = { Text(sourceOption.title, maxLines = 1, overflow = TextOverflow.Ellipsis) },
|
||||
supportingContent = sourceOption.disableReason?.let {{ Text(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
|
||||
)
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package app.revanced.manager.ui.screen
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.outlined.MoreVert
|
||||
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.IconButton
|
||||
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.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
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("Select version") },
|
||||
onBackClick = onBackClick,
|
||||
actions = {
|
||||
IconButton({}) {
|
||||
Icon(Icons.Outlined.MoreVert, contentDescription = null)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
HapticExtendedFloatingActionButton(
|
||||
text = { Text(stringResource(R.string.save)) },
|
||||
icon = { Icon(Icons.Outlined.Save, contentDescription = null) },
|
||||
onClick = { onSave(viewModel.selectedVersion) }
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
) {
|
||||
VersionOption(
|
||||
version = SelectedVersion.Auto,
|
||||
isSelected = viewModel.selectedVersion is SelectedVersion.Auto,
|
||||
onSelect = viewModel::selectVersion,
|
||||
headlineContent = { Text("Auto (Recommended)") },
|
||||
supportingContent = { Text("Automatically select the best available version") }
|
||||
)
|
||||
HorizontalDivider()
|
||||
|
||||
if (versions.isNotEmpty()) {
|
||||
LazyColumn {
|
||||
items(versions, key = { it.first.version }) { version ->
|
||||
val isDownloaded = downloadedVersions.contains(version.first.version)
|
||||
val isInstalled = viewModel.installedAppVersion == version.first.version
|
||||
val isLocal = localVersion == version.first.version
|
||||
|
||||
val overlineText = when {
|
||||
isLocal -> "Local"
|
||||
isDownloaded && isInstalled -> "Downloaded, Installed"
|
||||
isDownloaded -> "Downloaded"
|
||||
isInstalled -> "Installed"
|
||||
else -> null
|
||||
}
|
||||
|
||||
VersionOption(
|
||||
version = version.first,
|
||||
isSelected = viewModel.selectedVersion == version.first,
|
||||
onSelect = viewModel::selectVersion,
|
||||
headlineContent = { Text(version.first.version) },
|
||||
supportingContent = {
|
||||
Text(
|
||||
"${version.second.let { if (it == 0) "No" else it }} incompatible patches"
|
||||
)
|
||||
},
|
||||
overlineContent = overlineText?.let { { Text(it) } }
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
VersionOption(
|
||||
version = SelectedVersion.Any,
|
||||
isSelected = viewModel.selectedVersion is SelectedVersion.Any,
|
||||
onSelect = viewModel::selectVersion,
|
||||
headlineContent = { Text("Any available version") },
|
||||
supportingContent = { Text("Use any available version regardless of compatibility") }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package app.revanced.manager.ui.screen.settings
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
@@ -35,8 +34,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.platform.UriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -60,7 +57,6 @@ fun ContributorSettingsScreen(
|
||||
) {
|
||||
val repositories = viewModel.repositories
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -97,8 +93,7 @@ fun ContributorSettingsScreen(
|
||||
) {
|
||||
ContributorsCard(
|
||||
title = it.name,
|
||||
contributors = it.contributors,
|
||||
uriHandler = uriHandler
|
||||
contributors = it.contributors
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -120,8 +115,7 @@ fun ContributorsCard(
|
||||
title: String,
|
||||
contributors: List<ReVancedContributor>,
|
||||
itemsPerPage: Int = 12,
|
||||
numberOfRows: Int = 2,
|
||||
uriHandler: UriHandler
|
||||
numberOfRows: Int = 2
|
||||
) {
|
||||
val itemsPerRow = (itemsPerPage / numberOfRows)
|
||||
|
||||
@@ -178,11 +172,7 @@ fun ContributorsCard(
|
||||
contributorsByPage[page].forEach {
|
||||
if (itemSize > 100.dp) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.width(itemSize - 1.dp)
|
||||
.clickable {
|
||||
uriHandler.openUri("https://github.com/${it.username}")
|
||||
}, // we delete 1.dp to account for not-so divisible numbers
|
||||
modifier = Modifier.width(itemSize - 1.dp), // we delete 1.dp to account for not-so divisible numbers
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
@@ -213,9 +203,6 @@ fun ContributorsCard(
|
||||
modifier = Modifier
|
||||
.size(size = (itemSize - 1.dp).coerceAtMost(50.dp)) // we delete 1.dp to account for not-so divisible numbers
|
||||
.clip(CircleShape)
|
||||
.clickable {
|
||||
uriHandler.openUri("https://github.com/${it.username}")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package app.revanced.manager.ui.screen.settings
|
||||
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -12,13 +10,10 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -33,7 +28,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -42,10 +36,10 @@ import app.revanced.manager.R
|
||||
import app.revanced.manager.network.downloader.DownloaderPluginState
|
||||
import app.revanced.manager.ui.component.AppLabel
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.ConfirmDialog
|
||||
import app.revanced.manager.ui.component.ExceptionViewerDialog
|
||||
import app.revanced.manager.ui.component.GroupHeader
|
||||
import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
||||
import app.revanced.manager.ui.component.ConfirmDialog
|
||||
import app.revanced.manager.ui.component.haptics.HapticCheckbox
|
||||
import app.revanced.manager.ui.component.settings.SettingsListItem
|
||||
import app.revanced.manager.ui.viewmodel.DownloadsViewModel
|
||||
@@ -58,7 +52,6 @@ fun DownloadsSettingsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
viewModel: DownloadsViewModel = koinViewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList())
|
||||
val pluginStates by viewModel.downloaderPluginStates.collectAsStateWithLifecycle()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
@@ -82,7 +75,7 @@ fun DownloadsSettingsScreen(
|
||||
onBackClick = onBackClick,
|
||||
actions = {
|
||||
if (viewModel.appSelection.isNotEmpty()) {
|
||||
IconButton(onClick = { viewModel.deleteApps() }) {
|
||||
IconButton(onClick = { showDeleteConfirmationDialog = true }) {
|
||||
Icon(Icons.Default.Delete, stringResource(R.string.delete))
|
||||
}
|
||||
}
|
||||
@@ -128,11 +121,6 @@ fun DownloadsSettingsScreen(
|
||||
.digest(androidSignature.toByteArray())
|
||||
hash.toHexString(format = HexFormat.UpperCase)
|
||||
}
|
||||
val appName = remember {
|
||||
packageInfo.applicationInfo?.loadLabel(context.packageManager)
|
||||
?.toString()
|
||||
?: packageName
|
||||
}
|
||||
|
||||
when (state) {
|
||||
is DownloaderPluginState.Loaded -> TrustDialog(
|
||||
@@ -142,8 +130,6 @@ fun DownloadsSettingsScreen(
|
||||
packageName,
|
||||
signature
|
||||
),
|
||||
pluginName = appName,
|
||||
signature = signature,
|
||||
onDismiss = ::dismiss,
|
||||
onConfirm = {
|
||||
viewModel.revokePluginTrust(packageName)
|
||||
@@ -161,10 +147,10 @@ fun DownloadsSettingsScreen(
|
||||
is DownloaderPluginState.Untrusted -> TrustDialog(
|
||||
title = R.string.downloader_plugin_trust_dialog_title,
|
||||
body = stringResource(
|
||||
R.string.downloader_plugin_trust_dialog_body
|
||||
R.string.downloader_plugin_trust_dialog_body,
|
||||
packageName,
|
||||
signature
|
||||
),
|
||||
pluginName = appName,
|
||||
signature = signature,
|
||||
onDismiss = ::dismiss,
|
||||
onConfirm = {
|
||||
viewModel.trustPlugin(packageName)
|
||||
@@ -240,8 +226,6 @@ fun DownloadsSettingsScreen(
|
||||
private fun TrustDialog(
|
||||
@StringRes title: Int,
|
||||
body: String,
|
||||
pluginName: String,
|
||||
signature: String,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: () -> Unit
|
||||
) {
|
||||
@@ -254,39 +238,10 @@ private fun TrustDialog(
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
Text(stringResource(R.string.dismiss))
|
||||
}
|
||||
},
|
||||
title = { Text(stringResource(title)) },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text(body)
|
||||
Card {
|
||||
Column(
|
||||
Modifier.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.downloader_plugin_trust_dialog_plugin,
|
||||
pluginName
|
||||
),
|
||||
)
|
||||
OutlinedCard(
|
||||
colors = CardDefaults.outlinedCardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.downloader_plugin_trust_dialog_signature,
|
||||
signature.chunked(2).joinToString(" ")
|
||||
), modifier = Modifier.padding(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
text = { Text(body) }
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package app.revanced.manager.ui.screen.settings
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -97,14 +96,6 @@ fun GeneralSettingsScreen(
|
||||
description = R.string.dynamic_color_description
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(theme != Theme.LIGHT) {
|
||||
BooleanItem(
|
||||
preference = prefs.pureBlackTheme,
|
||||
coroutineScope = coroutineScope,
|
||||
headline = R.string.pure_black_theme,
|
||||
description = R.string.pure_black_theme_description
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,6 @@ private val LightColorScheme = lightColorScheme(
|
||||
fun ReVancedManagerTheme(
|
||||
darkTheme: Boolean,
|
||||
dynamicColor: Boolean,
|
||||
pureBlackTheme: Boolean,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
@@ -94,10 +93,6 @@ fun ReVancedManagerTheme(
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}.let {
|
||||
if (darkTheme && pureBlackTheme)
|
||||
it.copy(background = Color.Black, surface = Color.Black)
|
||||
else it
|
||||
}
|
||||
|
||||
val view = LocalView.current
|
||||
|
||||
@@ -47,7 +47,7 @@ class AdvancedSettingsViewModel(
|
||||
app.contentResolver.openOutputStream(target)!!.bufferedWriter().use { writer ->
|
||||
val consumer = Redirect.Consume { flow ->
|
||||
flow.onEach {
|
||||
writer.write("${it}\n")
|
||||
writer.write(it)
|
||||
}.flowOn(Dispatchers.IO).collect()
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ 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
|
||||
@@ -11,6 +14,7 @@ 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
|
||||
@@ -27,7 +31,7 @@ class AppSelectorViewModel(
|
||||
private val app: Application,
|
||||
private val pm: PM,
|
||||
fs: Filesystem,
|
||||
patchBundleRepository: PatchBundleRepository,
|
||||
private val patchBundleRepository: PatchBundleRepository,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel() {
|
||||
private val inputFile = savedStateHandle.saveable(key = "inputFile") {
|
||||
@@ -38,19 +42,19 @@ class AppSelectorViewModel(
|
||||
}
|
||||
val appList = pm.appList
|
||||
|
||||
private val storageSelectionChannel = Channel<Pair<String, String>>()
|
||||
private val storageSelectionChannel = Channel<SelectedApp.Local>()
|
||||
val storageSelectionFlow = storageSelectionChannel.receiveAsFlow()
|
||||
|
||||
val suggestedAppVersions = patchBundleRepository.suggestedVersions.flowOn(Dispatchers.Default)
|
||||
|
||||
// var nonSuggestedVersionDialogSubject by mutableStateOf<SelectedApp.Local?>(null)
|
||||
// private set
|
||||
var nonSuggestedVersionDialogSubject by mutableStateOf<SelectedApp.Local?>(null)
|
||||
private set
|
||||
|
||||
fun loadLabel(app: PackageInfo?) = with(pm) { app?.label() ?: "Not installed" }
|
||||
|
||||
// fun dismissNonSuggestedVersionDialog() {
|
||||
// nonSuggestedVersionDialogSubject = null
|
||||
// }
|
||||
fun dismissNonSuggestedVersionDialog() {
|
||||
nonSuggestedVersionDialogSubject = null
|
||||
}
|
||||
|
||||
fun handleStorageResult(uri: Uri) = viewModelScope.launch {
|
||||
val selectedApp = withContext(Dispatchers.IO) {
|
||||
@@ -62,8 +66,11 @@ class AppSelectorViewModel(
|
||||
return@launch
|
||||
}
|
||||
|
||||
// TODO: Disallow if 0 patches are compatible
|
||||
storageSelectionChannel.send(selectedApp)
|
||||
if (patchBundleRepository.isVersionAllowed(selectedApp.packageName, selectedApp.version)) {
|
||||
storageSelectionChannel.send(selectedApp)
|
||||
} else {
|
||||
nonSuggestedVersionDialogSubject = selectedApp
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadSelectedFile(uri: Uri) =
|
||||
@@ -73,7 +80,12 @@ class AppSelectorViewModel(
|
||||
Files.copy(stream, toPath())
|
||||
|
||||
pm.getPackageInfo(this)?.let { packageInfo ->
|
||||
Pair(packageInfo.packageName, path)
|
||||
SelectedApp.Local(
|
||||
packageName = packageInfo.packageName,
|
||||
version = packageInfo.versionName!!,
|
||||
file = this,
|
||||
temporary = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.R
|
||||
@@ -13,6 +19,7 @@ import app.revanced.manager.data.room.apps.installed.InstallType
|
||||
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
||||
import app.revanced.manager.domain.installer.RootInstaller
|
||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||
import app.revanced.manager.service.UninstallService
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import app.revanced.manager.util.simpleMessage
|
||||
@@ -23,8 +30,6 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import ru.solrudev.ackpine.session.Session
|
||||
import ru.solrudev.ackpine.uninstaller.UninstallFailure
|
||||
|
||||
class InstalledAppInfoViewModel(
|
||||
packageName: String
|
||||
@@ -82,28 +87,51 @@ class InstalledAppInfoViewModel(
|
||||
|
||||
fun uninstall() {
|
||||
val app = installedApp ?: return
|
||||
viewModelScope.launch {
|
||||
when (app.installType) {
|
||||
InstallType.DEFAULT -> {
|
||||
when (val result = pm.uninstallPackage(app.currentPackageName)) {
|
||||
is Session.State.Failed<UninstallFailure> -> {
|
||||
val msg = result.failure.message.orEmpty()
|
||||
context.toast(
|
||||
this@InstalledAppInfoViewModel.context.getString(
|
||||
R.string.uninstall_app_fail,
|
||||
msg
|
||||
)
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
Session.State.Succeeded -> {}
|
||||
}
|
||||
}
|
||||
when (app.installType) {
|
||||
InstallType.DEFAULT -> pm.uninstallPackage(app.currentPackageName)
|
||||
|
||||
InstallType.MOUNT -> rootInstaller.uninstall(app.currentPackageName)
|
||||
InstallType.MOUNT -> viewModelScope.launch {
|
||||
rootInstaller.uninstall(app.currentPackageName)
|
||||
installedAppRepository.delete(app)
|
||||
onBackClick()
|
||||
}
|
||||
installedAppRepository.delete(app)
|
||||
onBackClick()
|
||||
}
|
||||
}
|
||||
|
||||
private val uninstallBroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
UninstallService.APP_UNINSTALL_ACTION -> {
|
||||
val extraStatus =
|
||||
intent.getIntExtra(UninstallService.EXTRA_UNINSTALL_STATUS, -999)
|
||||
val extraStatusMessage =
|
||||
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
||||
|
||||
if (extraStatus == PackageInstaller.STATUS_SUCCESS) {
|
||||
viewModelScope.launch {
|
||||
installedApp?.let {
|
||||
installedAppRepository.delete(it)
|
||||
}
|
||||
onBackClick()
|
||||
}
|
||||
} else if (extraStatus != PackageInstaller.STATUS_FAILURE_ABORTED) {
|
||||
this@InstalledAppInfoViewModel.context.toast(this@InstalledAppInfoViewModel.context.getString(R.string.uninstall_app_fail, extraStatusMessage))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}.also {
|
||||
ContextCompat.registerReceiver(
|
||||
context,
|
||||
it,
|
||||
IntentFilter(UninstallService.APP_UNINSTALL_ACTION),
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
context.unregisterReceiver(uninstallBroadcastReceiver)
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,11 @@ import app.revanced.manager.R
|
||||
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.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.theme.Theme
|
||||
import app.revanced.manager.util.tag
|
||||
import app.revanced.manager.util.toast
|
||||
@@ -29,14 +31,44 @@ import kotlinx.serialization.json.Json
|
||||
class MainViewModel(
|
||||
private val patchBundleRepository: PatchBundleRepository,
|
||||
private val patchSelectionRepository: PatchSelectionRepository,
|
||||
private val downloadedAppRepository: DownloadedAppRepository,
|
||||
private val keystoreManager: KeystoreManager,
|
||||
private val app: Application,
|
||||
val prefs: PreferencesManager,
|
||||
private val json: Json
|
||||
) : ViewModel() {
|
||||
private val appSelectChannel = Channel<SelectedApp>()
|
||||
val appSelectFlow = appSelectChannel.receiveAsFlow()
|
||||
private val legacyImportActivityChannel = Channel<Intent>()
|
||||
val legacyImportActivityFlow = legacyImportActivityChannel.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)))
|
||||
}
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
if (!prefs.firstLaunch.get()) return@launch
|
||||
@@ -90,7 +122,6 @@ class MainViewModel(
|
||||
}
|
||||
settings.usePrereleases?.let { prereleases ->
|
||||
prefs.useManagerPrereleases.update(prereleases)
|
||||
prefs.usePatchesPrereleases.update(prereleases)
|
||||
}
|
||||
settings.apiUrl?.let { api ->
|
||||
prefs.api.update(api.removeSuffix("/"))
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller as AndroidPackageInstaller
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.net.Uri
|
||||
import android.os.ParcelUuid
|
||||
import android.util.Log
|
||||
@@ -14,6 +16,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.autoSaver
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.toMutableStateList
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.map
|
||||
@@ -29,35 +32,32 @@ import app.revanced.manager.data.room.apps.installed.InstalledApp
|
||||
import app.revanced.manager.domain.installer.RootInstaller
|
||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||
import app.revanced.manager.domain.worker.WorkerRepository
|
||||
import app.revanced.manager.patcher.ProgressEvent
|
||||
import app.revanced.manager.patcher.StepId
|
||||
import app.revanced.manager.patcher.logger.LogLevel
|
||||
import app.revanced.manager.patcher.logger.Logger
|
||||
import app.revanced.manager.patcher.worker.PatcherWorker
|
||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||
import app.revanced.manager.plugin.downloader.UserInteractionException
|
||||
import app.revanced.manager.service.InstallService
|
||||
import app.revanced.manager.service.UninstallService
|
||||
import app.revanced.manager.ui.model.InstallerModel
|
||||
import app.revanced.manager.ui.model.SelectedSource
|
||||
import app.revanced.manager.ui.model.ProgressKey
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.model.State
|
||||
import app.revanced.manager.ui.model.StepCategory
|
||||
import app.revanced.manager.ui.model.Step
|
||||
import app.revanced.manager.ui.model.StepCategory
|
||||
import app.revanced.manager.ui.model.StepProgressProvider
|
||||
import app.revanced.manager.ui.model.navigation.Patcher
|
||||
import app.revanced.manager.ui.model.withState
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import app.revanced.manager.util.asCode
|
||||
import app.revanced.manager.util.saveableVar
|
||||
import app.revanced.manager.util.saver.snapshotStateListSaver
|
||||
import app.revanced.manager.util.simpleMessage
|
||||
import app.revanced.manager.util.tag
|
||||
import app.revanced.manager.util.toast
|
||||
import app.revanced.manager.util.uiSafe
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -66,15 +66,6 @@ import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.koin.core.component.inject
|
||||
import ru.solrudev.ackpine.installer.InstallFailure
|
||||
import ru.solrudev.ackpine.installer.PackageInstaller
|
||||
import ru.solrudev.ackpine.installer.createSession
|
||||
import ru.solrudev.ackpine.installer.getSession
|
||||
import ru.solrudev.ackpine.session.ProgressSession
|
||||
import ru.solrudev.ackpine.session.Session
|
||||
import ru.solrudev.ackpine.session.await
|
||||
import ru.solrudev.ackpine.session.parameters.Confirmation
|
||||
import ru.solrudev.ackpine.uninstaller.UninstallFailure
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.time.Duration
|
||||
@@ -82,7 +73,7 @@ import java.time.Duration
|
||||
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
|
||||
class PatcherViewModel(
|
||||
private val input: Patcher.ViewModelParams
|
||||
) : ViewModel(), KoinComponent, InstallerModel {
|
||||
) : ViewModel(), KoinComponent, StepProgressProvider, InstallerModel {
|
||||
private val app: Application by inject()
|
||||
private val fs: Filesystem by inject()
|
||||
private val pm: PM by inject()
|
||||
@@ -90,11 +81,11 @@ class PatcherViewModel(
|
||||
private val installedAppRepository: InstalledAppRepository by inject()
|
||||
private val rootInstaller: RootInstaller by inject()
|
||||
private val savedStateHandle: SavedStateHandle = get()
|
||||
private val ackpineInstaller: PackageInstaller = get()
|
||||
|
||||
private var installedApp: InstalledApp? = null
|
||||
val packageName = input.packageName
|
||||
val version = input.version
|
||||
private val selectedApp = input.selectedApp
|
||||
val packageName = selectedApp.packageName
|
||||
val version = selectedApp.version
|
||||
|
||||
var installedPackageName by savedStateHandle.saveable(
|
||||
key = "installedPackageName",
|
||||
@@ -104,6 +95,7 @@ class PatcherViewModel(
|
||||
mutableStateOf<String?>(null)
|
||||
}
|
||||
private set
|
||||
private var ongoingPmSession: Boolean by savedStateHandle.saveableVar { false }
|
||||
var packageInstallerStatus: Int? by savedStateHandle.saveable(
|
||||
key = "packageInstallerStatus",
|
||||
stateSaver = autoSaver()
|
||||
@@ -112,7 +104,7 @@ class PatcherViewModel(
|
||||
}
|
||||
private set
|
||||
|
||||
var isInstalling by mutableStateOf(false)
|
||||
var isInstalling by mutableStateOf(ongoingPmSession)
|
||||
private set
|
||||
|
||||
private var currentActivityRequest: Pair<CompletableDeferred<Boolean>, String>? by mutableStateOf(
|
||||
@@ -131,18 +123,6 @@ class PatcherViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This coroutine scope is used to await installations.
|
||||
* It should not be cancelled on system-initiated process death since that would cancel the installation process.
|
||||
*/
|
||||
private val installerCoroutineScope = CoroutineScope(Dispatchers.Main)
|
||||
|
||||
/**
|
||||
* Holds the package name of the Apk we are trying to install.
|
||||
*/
|
||||
private var installerPkgName: String by savedStateHandle.saveableVar { "" }
|
||||
private var installerSessionId: ParcelUuid? by savedStateHandle.saveableVar()
|
||||
|
||||
private var inputFile: File? by savedStateHandle.saveableVar()
|
||||
private val outputFile = tempDir.resolve("output.apk")
|
||||
|
||||
@@ -158,15 +138,35 @@ class PatcherViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) {
|
||||
generateSteps(app, input.selectedSource, input.selectedPatches).toMutableStateList()
|
||||
private val patchCount = input.selectedPatches.values.sumOf { it.size }
|
||||
private var completedPatchCount by savedStateHandle.saveable {
|
||||
// SavedStateHandle.saveable only supports the boxed version.
|
||||
@Suppress("AutoboxingStateCreation") mutableStateOf(
|
||||
0
|
||||
)
|
||||
}
|
||||
val patchesProgress get() = completedPatchCount to patchCount
|
||||
override var downloadProgress by savedStateHandle.saveable(
|
||||
key = "downloadProgress",
|
||||
stateSaver = autoSaver()
|
||||
) {
|
||||
mutableStateOf<Pair<Long, Long?>?>(null)
|
||||
}
|
||||
private set
|
||||
val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) {
|
||||
generateSteps(
|
||||
app,
|
||||
input.selectedApp
|
||||
).toMutableStateList()
|
||||
}
|
||||
private var currentStepIndex = 0
|
||||
|
||||
val progress by derivedStateOf {
|
||||
val steps = steps.filter { it.id != StepId.ExecutePatches }
|
||||
val current = steps.count {
|
||||
it.state == State.COMPLETED && it.category != StepCategory.PATCHING
|
||||
} + completedPatchCount
|
||||
|
||||
val current = steps.count { it.state == State.COMPLETED }
|
||||
val total = steps.size
|
||||
val total = steps.size - 1 + patchCount
|
||||
|
||||
current.toFloat() / total.toFloat()
|
||||
}
|
||||
@@ -174,48 +174,67 @@ class PatcherViewModel(
|
||||
private val workManager = WorkManager.getInstance(app)
|
||||
|
||||
private val patcherWorkerId by savedStateHandle.saveable<ParcelUuid> {
|
||||
ParcelUuid(
|
||||
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
||||
"patching", PatcherWorker.Args(
|
||||
input.packageName,
|
||||
input.version,
|
||||
input.selectedSource,
|
||||
outputFile.path,
|
||||
input.selectedPatches,
|
||||
input.options,
|
||||
logger,
|
||||
setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
|
||||
handleStartActivityRequest = { plugin, intent ->
|
||||
withContext(Dispatchers.Main) {
|
||||
if (currentActivityRequest != null) throw Exception("Another request is already pending.")
|
||||
try {
|
||||
// Wait for the dialog interaction.
|
||||
val accepted = with(CompletableDeferred<Boolean>()) {
|
||||
currentActivityRequest = this to plugin.name
|
||||
ParcelUuid(workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
||||
"patching", PatcherWorker.Args(
|
||||
input.selectedApp,
|
||||
outputFile.path,
|
||||
input.selectedPatches,
|
||||
input.options,
|
||||
logger,
|
||||
onDownloadProgress = {
|
||||
withContext(Dispatchers.Main) {
|
||||
downloadProgress = it
|
||||
}
|
||||
},
|
||||
onPatchCompleted = { withContext(Dispatchers.Main) { completedPatchCount += 1 } },
|
||||
setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
|
||||
handleStartActivityRequest = { plugin, intent ->
|
||||
withContext(Dispatchers.Main) {
|
||||
if (currentActivityRequest != null) throw Exception("Another request is already pending.")
|
||||
try {
|
||||
// Wait for the dialog interaction.
|
||||
val accepted = with(CompletableDeferred<Boolean>()) {
|
||||
currentActivityRequest = this to plugin.name
|
||||
|
||||
await()
|
||||
}
|
||||
if (!accepted) throw UserInteractionException.RequestDenied()
|
||||
|
||||
// Launch the activity and wait for the result.
|
||||
try {
|
||||
with(CompletableDeferred<ActivityResult>()) {
|
||||
launchedActivity = this
|
||||
launchActivityChannel.send(intent)
|
||||
await()
|
||||
}
|
||||
if (!accepted) throw UserInteractionException.RequestDenied()
|
||||
|
||||
// Launch the activity and wait for the result.
|
||||
try {
|
||||
with(CompletableDeferred<ActivityResult>()) {
|
||||
launchedActivity = this
|
||||
launchActivityChannel.send(intent)
|
||||
await()
|
||||
}
|
||||
} finally {
|
||||
launchedActivity = null
|
||||
}
|
||||
} finally {
|
||||
currentActivityRequest = null
|
||||
launchedActivity = null
|
||||
}
|
||||
} finally {
|
||||
currentActivityRequest = null
|
||||
}
|
||||
},
|
||||
onEvent = ::handleProgressEvent,
|
||||
)
|
||||
}
|
||||
},
|
||||
onProgress = { name, state, message ->
|
||||
viewModelScope.launch {
|
||||
steps[currentStepIndex] = steps[currentStepIndex].run {
|
||||
copy(
|
||||
name = name ?: this.name,
|
||||
state = state ?: this.state,
|
||||
message = message ?: this.message
|
||||
)
|
||||
}
|
||||
|
||||
if (state == State.COMPLETED && currentStepIndex != steps.lastIndex) {
|
||||
currentStepIndex++
|
||||
|
||||
steps[currentStepIndex] =
|
||||
steps[currentStepIndex].copy(state = State.RUNNING)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
val patcherSucceeded =
|
||||
@@ -227,26 +246,64 @@ class PatcherViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
// TODO: detect system-initiated process death during the patching process.
|
||||
private val installerBroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
InstallService.APP_INSTALL_ACTION -> {
|
||||
val pmStatus = intent.getIntExtra(
|
||||
InstallService.EXTRA_INSTALL_STATUS,
|
||||
PackageInstaller.STATUS_FAILURE
|
||||
)
|
||||
|
||||
installerSessionId?.uuid?.let { id ->
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
isInstalling = true
|
||||
uiSafe(app, R.string.install_app_fail, "Failed to install") {
|
||||
// The process was killed during installation. Await the session again.
|
||||
withContext(Dispatchers.IO) {
|
||||
ackpineInstaller.getSession(id)
|
||||
}?.let {
|
||||
awaitInstallation(it)
|
||||
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
||||
?.let(logger::trace)
|
||||
|
||||
if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
|
||||
app.toast(app.getString(R.string.install_app_success))
|
||||
installedPackageName =
|
||||
intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
|
||||
viewModelScope.launch {
|
||||
installedAppRepository.addOrUpdate(
|
||||
installedPackageName!!,
|
||||
packageName,
|
||||
input.selectedApp.version
|
||||
?: pm.getPackageInfo(outputFile)?.versionName!!,
|
||||
InstallType.DEFAULT,
|
||||
input.selectedPatches
|
||||
)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
} else packageInstallerStatus = pmStatus
|
||||
|
||||
isInstalling = false
|
||||
}
|
||||
|
||||
UninstallService.APP_UNINSTALL_ACTION -> {
|
||||
val pmStatus = intent.getIntExtra(
|
||||
UninstallService.EXTRA_UNINSTALL_STATUS,
|
||||
PackageInstaller.STATUS_FAILURE
|
||||
)
|
||||
|
||||
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
||||
?.let(logger::trace)
|
||||
|
||||
if (pmStatus != PackageInstaller.STATUS_SUCCESS)
|
||||
packageInstallerStatus = pmStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
// TODO: detect system-initiated process death during the patching process.
|
||||
ContextCompat.registerReceiver(
|
||||
app,
|
||||
installerBroadcastReceiver,
|
||||
IntentFilter().apply {
|
||||
addAction(InstallService.APP_INSTALL_ACTION)
|
||||
addAction(UninstallService.APP_UNINSTALL_ACTION)
|
||||
},
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
)
|
||||
|
||||
viewModelScope.launch {
|
||||
installedApp = installedAppRepository.get(packageName)
|
||||
@@ -256,9 +313,10 @@ class PatcherViewModel(
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
app.unregisterReceiver(installerBroadcastReceiver)
|
||||
workManager.cancelWorkById(patcherWorkerId.uuid)
|
||||
|
||||
if (input.selectedSource is SelectedSource.Installed && installedApp?.installType == InstallType.MOUNT) {
|
||||
if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
uiSafe(app, R.string.failed_to_mount, "Failed to mount") {
|
||||
withTimeout(Duration.ofMinutes(1L)) {
|
||||
@@ -269,37 +327,7 @@ class PatcherViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleProgressEvent(event: ProgressEvent) = viewModelScope.launch {
|
||||
val stepIndex = steps.indexOfFirst {
|
||||
event.stepId?.let { id -> id == it.id }
|
||||
?: (it.state == State.RUNNING || it.state == State.WAITING)
|
||||
}
|
||||
|
||||
if (stepIndex != -1) steps[stepIndex] = steps[stepIndex].run {
|
||||
when (event) {
|
||||
is ProgressEvent.Started -> withState(State.RUNNING)
|
||||
|
||||
is ProgressEvent.Progress -> withState(
|
||||
message = event.message ?: message,
|
||||
progress = event.current?.let { event.current to event.total } ?: progress
|
||||
)
|
||||
|
||||
is ProgressEvent.Completed -> withState(State.COMPLETED, progress = null)
|
||||
|
||||
is ProgressEvent.Failed -> {
|
||||
if (event.stepId == null && steps.any { it.state == State.FAILED }) return@launch
|
||||
withState(
|
||||
State.FAILED,
|
||||
message = event.error.stackTrace,
|
||||
progress = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onBack() {
|
||||
installerCoroutineScope.cancel()
|
||||
// tempDir cannot be deleted inside onCleared because it gets called on system-initiated process death.
|
||||
tempDir.deleteRecursively()
|
||||
}
|
||||
@@ -344,93 +372,44 @@ class PatcherViewModel(
|
||||
|
||||
fun open() = installedPackageName?.let(pm::launch)
|
||||
|
||||
private suspend fun startInstallation(file: File, packageName: String) {
|
||||
val session = withContext(Dispatchers.IO) {
|
||||
ackpineInstaller.createSession(Uri.fromFile(file)) {
|
||||
confirmation = Confirmation.IMMEDIATE
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
installerPkgName = packageName
|
||||
}
|
||||
awaitInstallation(session)
|
||||
}
|
||||
|
||||
private suspend fun awaitInstallation(session: ProgressSession<InstallFailure>) = withContext(
|
||||
Dispatchers.Main
|
||||
) {
|
||||
val result = installerCoroutineScope.async {
|
||||
try {
|
||||
installerSessionId = ParcelUuid(session.id)
|
||||
withContext(Dispatchers.IO) {
|
||||
session.await()
|
||||
}
|
||||
} finally {
|
||||
installerSessionId = null
|
||||
}
|
||||
}.await()
|
||||
|
||||
when (result) {
|
||||
is Session.State.Failed<InstallFailure> -> {
|
||||
result.failure.message?.let(logger::trace)
|
||||
packageInstallerStatus = result.failure.asCode()
|
||||
}
|
||||
|
||||
Session.State.Succeeded -> {
|
||||
app.toast(app.getString(R.string.install_app_success))
|
||||
installedPackageName = installerPkgName
|
||||
installedAppRepository.addOrUpdate(
|
||||
installerPkgName,
|
||||
packageName,
|
||||
input.version
|
||||
?: withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile)?.versionName!! },
|
||||
InstallType.DEFAULT,
|
||||
input.selectedPatches
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun install(installType: InstallType) = viewModelScope.launch {
|
||||
isInstalling = true
|
||||
var needsRootUninstall = false
|
||||
var pmInstallStarted = false
|
||||
try {
|
||||
uiSafe(app, R.string.install_app_fail, "Failed to install") {
|
||||
val currentPackageInfo =
|
||||
withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile) }
|
||||
?: throw Exception("Failed to load application info")
|
||||
isInstalling = true
|
||||
|
||||
// If the app is currently installed
|
||||
val existingPackageInfo =
|
||||
withContext(Dispatchers.IO) { pm.getPackageInfo(currentPackageInfo.packageName) }
|
||||
if (existingPackageInfo != null) {
|
||||
// Check if the app version is less than the installed version
|
||||
if (
|
||||
pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(
|
||||
existingPackageInfo
|
||||
)
|
||||
) {
|
||||
// Exit if the selected app version is less than the installed version
|
||||
packageInstallerStatus = AndroidPackageInstaller.STATUS_FAILURE_CONFLICT
|
||||
return@launch
|
||||
val currentPackageInfo = pm.getPackageInfo(outputFile)
|
||||
?: throw Exception("Failed to load application info")
|
||||
|
||||
// If the app is currently installed
|
||||
val existingPackageInfo = pm.getPackageInfo(currentPackageInfo.packageName)
|
||||
if (existingPackageInfo != null) {
|
||||
// Check if the app version is less than the installed version
|
||||
if (pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(existingPackageInfo)) {
|
||||
// Exit if the selected app version is less than the installed version
|
||||
packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
when (installType) {
|
||||
InstallType.DEFAULT -> {
|
||||
// Check if the app is mounted as root
|
||||
// If it is, unmount it first, silently
|
||||
if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) {
|
||||
rootInstaller.unmount(packageName)
|
||||
}
|
||||
|
||||
// Install regularly
|
||||
pm.installApp(listOf(outputFile))
|
||||
pmInstallStarted = true
|
||||
}
|
||||
|
||||
when (installType) {
|
||||
InstallType.DEFAULT -> {
|
||||
// Check if the app is mounted as root
|
||||
// If it is, unmount it first, silently
|
||||
if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) {
|
||||
rootInstaller.unmount(packageName)
|
||||
}
|
||||
|
||||
// Install regularly
|
||||
startInstallation(outputFile, currentPackageInfo.packageName)
|
||||
}
|
||||
|
||||
InstallType.MOUNT -> {
|
||||
InstallType.MOUNT -> {
|
||||
try {
|
||||
val packageInfo = pm.getPackageInfo(outputFile)
|
||||
?: throw Exception("Failed to load application info")
|
||||
val label = with(pm) {
|
||||
currentPackageInfo.label()
|
||||
packageInfo.label()
|
||||
}
|
||||
|
||||
// Check for base APK, first check if the app is already installed
|
||||
@@ -438,17 +417,15 @@ class PatcherViewModel(
|
||||
// If the app is not installed, check if the output file is a base apk
|
||||
if (currentPackageInfo.splitNames.isNotEmpty()) {
|
||||
// Exit if there is no base APK package
|
||||
packageInstallerStatus =
|
||||
AndroidPackageInstaller.STATUS_FAILURE_INVALID
|
||||
packageInstallerStatus = PackageInstaller.STATUS_FAILURE_INVALID
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
val inputVersion = input.version
|
||||
?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName }
|
||||
val inputVersion = input.selectedApp.version
|
||||
?: inputFile?.let(pm::getPackageInfo)?.versionName
|
||||
?: throw Exception("Failed to determine input APK version")
|
||||
|
||||
needsRootUninstall = true
|
||||
// Install as root
|
||||
rootInstaller.install(
|
||||
outputFile,
|
||||
@@ -459,7 +436,7 @@ class PatcherViewModel(
|
||||
)
|
||||
|
||||
installedAppRepository.addOrUpdate(
|
||||
currentPackageInfo.packageName,
|
||||
packageInfo.packageName,
|
||||
packageName,
|
||||
inputVersion,
|
||||
InstallType.MOUNT,
|
||||
@@ -471,20 +448,21 @@ class PatcherViewModel(
|
||||
installedPackageName = packageName
|
||||
|
||||
app.toast(app.getString(R.string.install_app_success))
|
||||
needsRootUninstall = false
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Failed to install as root", e)
|
||||
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
|
||||
try {
|
||||
rootInstaller.uninstall(packageName)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Failed to install", e)
|
||||
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
|
||||
} finally {
|
||||
isInstalling = false
|
||||
if (needsRootUninstall) {
|
||||
try {
|
||||
withContext(NonCancellable) {
|
||||
rootInstaller.uninstall(packageName)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
if (!pmInstallStarted) isInstalling = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,27 +473,12 @@ class PatcherViewModel(
|
||||
|
||||
override fun reinstall() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") {
|
||||
pm.getPackageInfo(outputFile)?.packageName?.let { pm.uninstallPackage(it) }
|
||||
?: throw Exception("Failed to load application info")
|
||||
|
||||
pm.installApp(listOf(outputFile))
|
||||
isInstalling = true
|
||||
uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") {
|
||||
val pkgName = withContext(Dispatchers.IO) {
|
||||
pm.getPackageInfo(outputFile)?.packageName
|
||||
?: throw Exception("Failed to load application info")
|
||||
}
|
||||
|
||||
when (val result = pm.uninstallPackage(pkgName)) {
|
||||
is Session.State.Failed<UninstallFailure> -> {
|
||||
result.failure.message?.let(logger::trace)
|
||||
packageInstallerStatus = result.failure.asCode()
|
||||
return@launch
|
||||
}
|
||||
|
||||
Session.State.Succeeded -> {}
|
||||
}
|
||||
startInstallation(outputFile, pkgName)
|
||||
}
|
||||
} finally {
|
||||
isInstalling = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -534,66 +497,34 @@ class PatcherViewModel(
|
||||
LogLevel.ERROR -> Log.e(TAG, msg)
|
||||
}
|
||||
|
||||
fun generateSteps(
|
||||
context: Context,
|
||||
selectedSource: SelectedSource,
|
||||
selectedPatches: PatchSelection
|
||||
): List<Step> = buildList {
|
||||
if (selectedSource is SelectedSource.Plugin)
|
||||
add(
|
||||
Step(
|
||||
StepId.DownloadAPK,
|
||||
context.getString(R.string.download_apk),
|
||||
StepCategory.PREPARING
|
||||
)
|
||||
)
|
||||
fun generateSteps(context: Context, selectedApp: SelectedApp): List<Step> {
|
||||
val needsDownload =
|
||||
selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search
|
||||
|
||||
add(
|
||||
return listOfNotNull(
|
||||
Step(
|
||||
context.getString(R.string.download_apk),
|
||||
StepCategory.PREPARING,
|
||||
state = State.RUNNING,
|
||||
progressKey = ProgressKey.DOWNLOAD,
|
||||
).takeIf { needsDownload },
|
||||
Step(
|
||||
StepId.LoadPatches,
|
||||
context.getString(R.string.patcher_step_load_patches),
|
||||
StepCategory.PREPARING
|
||||
)
|
||||
)
|
||||
add(
|
||||
StepCategory.PREPARING,
|
||||
state = if (needsDownload) State.WAITING else State.RUNNING,
|
||||
),
|
||||
Step(
|
||||
StepId.ReadAPK,
|
||||
context.getString(R.string.patcher_step_unpack),
|
||||
StepCategory.PREPARING
|
||||
)
|
||||
)
|
||||
add(
|
||||
),
|
||||
|
||||
Step(
|
||||
StepId.ExecutePatches,
|
||||
context.getString(R.string.execute_patches),
|
||||
StepCategory.PATCHING,
|
||||
hide = true
|
||||
)
|
||||
)
|
||||
StepCategory.PATCHING
|
||||
),
|
||||
|
||||
selectedPatches.values.asSequence().flatten().sorted().forEachIndexed { index, name ->
|
||||
add(
|
||||
Step(
|
||||
StepId.ExecutePatch(index),
|
||||
name,
|
||||
StepCategory.PATCHING
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
add(
|
||||
Step(
|
||||
StepId.WriteAPK,
|
||||
context.getString(R.string.patcher_step_write_patched),
|
||||
StepCategory.SAVING
|
||||
)
|
||||
)
|
||||
add(
|
||||
Step(
|
||||
StepId.SignAPK,
|
||||
context.getString(R.string.patcher_step_sign_apk),
|
||||
StepCategory.SAVING
|
||||
)
|
||||
Step(context.getString(R.string.patcher_step_write_patched), StepCategory.SAVING),
|
||||
Step(context.getString(R.string.patcher_step_sign_apk), StepCategory.SAVING)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.SelectedAppInfo
|
||||
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
||||
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: SelectedAppInfo.PatchesSelector.ViewModelParams) :
|
||||
class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.ViewModelParams) :
|
||||
ViewModel(), KoinComponent {
|
||||
private val app: Application = get()
|
||||
private val savedStateHandle: SavedStateHandle = get()
|
||||
private val prefs: PreferencesManager = get()
|
||||
|
||||
private val packageName = input.packageName
|
||||
val appVersion = input.version
|
||||
private val packageName = input.app.packageName
|
||||
val appVersion = input.app.version
|
||||
|
||||
var selectionWarningEnabled by mutableStateOf(true)
|
||||
private set
|
||||
@@ -62,7 +62,7 @@ class PatchesSelectorViewModel(input: SelectedAppInfo.PatchesSelector.ViewModelP
|
||||
val allowIncompatiblePatches =
|
||||
get<PreferencesManager>().disablePatchVersionCompatCheck.getBlocking()
|
||||
val bundlesFlow =
|
||||
get<PatchBundleRepository>().scopedBundleInfoFlow(packageName, input.version)
|
||||
get<PatchBundleRepository>().scopedBundleInfoFlow(packageName, input.app.version)
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
@@ -88,7 +88,7 @@ class PatchesSelectorViewModel(input: SelectedAppInfo.PatchesSelector.ViewModelP
|
||||
key = "selection",
|
||||
stateSaver = selectionSaver,
|
||||
) {
|
||||
mutableStateOf(input.patchSelection?.toPersistentPatchSelection())
|
||||
mutableStateOf(input.currentSelection?.toPersistentPatchSelection())
|
||||
}
|
||||
|
||||
private val patchOptions: PersistentOptions by savedStateHandle.saveable(
|
||||
|
||||
@@ -1,223 +1,91 @@
|
||||
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.MutableState
|
||||
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.domain.installer.RootInstaller
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
||||
import app.revanced.manager.domain.repository.DownloaderPluginRepository
|
||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.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.requiredOptionsSet
|
||||
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection
|
||||
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||
import app.revanced.manager.network.downloader.ParceledDownloaderData
|
||||
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.requiredOptionsSet
|
||||
import app.revanced.manager.plugin.downloader.GetScope
|
||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||
import app.revanced.manager.ui.model.SelectedSource
|
||||
import app.revanced.manager.ui.model.SelectedVersion
|
||||
import app.revanced.manager.plugin.downloader.UserInteractionException
|
||||
import app.revanced.manager.ui.model.SelectedApp
|
||||
import app.revanced.manager.ui.model.navigation.Patcher
|
||||
import app.revanced.manager.ui.model.navigation.SelectedAppInfo
|
||||
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.PatchSelection
|
||||
import app.revanced.manager.util.patchCount
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
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, PluginHostApi::class)
|
||||
class SelectedAppInfoViewModel(
|
||||
private val input: SelectedAppInfo.ViewModelParams
|
||||
input: SelectedApplicationInfo.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 pluginsRepository: DownloaderPluginRepository = 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()
|
||||
private val prefs: PreferencesManager = get()
|
||||
val prefs: PreferencesManager = get()
|
||||
val plugins = pluginsRepository.loadedPluginsFlow
|
||||
val packageName = input.packageName
|
||||
val localPath = input.localPath
|
||||
val desiredVersion = input.app.version
|
||||
val packageName = input.app.packageName
|
||||
|
||||
private val persistConfiguration = input.patches == null
|
||||
|
||||
|
||||
// User selection
|
||||
private var selectionFlow = MutableStateFlow(
|
||||
input.patches?.let { selection ->
|
||||
SelectionState.Customized(selection)
|
||||
} ?: SelectionState.Default
|
||||
)
|
||||
|
||||
private val _selectedVersion = MutableStateFlow<SelectedVersion>(SelectedVersion.Auto)
|
||||
val selectedVersion: StateFlow<SelectedVersion> = _selectedVersion
|
||||
|
||||
private val _selectedSource = MutableStateFlow<SelectedSource>(SelectedSource.Auto)
|
||||
val selectedSource: StateFlow<SelectedSource> = _selectedSource
|
||||
|
||||
fun updateVersion(version: SelectedVersion) {
|
||||
_selectedVersion.value = version
|
||||
}
|
||||
fun updateSource(source: SelectedSource) {
|
||||
_selectedSource.value = source
|
||||
}
|
||||
fun updateConfiguration(
|
||||
selection: PatchSelection?,
|
||||
selectedOptions: Options
|
||||
) = viewModelScope.launch {
|
||||
selectionFlow.value = selection?.let(SelectionState::Customized) ?: SelectionState.Default
|
||||
|
||||
val filteredOptions = selectedOptions.filtered(bundleInfoFlow.first())
|
||||
options = filteredOptions
|
||||
|
||||
if (persistConfiguration) {
|
||||
selection?.let { selectionRepository.updateSelection(packageName, it) }
|
||||
?: selectionRepository.resetSelectionForPackage(packageName)
|
||||
|
||||
optionsRepository.saveOptions(packageName, filteredOptions)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// All patches for package
|
||||
val bundles = bundleRepository.scopedBundleInfoFlow(packageName, null)
|
||||
|
||||
// Selection derived from selectionFlow
|
||||
val patchSelection = combine(
|
||||
selectionFlow,
|
||||
bundles,
|
||||
) { selection, bundles ->
|
||||
selection.patches(bundles, allowIncompatible = true)
|
||||
}
|
||||
|
||||
val customSelection = combine(
|
||||
selectionFlow,
|
||||
bundles,
|
||||
) { selection, bundles ->
|
||||
(selection as? SelectionState.Customized)?.patches(bundles, allowIncompatible = true)
|
||||
}
|
||||
|
||||
// Most compatible versions based on patch selection
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val mostCompatibleVersions = patchSelection.flatMapLatest { patchSelection ->
|
||||
bundleRepository.suggestedVersions(
|
||||
packageName,
|
||||
patchSelection
|
||||
)
|
||||
}
|
||||
|
||||
// Resolve actual version from user selection
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val scopedBundles = resolvedVersion.flatMapLatest { version ->
|
||||
bundleRepository.scopedBundleInfoFlow(packageName, version)
|
||||
}
|
||||
|
||||
val incompatiblePatchCount = scopedBundles.map { bundles ->
|
||||
bundles.sumOf { bundle ->
|
||||
bundle.incompatible.size
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve actual source from user selection
|
||||
val resolvedSource = combine(
|
||||
_selectedSource,
|
||||
resolvedVersion
|
||||
) { source, version ->
|
||||
when (source) {
|
||||
is SelectedSource.Installed -> source
|
||||
is SelectedSource.Local -> source
|
||||
is SelectedSource.Downloaded -> source
|
||||
is SelectedSource.Plugin -> source
|
||||
is SelectedSource.Auto -> {
|
||||
val app = version?.let {
|
||||
downloadedAppRepository.get(packageName, it)
|
||||
}
|
||||
val file = app?.let {
|
||||
downloadedAppRepository.getApkFileForApp(it)
|
||||
}
|
||||
|
||||
file?.let { SelectedSource.Downloaded(it.path, version) }
|
||||
?: SelectedSource.Plugin(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val bundleInfoFlow by derivedStateOf {
|
||||
bundleRepository.scopedBundleInfoFlow(packageName, null)
|
||||
}
|
||||
|
||||
var options: Options by savedStateHandle.saveable {
|
||||
viewModelScope.launch {
|
||||
if (!persistConfiguration) return@launch // TODO: save options for patched apps.
|
||||
val bundlePatches = bundleInfoFlow.first()
|
||||
.associate { it.uid to it.patches.associateBy { patch -> patch.name } }
|
||||
|
||||
options = withContext(Dispatchers.Default) {
|
||||
optionsRepository.getOptions(packageName, bundlePatches)
|
||||
}
|
||||
}
|
||||
|
||||
mutableStateOf(emptyMap())
|
||||
}
|
||||
val hasRoot = rootInstaller.hasRootAccess()
|
||||
var installedAppData: Pair<SelectedApp.Installed, InstalledApp?>? by mutableStateOf(null)
|
||||
private set
|
||||
|
||||
|
||||
val errorFlow = combine(
|
||||
plugins,
|
||||
resolvedSource,
|
||||
) { pluginsList, source ->
|
||||
when {
|
||||
source is SelectedSource.Plugin && pluginsList.isEmpty() -> Error.NoPlugins
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// var installedAppData: Pair<SelectedApp.Installed, InstalledApp?>? by mutableStateOf(null)
|
||||
// private set
|
||||
|
||||
private var _selectedApp by savedStateHandle.saveable {
|
||||
mutableStateOf(null)
|
||||
mutableStateOf(input.app)
|
||||
}
|
||||
|
||||
var selectedAppInfo: PackageInfo? by mutableStateOf(null)
|
||||
@@ -230,12 +98,170 @@ class SelectedAppInfoViewModel(
|
||||
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 requiredVersion = combine(
|
||||
prefs.suggestedVersionSafeguard.flow,
|
||||
bundleRepository.suggestedVersions
|
||||
) { suggestedVersionSafeguard, suggestedVersions ->
|
||||
if (!suggestedVersionSafeguard) return@combine null
|
||||
|
||||
suggestedVersions[input.app.packageName]
|
||||
}
|
||||
|
||||
val bundleInfoFlow by derivedStateOf {
|
||||
bundleRepository.scopedBundleInfoFlow(packageName, selectedApp.version)
|
||||
}
|
||||
|
||||
var options: Options by savedStateHandle.saveable {
|
||||
val state = mutableStateOf<Options>(emptyMap())
|
||||
|
||||
viewModelScope.launch {
|
||||
if (!persistConfiguration) return@launch // TODO: save options for patched apps.
|
||||
val bundlePatches = bundleInfoFlow.first()
|
||||
.associate { it.uid to it.patches.associateBy { patch -> patch.name } }
|
||||
|
||||
options = withContext(Dispatchers.Default) {
|
||||
optionsRepository.getOptions(packageName, bundlePatches)
|
||||
}
|
||||
}
|
||||
|
||||
state
|
||||
}
|
||||
private set
|
||||
|
||||
private var selectionState: SelectionState by savedStateHandle.saveable {
|
||||
if (input.patches != null)
|
||||
return@saveable mutableStateOf(SelectionState.Customized(input.patches))
|
||||
|
||||
val selection: MutableState<SelectionState> = mutableStateOf(SelectionState.Default)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
selection
|
||||
}
|
||||
|
||||
var showSourceSelector by mutableStateOf(false)
|
||||
private set
|
||||
private var pluginAction: Pair<LoadedDownloaderPlugin, Job>? by mutableStateOf(null)
|
||||
val activePluginAction get() = pluginAction?.first?.packageName
|
||||
private var launchedActivity by mutableStateOf<CompletableDeferred<ActivityResult>?>(null)
|
||||
private val launchActivityChannel = Channel<Intent>()
|
||||
val launchActivityFlow = launchActivityChannel.receiveAsFlow()
|
||||
|
||||
val errorFlow = combine(plugins, snapshotFlow { selectedApp }) { pluginsList, app ->
|
||||
when {
|
||||
app is SelectedApp.Search && pluginsList.isEmpty() -> Error.NoPlugins
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun showSourceSelector() {
|
||||
dismissSourceSelector()
|
||||
showSourceSelector = true
|
||||
}
|
||||
|
||||
private fun cancelPluginAction() {
|
||||
pluginAction?.second?.cancel()
|
||||
pluginAction = null
|
||||
}
|
||||
|
||||
fun dismissSourceSelector() {
|
||||
cancelPluginAction()
|
||||
showSourceSelector = false
|
||||
}
|
||||
|
||||
fun searchUsingPlugin(plugin: LoadedDownloaderPlugin) {
|
||||
cancelPluginAction()
|
||||
pluginAction = plugin to viewModelScope.launch {
|
||||
try {
|
||||
val scope = object : GetScope {
|
||||
override val hostPackageName = app.packageName
|
||||
override val pluginPackageName = plugin.packageName
|
||||
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) {
|
||||
plugin.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(plugin, 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 {
|
||||
pluginAction = null
|
||||
dismissSourceSelector()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handlePluginActivityResult(result: ActivityResult) {
|
||||
launchedActivity?.complete(result)
|
||||
}
|
||||
|
||||
// TODO: Load from local file or downloaded app
|
||||
private fun invalidateSelectedAppInfo() = viewModelScope.launch {
|
||||
selectedAppInfo = pm.getPackageInfo(packageName)
|
||||
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
|
||||
}
|
||||
|
||||
fun getOptionsFiltered(bundles: List<PatchBundleInfo.Scoped>) = options.filtered(bundles)
|
||||
@@ -251,54 +277,41 @@ class SelectedAppInfoViewModel(
|
||||
val allowIncompatible = prefs.disablePatchVersionCompatCheck.get()
|
||||
val bundles = bundleInfoFlow.first()
|
||||
return Patcher.ViewModelParams(
|
||||
input.packageName,
|
||||
resolvedVersion.first(),
|
||||
resolvedSource.first(),
|
||||
patchSelection.first(),
|
||||
selectedApp,
|
||||
getPatches(bundles, allowIncompatible),
|
||||
getOptionsFiltered(bundles)
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
invalidateSelectedAppInfo()
|
||||
fun getPatches(bundles: List<PatchBundleInfo.Scoped>, allowIncompatible: Boolean) =
|
||||
selectionState.patches(bundles, allowIncompatible)
|
||||
|
||||
input.localPath?.let { local ->
|
||||
viewModelScope.launch {
|
||||
val packageInfo = pm.getPackageInfo(File(local))
|
||||
fun getCustomPatches(
|
||||
bundles: List<PatchBundleInfo.Scoped>,
|
||||
allowIncompatible: Boolean
|
||||
): PatchSelection? =
|
||||
(selectionState as? SelectionState.Customized)?.patches(bundles, allowIncompatible)
|
||||
|
||||
_selectedVersion.value = SelectedVersion.Specific(
|
||||
packageInfo?.versionName ?: return@launch
|
||||
)
|
||||
_selectedSource.value = SelectedSource.Local(local)
|
||||
}
|
||||
}
|
||||
|
||||
// Get the previous selection if customization is enabled.
|
||||
viewModelScope.launch {
|
||||
if (prefs.disableSelectionWarning.get()) {
|
||||
val previous = selectionRepository.getSelection(packageName)
|
||||
if (previous.patchCount == 0) return@launch
|
||||
selectionFlow.value = SelectionState.Customized(previous)
|
||||
}
|
||||
}
|
||||
fun updateConfiguration(
|
||||
selection: PatchSelection?,
|
||||
options: Options
|
||||
) = viewModelScope.launch {
|
||||
selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default
|
||||
|
||||
// Get installed app info
|
||||
viewModelScope.launch {
|
||||
val packageInfo = async(Dispatchers.IO) { pm.getPackageInfo(packageName) }
|
||||
val installedAppDeferred =
|
||||
async(Dispatchers.IO) { installedAppRepository.get(packageName) }
|
||||
val filteredOptions = options.filtered(bundleInfoFlow.first())
|
||||
this@SelectedAppInfoViewModel.options = filteredOptions
|
||||
|
||||
// installedAppData =
|
||||
// packageInfo.await()?.let {
|
||||
// SelectedApp.Installed(
|
||||
// packageName,
|
||||
// it.versionName!!
|
||||
// ) to installedAppDeferred.await()
|
||||
// }
|
||||
if (!persistConfiguration) return@launch
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
selection?.let { selectionRepository.updateSelection(packageName, it) }
|
||||
?: selectionRepository.resetSelectionForPackage(packageName)
|
||||
|
||||
optionsRepository.saveOptions(packageName, filteredOptions)
|
||||
}
|
||||
}
|
||||
|
||||
enum class Error(@param:StringRes val resourceId: Int) {
|
||||
enum class Error(@StringRes val resourceId: Int) {
|
||||
NoPlugins(R.string.downloader_no_plugins_available)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
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.domain.repository.DownloadedAppRepository
|
||||
import app.revanced.manager.domain.repository.DownloaderPluginRepository
|
||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||
import app.revanced.manager.network.downloader.DownloaderPluginState
|
||||
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 app: Application = get()
|
||||
private val downloadedAppRepository: DownloadedAppRepository = get()
|
||||
private val pluginRepository: DownloaderPluginRepository = get()
|
||||
private val installedAppRepository: InstalledAppRepository = get()
|
||||
private val pm: PM = get()
|
||||
|
||||
var selectedSource by mutableStateOf(input.selectedSource)
|
||||
private set
|
||||
|
||||
fun selectSource(source: SelectedSource) {
|
||||
selectedSource = source
|
||||
}
|
||||
|
||||
var localApp by mutableStateOf<SourceOption?>(null)
|
||||
private set
|
||||
|
||||
val downloadedApps = downloadedAppRepository.get(input.packageName)
|
||||
.map { apps ->
|
||||
apps.sortedByDescending { app -> app.version }
|
||||
.map {
|
||||
SourceOption(
|
||||
source = SelectedSource.Downloaded(
|
||||
path = downloadedAppRepository.getApkFileForApp(it).path,
|
||||
version = it.version
|
||||
),
|
||||
title = it.version,
|
||||
category = "Downloaded",
|
||||
key = it.version,
|
||||
disableReason = if (input.version != null && it.version != input.version) {
|
||||
DisableReason.VERSION_NOT_MATCHING
|
||||
} else null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val plugins = pluginRepository.pluginStates.map { plugins ->
|
||||
plugins.toList().sortedByDescending { it.second is DownloaderPluginState.Loaded }
|
||||
.map {
|
||||
val packageInfo = pm.getPackageInfo(it.first)
|
||||
val label = packageInfo?.applicationInfo?.loadLabel(app.packageManager)
|
||||
?.toString()
|
||||
|
||||
SourceOption(
|
||||
source = SelectedSource.Plugin(it.first),
|
||||
title = label ?: it.first,
|
||||
category = "Plugin",
|
||||
key = it.first,
|
||||
disableReason = when (it.second) {
|
||||
is DownloaderPluginState.Loaded -> null
|
||||
is DownloaderPluginState.Untrusted -> DisableReason.NOT_TRUSTED
|
||||
is DownloaderPluginState.Failed -> DisableReason.FAILED_TO_LOAD
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getPackageInfo(packageName: String) = pm.getPackageInfo(packageName)
|
||||
|
||||
var installedSource by mutableStateOf<SourceOption?>(null)
|
||||
private set
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
val packageInfo = pm.getPackageInfo(input.packageName) ?: return@launch
|
||||
|
||||
val installedApp = installedAppRepository.get(input.packageName)
|
||||
|
||||
installedSource = SourceOption(
|
||||
source = SelectedSource.Installed,
|
||||
title = packageInfo.versionName.toString(),
|
||||
category = "Installed",
|
||||
key = input.packageName,
|
||||
disableReason = when {
|
||||
installedApp != null -> DisableReason.ALREADY_PATCHED
|
||||
input.version != null && packageInfo.versionName != input.version -> DisableReason.VERSION_NOT_MATCHING
|
||||
else -> null
|
||||
}
|
||||
)
|
||||
}
|
||||
input.localPath?.let { local ->
|
||||
viewModelScope.launch {
|
||||
val packageInfo = pm.getPackageInfo(File(local))
|
||||
?: return@launch
|
||||
|
||||
localApp = SourceOption(
|
||||
source = SelectedSource.Local(local),
|
||||
title = packageInfo.versionName.toString(),
|
||||
category = "Local",
|
||||
key = "local",
|
||||
disableReason = if (input.version != null && packageInfo.versionName != input.version) {
|
||||
DisableReason.VERSION_NOT_MATCHING
|
||||
} else null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class DisableReason(val message: String) {
|
||||
VERSION_NOT_MATCHING("Does not match the selected version"),
|
||||
ALREADY_PATCHED("Already patched"),
|
||||
NOT_TRUSTED("Not trusted"),
|
||||
FAILED_TO_LOAD("Failed to load"),
|
||||
}
|
||||
|
||||
data class SourceOption(
|
||||
val source: SelectedSource,
|
||||
val title: String,
|
||||
val category: String? = null,
|
||||
val key: String,
|
||||
val disableReason: DisableReason? = null
|
||||
)
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageInstaller
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.R
|
||||
@@ -16,6 +21,8 @@ import app.revanced.manager.data.platform.NetworkInfo
|
||||
import app.revanced.manager.network.api.ReVancedAPI
|
||||
import app.revanced.manager.network.dto.ReVancedAsset
|
||||
import app.revanced.manager.network.service.HttpService
|
||||
import app.revanced.manager.service.InstallService
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.toast
|
||||
import app.revanced.manager.util.uiSafe
|
||||
import io.ktor.client.plugins.onDownload
|
||||
@@ -24,14 +31,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.koin.core.component.inject
|
||||
import ru.solrudev.ackpine.installer.InstallFailure
|
||||
import ru.solrudev.ackpine.installer.PackageInstaller
|
||||
import ru.solrudev.ackpine.installer.createSession
|
||||
import ru.solrudev.ackpine.session.Session
|
||||
import ru.solrudev.ackpine.session.await
|
||||
import ru.solrudev.ackpine.session.parameters.Confirmation
|
||||
|
||||
class UpdateViewModel(
|
||||
private val downloadOnScreenEntry: Boolean
|
||||
@@ -39,11 +39,10 @@ class UpdateViewModel(
|
||||
private val app: Application by inject()
|
||||
private val reVancedAPI: ReVancedAPI by inject()
|
||||
private val http: HttpService by inject()
|
||||
private val pm: PM by inject()
|
||||
private val networkInfo: NetworkInfo by inject()
|
||||
private val fs: Filesystem by inject()
|
||||
private val ackpineInstaller: PackageInstaller = get()
|
||||
|
||||
// TODO: save state to handle process death.
|
||||
var downloadedSize by mutableLongStateOf(0L)
|
||||
private set
|
||||
var totalSize by mutableLongStateOf(0L)
|
||||
@@ -63,17 +62,14 @@ class UpdateViewModel(
|
||||
private set
|
||||
|
||||
private val location = fs.tempDir.resolve("updater.apk")
|
||||
private val job = viewModelScope.launch {
|
||||
uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
|
||||
releaseInfo = reVancedAPI.getAppUpdate() ?: throw Exception("No update available")
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
|
||||
releaseInfo = reVancedAPI.getAppUpdate() ?: throw Exception("No update available")
|
||||
|
||||
if (downloadOnScreenEntry) {
|
||||
downloadUpdate()
|
||||
} else {
|
||||
state = State.CAN_DOWNLOAD
|
||||
}
|
||||
if (downloadOnScreenEntry) {
|
||||
downloadUpdate()
|
||||
} else {
|
||||
state = State.CAN_DOWNLOAD
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,36 +98,50 @@ class UpdateViewModel(
|
||||
|
||||
fun installUpdate() = viewModelScope.launch {
|
||||
state = State.INSTALLING
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
ackpineInstaller.createSession(Uri.fromFile(location)) {
|
||||
confirmation = Confirmation.IMMEDIATE
|
||||
}.await()
|
||||
}
|
||||
|
||||
when (result) {
|
||||
is Session.State.Failed<InstallFailure> -> when (val failure = result.failure) {
|
||||
is InstallFailure.Aborted -> state = State.CAN_INSTALL
|
||||
else -> {
|
||||
val msg = failure.message.orEmpty()
|
||||
app.toast(app.getString(R.string.install_app_fail, msg))
|
||||
installError = msg
|
||||
state = State.FAILED
|
||||
pm.installApp(listOf(location))
|
||||
}
|
||||
|
||||
private val installBroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
intent?.let {
|
||||
val pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999)
|
||||
val extra =
|
||||
intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!!
|
||||
|
||||
when(pmStatus) {
|
||||
PackageInstaller.STATUS_SUCCESS -> {
|
||||
app.toast(app.getString(R.string.install_app_success))
|
||||
state = State.SUCCESS
|
||||
}
|
||||
PackageInstaller.STATUS_FAILURE_ABORTED -> {
|
||||
state = State.CAN_INSTALL
|
||||
}
|
||||
else -> {
|
||||
app.toast(app.getString(R.string.install_app_fail, extra))
|
||||
installError = extra
|
||||
state = State.FAILED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Session.State.Succeeded -> {
|
||||
app.toast(app.getString(R.string.install_app_success))
|
||||
state = State.SUCCESS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply {
|
||||
addAction(InstallService.APP_INSTALL_ACTION)
|
||||
}, ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
app.unregisterReceiver(installBroadcastReceiver)
|
||||
|
||||
job.cancel()
|
||||
location.delete()
|
||||
}
|
||||
|
||||
enum class State(@param:StringRes val title: Int) {
|
||||
enum class State(@StringRes val title: Int) {
|
||||
CAN_DOWNLOAD(R.string.update_available),
|
||||
DOWNLOADING(R.string.downloading_manager_update),
|
||||
CAN_INSTALL(R.string.ready_to_install_update),
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
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.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
|
||||
|
||||
val downloadedVersions = downloadedAppsRepository.get(input.packageName)
|
||||
.map { apps ->
|
||||
apps.map { it.version }
|
||||
}
|
||||
|
||||
private val _localVersion = MutableStateFlow<String?>(null)
|
||||
val localVersion: StateFlow<String?> = _localVersion
|
||||
|
||||
val availableVersions = combine(
|
||||
patchBundleRepository.suggestedVersions(input.packageName, input.patchSelection),
|
||||
_localVersion,
|
||||
) { versions, local ->
|
||||
versions.orEmpty()
|
||||
.let { versions ->
|
||||
local?.let {
|
||||
versions.toMutableMap().also { it.putIfAbsent(local, 0) }
|
||||
} ?: versions
|
||||
}
|
||||
.map { (key, value) -> SelectedVersion.Specific(key) to patchCount - value }
|
||||
.sortedWith(
|
||||
compareBy<Pair<SelectedVersion.Specific, Int>>{ it.second }
|
||||
.thenByDescending { it.first.version }
|
||||
)
|
||||
}
|
||||
|
||||
var installedAppVersion by mutableStateOf<String?>(null)
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
val currentApp = pm.getPackageInfo(input.packageName)
|
||||
val patchedApp = installedAppRepository.get(input.packageName)
|
||||
|
||||
// Skip if installed app is patched
|
||||
if (patchedApp?.currentPackageName == input.packageName) return@launch
|
||||
|
||||
installedAppVersion = currentApp?.versionName
|
||||
}
|
||||
input.localPath?.let { local ->
|
||||
viewModelScope.launch {
|
||||
val packageInfo = pm.getPackageInfo(File(local))
|
||||
_localVersion.value = packageInfo?.versionName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var selectedVersion by mutableStateOf(input.selectedVersion)
|
||||
private set
|
||||
|
||||
fun selectVersion(version: SelectedVersion) {
|
||||
selectedVersion = version
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package app.revanced.manager.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageInstaller
|
||||
import ru.solrudev.ackpine.installer.InstallFailure
|
||||
import ru.solrudev.ackpine.uninstaller.UninstallFailure
|
||||
|
||||
/**
|
||||
* Converts an Ackpine installation failure into a PM status code
|
||||
*/
|
||||
fun InstallFailure.asCode() = when (this) {
|
||||
is InstallFailure.Aborted -> PackageInstaller.STATUS_FAILURE_ABORTED
|
||||
is InstallFailure.Blocked -> PackageInstaller.STATUS_FAILURE_BLOCKED
|
||||
is InstallFailure.Conflict -> PackageInstaller.STATUS_FAILURE_CONFLICT
|
||||
is InstallFailure.Incompatible -> PackageInstaller.STATUS_FAILURE_INCOMPATIBLE
|
||||
is InstallFailure.Invalid -> PackageInstaller.STATUS_FAILURE_INVALID
|
||||
is InstallFailure.Storage -> PackageInstaller.STATUS_FAILURE_STORAGE
|
||||
is InstallFailure.Timeout -> @SuppressLint("InlinedApi") PackageInstaller.STATUS_FAILURE_TIMEOUT
|
||||
else -> PackageInstaller.STATUS_FAILURE
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an Ackpine uninstallation failure into a PM status code
|
||||
*/
|
||||
fun UninstallFailure.asCode() = when (this) {
|
||||
is UninstallFailure.Aborted -> PackageInstaller.STATUS_FAILURE_ABORTED
|
||||
is UninstallFailure.Blocked -> PackageInstaller.STATUS_FAILURE_BLOCKED
|
||||
is UninstallFailure.Conflict -> PackageInstaller.STATUS_FAILURE_CONFLICT
|
||||
else -> PackageInstaller.STATUS_FAILURE
|
||||
}
|
||||
@@ -2,8 +2,11 @@ package app.revanced.manager.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.PackageInfoFlags
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
@@ -13,6 +16,8 @@ import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||
import app.revanced.manager.service.InstallService
|
||||
import app.revanced.manager.service.UninstallService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
@@ -20,13 +25,10 @@ import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import ru.solrudev.ackpine.session.await
|
||||
import ru.solrudev.ackpine.session.parameters.Confirmation
|
||||
import ru.solrudev.ackpine.uninstaller.PackageUninstaller
|
||||
import ru.solrudev.ackpine.uninstaller.createSession
|
||||
import ru.solrudev.ackpine.uninstaller.parameters.UninstallParametersDsl
|
||||
import java.io.File
|
||||
|
||||
private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable
|
||||
|
||||
@Immutable
|
||||
@Parcelize
|
||||
data class AppInfo(
|
||||
@@ -38,8 +40,7 @@ data class AppInfo(
|
||||
@SuppressLint("QueryPermissionsNeeded")
|
||||
class PM(
|
||||
private val app: Application,
|
||||
patchBundleRepository: PatchBundleRepository,
|
||||
private val uninstaller: PackageUninstaller
|
||||
patchBundleRepository: PatchBundleRepository
|
||||
) {
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
@@ -144,11 +145,17 @@ class PM(
|
||||
false
|
||||
)
|
||||
|
||||
suspend fun uninstallPackage(pkg: String, config: UninstallParametersDsl.() -> Unit = {}) = withContext(Dispatchers.IO) {
|
||||
uninstaller.createSession(pkg) {
|
||||
confirmation = Confirmation.IMMEDIATE
|
||||
config()
|
||||
}.await()
|
||||
suspend fun installApp(apks: List<File>) = withContext(Dispatchers.IO) {
|
||||
val packageInstaller = app.packageManager.packageInstaller
|
||||
packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
|
||||
apks.forEach { apk -> session.writeApk(apk) }
|
||||
session.commit(app.installIntentSender)
|
||||
}
|
||||
}
|
||||
|
||||
fun uninstallPackage(pkg: String) {
|
||||
val packageInstaller = app.packageManager.packageInstaller
|
||||
packageInstaller.uninstall(pkg, app.uninstallIntentSender)
|
||||
}
|
||||
|
||||
fun launch(pkg: String) = app.packageManager.getLaunchIntentForPackage(pkg)?.let {
|
||||
@@ -157,4 +164,44 @@ class PM(
|
||||
}
|
||||
|
||||
fun canInstallPackages() = app.packageManager.canRequestPackageInstalls()
|
||||
|
||||
private fun PackageInstaller.Session.writeApk(apk: File) {
|
||||
apk.inputStream().use { inputStream ->
|
||||
openWrite(apk.name, 0, apk.length()).use { outputStream ->
|
||||
inputStream.copyTo(outputStream, byteArraySize)
|
||||
fsync(outputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val intentFlags
|
||||
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
PendingIntent.FLAG_MUTABLE
|
||||
else
|
||||
0
|
||||
|
||||
private val sessionParams
|
||||
get() = PackageInstaller.SessionParams(
|
||||
PackageInstaller.SessionParams.MODE_FULL_INSTALL
|
||||
).apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
setRequestUpdateOwnership(true)
|
||||
setInstallReason(PackageManager.INSTALL_REASON_USER)
|
||||
}
|
||||
|
||||
private val Context.installIntentSender
|
||||
get() = PendingIntent.getService(
|
||||
this,
|
||||
0,
|
||||
Intent(this, InstallService::class.java),
|
||||
intentFlags
|
||||
).intentSender
|
||||
|
||||
private val Context.uninstallIntentSender
|
||||
get() = PendingIntent.getService(
|
||||
this,
|
||||
0,
|
||||
Intent(this, UninstallService::class.java),
|
||||
intentFlags
|
||||
).intentSender
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import app.revanced.manager.R
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -58,9 +57,6 @@ import kotlin.reflect.KProperty
|
||||
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) {
|
||||
@@ -86,8 +82,6 @@ fun Context.toast(string: String, duration: Int = Toast.LENGTH_SHORT) {
|
||||
inline fun uiSafe(context: Context, @StringRes toastMsg: Int, logMsg: String, block: () -> Unit) {
|
||||
try {
|
||||
block()
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (error: Exception) {
|
||||
// You can only toast on the main thread.
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
<item quantity="other">%d patches</item>
|
||||
</plurals>
|
||||
<plurals name="patches_executed">
|
||||
<item quantity="one">Execute %d patch</item>
|
||||
<item quantity="other">Execute %d patches</item>
|
||||
<item quantity="one">Executed %d patch</item>
|
||||
<item quantity="other">Executed %d patches</item>
|
||||
</plurals>
|
||||
<plurals name="selected_count">
|
||||
<item quantity="other">%d selected</item>
|
||||
|
||||
@@ -1,17 +1,3 @@
|
||||
<!--
|
||||
Strings with new lines must be raw strings, where the string is wrapped in double quotes and new lines are regular line breaks and not \n
|
||||
Raw strings still require escaping embedded double quotes, but single quote characters can be escaped or used as-is.
|
||||
|
||||
Raw strings are required because Crowdin AI translations regularly gets confused and
|
||||
replace \n with an encoded new line character.
|
||||
|
||||
Bad:
|
||||
<string name="summary_key">First \'item\' text\nSecond \"item\" text</string>
|
||||
Good:
|
||||
<string name="summary_key">"First 'item' text
|
||||
Second \"item\" text"</string>
|
||||
|
||||
-->
|
||||
<resources>
|
||||
<string name="app_name">ReVanced Manager</string>
|
||||
<string name="patcher">Patcher</string>
|
||||
@@ -61,16 +47,15 @@ Second \"item\" text"</string>
|
||||
<string name="app_source_dialog_option_installed_version_not_suggested">Version %s does not match the suggested version</string>
|
||||
|
||||
<string name="patch_item_description">Start patching the application</string>
|
||||
<string name="patch_selector_item">Patches</string>
|
||||
<string name="patch_selector_item_description">%d selected</string>
|
||||
<string name="patch_selector_item">Select patches</string>
|
||||
<string name="patch_selector_item_description">%d patches 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">APK source</string>
|
||||
<string name="apk_source_auto">Using all APK downloaders</string>
|
||||
<string name="apk_source_selector_item">Select APK source</string>
|
||||
<string name="apk_source_auto">Using all APK downloader</string>
|
||||
<string name="apk_source_downloader">Using %s</string>
|
||||
<string name="apk_source_installed">Using installed APK</string>
|
||||
<string name="apk_source_local">Using a local APK file</string>
|
||||
@@ -101,31 +86,21 @@ Second \"item\" text"</string>
|
||||
<string name="contributors_description">View the contributors of ReVanced</string>
|
||||
<string name="dynamic_color">Dynamic color</string>
|
||||
<string name="dynamic_color_description">Adapt colors to the wallpaper</string>
|
||||
<string name="pure_black_theme">Pure black theme</string>
|
||||
<string name="pure_black_theme_description">Use pure black backgrounds for dark theme</string>
|
||||
<string name="theme">Theme</string>
|
||||
<string name="theme_description">Choose between light or dark theme</string>
|
||||
<string name="safeguards">Safeguards</string>
|
||||
<string name="patch_compat_check">Disable version compatibility check</string>
|
||||
<string name="patch_compat_check_description">Do not restrict patches to compatible app versions</string>
|
||||
<string name="patch_compat_check_confirmation">"Selecting incompatible patches can result in a broken app.
|
||||
|
||||
Do you want to proceed anyways?"</string>
|
||||
<string name="patch_compat_check_description">The check restricts patches to compatible app versions</string>
|
||||
<string name="patch_compat_check_confirmation">Selecting incompatible patches can result in a broken app.\n\nDo you want to proceed anyways?</string>
|
||||
<string name="suggested_version_safeguard">Require suggested app version</string>
|
||||
<string name="suggested_version_safeguard_description">Enforce selection of the suggested app version</string>
|
||||
<string name="suggested_version_safeguard_confirmation">"Selecting an app that is not the suggested version may cause unexpected issues.
|
||||
|
||||
Do you want to proceed anyways?"</string>
|
||||
<string name="suggested_version_safeguard_confirmation">Selecting an app that is not the suggested version may cause unexpected issues.\n\nDo you want to proceed anyways?</string>
|
||||
<string name="patch_selection_safeguard">Allow changing patch selection and options</string>
|
||||
<string name="patch_selection_safeguard_description">Do not prevent selecting or deselecting patches and customization of options</string>
|
||||
<string name="patch_selection_safeguard_confirmation">"Changing the selection of patches may cause unexpected issues.
|
||||
|
||||
Enable anyways?"</string>
|
||||
<string name="patch_selection_safeguard_confirmation">Changing the selection of patches may cause unexpected issues.\n\nEnable anyways?</string>
|
||||
<string name="universal_patches_safeguard">Allow using universal patches</string>
|
||||
<string name="universal_patches_safeguard_description">Do not prevent using universal patches</string>
|
||||
<string name="universal_patches_safeguard_confirmation">"Universal patches are not as well tested as those that target specific apps.
|
||||
|
||||
Enable anyways?"</string>
|
||||
<string name="universal_patches_safeguard_confirmation">Universal patches are not as well tested as those that target specific apps.\n\nEnable anyways?</string>
|
||||
<string name="import_keystore">Import keystore</string>
|
||||
<string name="import_keystore_description">Import a custom keystore</string>
|
||||
<string name="import_keystore_dialog_title">Enter keystore credentials</string>
|
||||
@@ -141,9 +116,7 @@ Enable anyways?"</string>
|
||||
<string name="export_keystore_success">Exported keystore</string>
|
||||
<string name="regenerate_keystore">Regenerate keystore</string>
|
||||
<string name="regenerate_keystore_description">Generate a new keystore</string>
|
||||
<string name="regenerate_keystore_dialog_description">"You are about to regenerate your keystore the manager will use during the patching process.
|
||||
|
||||
You will not be able to update the previously installed apps from this source."</string>
|
||||
<string name="regenerate_keystore_dialog_description">You are about to regenerate your keystore the manager will use during the patching process.\n\nYou will not be able to update the previously installed apps from this source.</string>
|
||||
<string name="regenerate_keystore_success">The keystore has been successfully replaced</string>
|
||||
<string name="import_patch_selection">Import patch selection</string>
|
||||
<string name="import_patch_selection_description">Import patch selection from a JSON file</string>
|
||||
@@ -159,8 +132,8 @@ You will not be able to update the previously installed apps from this source."<
|
||||
<string name="reset_patch_options_description">Reset the stored patch options</string>
|
||||
<string name="reset_patch_selection_success">Patch selection has been reset</string>
|
||||
<string name="patch_selection_reset_all">Reset patch selection globally</string>
|
||||
<string name="patch_selection_reset_all_dialog_description">You are about to reset all patch selections. You will need to manually select each patch again.</string>
|
||||
<string name="patch_selection_reset_all_description">Resets all patch selections</string>
|
||||
<string name="patch_selection_reset_all_dialog_description">You are about to reset all the patch selections. You will need to manually select each patch again.</string>
|
||||
<string name="patch_selection_reset_all_description">Resets all the patch selections</string>
|
||||
<string name="patch_selection_reset_package">Reset patch selection for app</string>
|
||||
<string name="patch_selection_reset_package_dialog_description">You are about to reset the patch selection for the app \"%s\". You will have to manually select each patch again.</string>
|
||||
<string name="patch_selection_reset_package_description">Resets patch selection for a single app</string>
|
||||
@@ -174,7 +147,7 @@ You will not be able to update the previously installed apps from this source."<
|
||||
<string name="patch_options_reset_patches_dialog_description">You are about to reset the patch options for \"%s\". You will have to reapply each option again.</string>
|
||||
<string name="patch_options_reset_patches_description">Resets the patch options for a specific collection of patches</string>
|
||||
<string name="patch_options_reset_all">Reset patch options globally</string>
|
||||
<string name="patch_options_reset_all_dialog_description">You are about to reset all patch options. You will have to reapply each option again.</string>
|
||||
<string name="patch_options_reset_all_dialog_description">You are about to reset patch options. You will have to reapply each option again.</string>
|
||||
<string name="patch_options_reset_all_description">Resets all patch options</string>
|
||||
<string name="downloader_plugins">Plugins</string>
|
||||
<string name="downloader_plugin_state_trusted">Trusted</string>
|
||||
@@ -182,12 +155,10 @@ You will not be able to update the previously installed apps from this source."<
|
||||
<string name="downloader_plugin_state_untrusted">Untrusted</string>
|
||||
<string name="downloader_plugin_trust_dialog_title">Trust plugin?</string>
|
||||
<string name="downloader_plugin_revoke_trust_dialog_title">Revoke trust?</string>
|
||||
<string name="downloader_plugin_trust_dialog_body">Continuing will allow this plugin to run on your system.\n\nOnly enable this plugin if you trust it. Plugins can execute arbitrary code and may compromise your device.</string>
|
||||
<string name="downloader_plugin_trust_dialog_signature">Signature:\n\n%s</string>
|
||||
<string name="downloader_plugin_trust_dialog_plugin">Plugin:\n%s</string>
|
||||
<string name="downloader_plugin_trust_dialog_body">Package name: %1$s\nSignature (SHA-256): %2$s</string>
|
||||
<string name="downloader_plugin_delete_apps_title">Delete selected apps</string>
|
||||
<string name="downloader_plugin_delete_apps_description">Are you sure you want to delete the selected apps?</string>
|
||||
<string name="downloader_settings_no_apps">No downloaded apps found.</string>
|
||||
<string name="downloader_settings_no_apps">No downloaded apps found</string>
|
||||
|
||||
<string name="search_apps">Search apps…</string>
|
||||
<string name="loading_body">Loading…</string>
|
||||
@@ -203,7 +174,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</string>
|
||||
<string name="select_from_storage_description">Select an APK file from storage using file picker</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>
|
||||
@@ -220,7 +191,7 @@ You will not be able to update the previously installed apps from this source."<
|
||||
<string name="appearance">Appearance</string>
|
||||
<string name="downloaded_apps">Downloaded apps</string>
|
||||
<string name="process_runtime">Run Patcher in another process (experimental)</string>
|
||||
<string name="process_runtime_description">This is faster and allows Patcher to use more memory</string>
|
||||
<string name="process_runtime_description">This is faster and allows Patcher to use more memory.</string>
|
||||
<string name="process_runtime_memory_limit">Patcher process memory limit</string>
|
||||
<string name="process_runtime_memory_limit_description">The max amount of memory that the Patcher process can use (in megabytes)</string>
|
||||
<string name="debug_logs_export">Export debug logs</string>
|
||||
@@ -228,7 +199,7 @@ You will not be able to update the previously installed apps from this source."<
|
||||
<string name="debug_logs_export_failed">Failed to export logs</string>
|
||||
<string name="debug_logs_export_success">Exported logs</string>
|
||||
<string name="api_url">API URL</string>
|
||||
<string name="api_url_description">The API used to download necessary files</string>
|
||||
<string name="api_url_description">The API used to download necessary files.</string>
|
||||
<string name="api_url_dialog_title">Change API URL</string>
|
||||
<string name="api_url_dialog_description">Change the API URL of ReVanced Manager. ReVanced Manager uses the API to download patches and updates.</string>
|
||||
<string name="api_url_dialog_warning">ReVanced Manager connects to the API to download patches and updates. Make sure that you trust it.</string>
|
||||
@@ -264,23 +235,14 @@ You will not be able to update the previously installed apps from this source."<
|
||||
<string name="patch_selection_reset_toast">Patch selection and options has been reset to recommended defaults</string>
|
||||
<string name="patch_options_reset_toast">Patch options have been reset</string>
|
||||
<string name="non_suggested_version_warning_title">Non suggested version</string>
|
||||
<string name="non_suggested_version_warning_description">"The version of the app you have selected does not match the suggested version.
|
||||
Please use the suggested version: %s
|
||||
|
||||
To continue anyway, disable \"Require suggested app version\" in the advanced settings."</string>
|
||||
<string name="non_suggested_version_warning_description">The version of the app you have selected does not match the suggested version.\nPlease use the suggested version: %s\n\nTo continue anyway, disable \"Require suggested app version\" in the advanced settings.</string>
|
||||
<string name="selection_warning_title">Stop using defaults?</string>
|
||||
<string name="selection_warning_description">"It is recommended to use the default patch selection and options. Changing them may result in unexpected issues.
|
||||
|
||||
You need to turn on \"Allow changing patch selection and options\" in the advanced settings before toggling patches."</string>
|
||||
<string name="universal_patch_warning_description">"Universal patches have a more generalized use and do not work as reliably as patches that target specific apps. You may encounter issues while using them.
|
||||
|
||||
You need to turn on \"Allow using universal patches\" in the advanced settings before using universal patches."</string>
|
||||
<string name="selection_warning_description">It is recommended to use the default patch selection and options. Changing them may result in unexpected issues.\n\nYou need to turn on \"Allow changing patch selection\" in the advanced settings before toggling patches.</string>
|
||||
<string name="universal_patch_warning_description">Universal patches have a more generalized use and do not work as reliably as patches that target specific apps. You may encounter issues while using them.\n\nYou need to turn on \"Allow using universal patches\" in the advanced settings before using universal patches.</string>
|
||||
<string name="this_version">This version</string>
|
||||
<string name="universal">Any app</string>
|
||||
<string name="search_patches">Search patches</string>
|
||||
<string name="app_version_not_compatible">"This patch is not compatible with the selected app version (%1$s)
|
||||
|
||||
It is only compatible with the following version(s): %2$s"</string>
|
||||
<string name="app_version_not_compatible">This patch is not compatible with the selected app version (%1$s).\n\nIt is only compatible with the following version(s): %2$s.</string>
|
||||
<string name="continue_with_version">Continue with this version?</string>
|
||||
<string name="version_not_compatible">Not all patches are compatible with this version (%s). Do you want to continue anyway?</string>
|
||||
<string name="download_application">Download application?</string>
|
||||
@@ -314,7 +276,7 @@ It is only compatible with the following version(s): %2$s"</string>
|
||||
<string name="downloader_app_not_found">Downloader did not find the app</string>
|
||||
<string name="downloader_error">Downloader error: %s</string>
|
||||
<string name="downloader_no_plugins_installed">No downloader installed.</string>
|
||||
<string name="downloader_no_plugins_available">There are downloaders installed but none are trusted. Check your settings.</string>
|
||||
<string name="downloader_no_plugins_available">There are downloader installed but none is trusted. Check your settings.</string>
|
||||
<string name="already_patched">Already patched</string>
|
||||
|
||||
<string name="patch_selector_sheet_filter_title">Filter</string>
|
||||
@@ -356,7 +318,6 @@ It is only compatible with the following version(s): %2$s"</string>
|
||||
<string name="patcher_notification_text">Tap to return to the patcher</string>
|
||||
<string name="patcher_stop_confirm_title">Stop patcher</string>
|
||||
<string name="patcher_stop_confirm_description">Are you sure you want to stop the patching process?</string>
|
||||
<string name="patcher_install_in_progress">Installation is in progress. Please wait</string>
|
||||
<string name="execute_patches">Execute patches</string>
|
||||
<string name="executing_patch">Execute %s</string>
|
||||
<string name="failed_to_execute_patch">Failed to execute %s</string>
|
||||
@@ -422,8 +383,7 @@ It is only compatible with the following version(s): %2$s"</string>
|
||||
<string name="save_with_count">Save (%1$s)</string>
|
||||
<string name="update">Update</string>
|
||||
<string name="empty">Empty</string>
|
||||
<string name="installing_message">"Tap on <b>Update</b> when prompted.
|
||||
ReVanced Manager will close when updating."</string>
|
||||
<string name="installing_message">Tap on <b>Update</b> when prompted.\nReVanced Manager will close when updating.</string>
|
||||
<string name="no_changelogs_found">No changelogs found</string>
|
||||
<string name="just_now">Just now</string>
|
||||
<string name="minutes_ago">%sm ago</string>
|
||||
@@ -442,9 +402,7 @@ ReVanced Manager will close when updating."</string>
|
||||
<string name="update_available_dialog_description">A new version of ReVanced Manager (%s) is available.</string>
|
||||
<string name="failed_to_download_update">Failed to download update: %s</string>
|
||||
<string name="download">Download</string>
|
||||
<string name="download_confirmation_metered">"You are currently on a metered connection, and data charges from your service provider may apply.
|
||||
|
||||
Do you still want to continue?"</string>
|
||||
<string name="download_confirmation_metered">You are currently on a metered connection, and data charges from your service provider may apply.\n\nDo you still want to continue?</string>
|
||||
<string name="download_update_confirmation">Download update?</string>
|
||||
<string name="no_contributors_found">No contributors found</string>
|
||||
<string name="select">Select</string>
|
||||
@@ -479,17 +437,13 @@ Do you still want to continue?"</string>
|
||||
<string name="auto_update">Auto update</string>
|
||||
<string name="add_patches">Add patches</string>
|
||||
<string name="auto_update_description">Automatically update when a new version is available</string>
|
||||
<string name="patches_prereleases">Use pre-releases</string>
|
||||
<string name="patches_prereleases_description">Use pre-release versions of %s</string>
|
||||
<string name="patches_url">Patches URL</string>
|
||||
<string name="incompatible_patches_dialog">"These patches are not compatible with the selected app version (%1$s).
|
||||
|
||||
Click on the patches to see more details."</string>
|
||||
<string name="incompatible_patches_dialog">These patches are not compatible with the selected app version (%1$s).\n\nClick on the patches to see more details.</string>
|
||||
<string name="incompatible_patch">Incompatible patch</string>
|
||||
<string name="any_version">Any</string>
|
||||
<string name="never_show_again">Never show again</string>
|
||||
<string name="show_manager_update_dialog_on_launch">Show update message on launch</string>
|
||||
<string name="show_manager_update_dialog_on_launch_description">Show a popup notification whenever a new update is available on launch</string>
|
||||
<string name="show_manager_update_dialog_on_launch_description">Shows a popup notification whenever there is a new update available on launch.</string>
|
||||
<string name="failed_to_import_keystore">Failed to import keystore</string>
|
||||
<string name="export">Export</string>
|
||||
<string name="confirm">Confirm</string>
|
||||
|
||||
@@ -13,9 +13,9 @@ Learn how to use ReVanced Manager to patch apps.
|
||||
7. Tap on the `Install` button to install the patched app[^4]
|
||||
|
||||
[^1]: Here you can see all the apps that are supported by ReVanced.
|
||||
You can also add custom apps by tapping on the `Select from storage` button at the top.
|
||||
You can also add custom apps by tapping on the `+` button in the top right corner.
|
||||
[^2]: It is recommended to use the default set of patches by tapping on the `Reset` button in the bottom right corner.
|
||||
[^3]: By default, all available downloaders will be used to download the app.
|
||||
[^3]: By default, all available downloader will be used to download the app.
|
||||
If you want to use a specific downloader, you can change it here.
|
||||
[^4]: You can export the patched app or the patch logs in the bottom left corner.
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Learn how to manage downloaders.
|
||||
Refer to the [template](https://github.com/ReVanced/revanced-manager-downloader-template) if you are developer who wants to create a plugin.
|
||||
|
||||
Downloaders are APK files and are installed, updated and uninstalled just like regular Android apps.
|
||||
Downloaders are Apk files and are installed, updated and uninstalled just like regular Android apps.
|
||||
Downloaders can execute arbitrary code inside ReVanced Manager and must be marked as trusted before use. Manager will show a notification in the dashboard when a new downloader is discovered.
|
||||
Trust can also be granted and revoked under `Settings` > `Downloads`.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ Learn how to update ReVanced Manager.
|
||||
3. Configure the update settings accordingly[^1]
|
||||
|
||||
[^1]: By default, ReVanced Manager will check for updates automatically on launch
|
||||
and let you know when an update is available.
|
||||
and let you when an update is available.
|
||||
|
||||
## ⏭️ What's next
|
||||
|
||||
|
||||
@@ -4,29 +4,29 @@ Learn how to configure ReVanced Manager.
|
||||
|
||||
## 🔧 Settings
|
||||
|
||||
- **Downloads**: Enable or disable ReVanced Manager downloader and manage past downloaded apps here
|
||||
- **Import & export**: Import or export patch selections, patch options and the signing keystore
|
||||
- **Downloads**: Enable or disable ReVanced Manager downloader and manage past downloaded apps here
|
||||
- **Advanced**:
|
||||
- **API URL**: Set the URL of the ReVanced API, ReVanced Manager will use
|
||||
- **Disable version compatibility check**: Patching versions of apps the patches are explicitly compatible with is enforced.
|
||||
Disabling this will allow patching versions of apps the patches are not explicitly compatible with
|
||||
> ⚠️ Warning
|
||||
> Patches may fail on app versions they are not explicitly compatible with.
|
||||
> Patches may fail patching versions they are not explicitly compatible with.
|
||||
> Unless you know what you are doing, it is recommended to keep this enabled.
|
||||
- **Allow changing patch selection**: The default selection of patches is enforced.
|
||||
Enabling this will allow you to change the patch selection
|
||||
> ⚠️ Warning
|
||||
> Changing the selection may cause unexpected issues.
|
||||
> Unless you know what you are doing, it is recommended to keep this disabled.
|
||||
- **Require suggested app version**: Specific versions of apps is enforced based on the patch selection automatically.
|
||||
Disabling this will allow you to patch any version of apps
|
||||
> ⚠️ Warning
|
||||
> Patches not compatible with the selected version of the app will not be used.
|
||||
> Unless you know what you are doing, it is recommended to keep this enabled.
|
||||
- **Allow changing patch selection and options**: The default selection of patches is enforced.
|
||||
Enabling this will allow you to change the patch selection
|
||||
> ⚠️ Warning
|
||||
> Changing the selection may cause unexpected issues.
|
||||
> Unless you know what you are doing, it is recommended to keep this disabled.
|
||||
- **Allow using universal patches**: Patches that do not specify compatibility with an app are forcibly disabled.
|
||||
- **Allow universal patches**: Patches that do not specify compatibility with an app explicitly are forcibly disabled.
|
||||
Enabling this will allow selecting such patches
|
||||
> ⚠️ Warning
|
||||
> Universal patches do not specify compatibility with an app and may not work on all apps regardless.
|
||||
> Universal patches do not specify compatibility with an app explicitly may not work on all apps regardless.
|
||||
> Unless you know what you are doing, it is recommended to keep this disabled.
|
||||
- **About**: View more information and links about ReVanced and ReVanced Manager.
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ kotlin-process = "1.5.1"
|
||||
hidden-api-stub = "4.3.3"
|
||||
binary-compatibility-validator = "0.17.0"
|
||||
semver-parser = "3.0.0"
|
||||
ackpine = "0.18.5"
|
||||
|
||||
[libraries]
|
||||
# AndroidX Core
|
||||
@@ -134,10 +133,6 @@ compose-icons-fontawesome = { group = "com.github.BenjaminHalko.compose-icons",
|
||||
# Semantic versioning parser
|
||||
semver-parser = { module = "io.github.z4kn4fein:semver", version.ref = "semver-parser" }
|
||||
|
||||
# Ackpine
|
||||
ackpine-core = { module = "ru.solrudev.ackpine:ackpine-core", version.ref = "ackpine" }
|
||||
ackpine-ktx = { module = "ru.solrudev.ackpine:ackpine-ktx", version.ref = "ackpine" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
|
||||
android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" }
|
||||
|
||||
Reference in New Issue
Block a user