mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2026-01-20 01:33:58 +00:00
Compare commits
13 Commits
v1.26.0-de
...
feat/impro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b28e9a15be | ||
|
|
9fc2b4fdef | ||
|
|
08662b2132 | ||
|
|
0169fd2109 | ||
|
|
c53d0462d6 | ||
|
|
af8f2afa36 | ||
|
|
9cdb8eafb3 | ||
|
|
fda0e1697b | ||
|
|
2d98923f50 | ||
|
|
ffa42099e3 | ||
|
|
11dd6e4064 | ||
|
|
35fb59b31d | ||
|
|
18a4df9af9 |
@@ -1,3 +1,17 @@
|
|||||||
|
# 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)
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,10 @@ dependencies {
|
|||||||
|
|
||||||
// Compose Icons
|
// Compose Icons
|
||||||
implementation(libs.compose.icons.fontawesome)
|
implementation(libs.compose.icons.fontawesome)
|
||||||
|
|
||||||
|
// Ackpine
|
||||||
|
implementation(libs.ackpine.core)
|
||||||
|
implementation(libs.ackpine.ktx)
|
||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
version = 1.26.0-dev.14
|
version = 1.26.0-dev.16
|
||||||
|
|||||||
@@ -51,9 +51,6 @@
|
|||||||
|
|
||||||
<activity android:name=".plugin.downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" />
|
<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
|
<service
|
||||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
android:foregroundServiceType="specialUse"
|
android:foregroundServiceType="specialUse"
|
||||||
@@ -75,5 +72,15 @@
|
|||||||
android:value="androidx.startup"
|
android:value="androidx.startup"
|
||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
</provider>
|
</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>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// ProgressEventParcel.aidl
|
||||||
|
package app.revanced.manager.patcher;
|
||||||
|
|
||||||
|
parcelable ProgressEventParcel;
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
// IPatcherEvents.aidl
|
// IPatcherEvents.aidl
|
||||||
package app.revanced.manager.patcher.runtime.process;
|
package app.revanced.manager.patcher.runtime.process;
|
||||||
|
|
||||||
|
import app.revanced.manager.patcher.ProgressEventParcel;
|
||||||
|
|
||||||
// Interface for sending events back to the main app process.
|
// Interface for sending events back to the main app process.
|
||||||
oneway interface IPatcherEvents {
|
oneway interface IPatcherEvents {
|
||||||
void log(String level, String msg);
|
void log(String level, String msg);
|
||||||
void patchSucceeded();
|
void event(in ProgressEventParcel event);
|
||||||
void progress(String name, String state, String msg);
|
|
||||||
// The patching process has ended. The exceptionStackTrace is null if it finished successfully.
|
// The patching process has ended. The exceptionStackTrace is null if it finished successfully.
|
||||||
void finished(String exceptionStackTrace);
|
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.Dashboard
|
||||||
import app.revanced.manager.ui.model.navigation.InstalledApplicationInfo
|
import app.revanced.manager.ui.model.navigation.InstalledApplicationInfo
|
||||||
import app.revanced.manager.ui.model.navigation.Patcher
|
import app.revanced.manager.ui.model.navigation.Patcher
|
||||||
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
import app.revanced.manager.ui.model.navigation.SelectedAppInfo
|
||||||
import app.revanced.manager.ui.model.navigation.Settings
|
import app.revanced.manager.ui.model.navigation.Settings
|
||||||
import app.revanced.manager.ui.model.navigation.Update
|
import app.revanced.manager.ui.model.navigation.Update
|
||||||
import app.revanced.manager.ui.screen.AppSelectorScreen
|
import app.revanced.manager.ui.screen.AppSelectorScreen
|
||||||
@@ -41,7 +41,9 @@ import app.revanced.manager.ui.screen.PatchesSelectorScreen
|
|||||||
import app.revanced.manager.ui.screen.RequiredOptionsScreen
|
import app.revanced.manager.ui.screen.RequiredOptionsScreen
|
||||||
import app.revanced.manager.ui.screen.SelectedAppInfoScreen
|
import app.revanced.manager.ui.screen.SelectedAppInfoScreen
|
||||||
import app.revanced.manager.ui.screen.SettingsScreen
|
import app.revanced.manager.ui.screen.SettingsScreen
|
||||||
|
import app.revanced.manager.ui.screen.SourceSelectorScreen
|
||||||
import app.revanced.manager.ui.screen.UpdateScreen
|
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.AboutSettingsScreen
|
||||||
import app.revanced.manager.ui.screen.settings.AdvancedSettingsScreen
|
import app.revanced.manager.ui.screen.settings.AdvancedSettingsScreen
|
||||||
import app.revanced.manager.ui.screen.settings.ContributorSettingsScreen
|
import app.revanced.manager.ui.screen.settings.ContributorSettingsScreen
|
||||||
@@ -95,23 +97,16 @@ class MainActivity : ComponentActivity() {
|
|||||||
dynamicColor = dynamicColor,
|
dynamicColor = dynamicColor,
|
||||||
pureBlackTheme = pureBlackTheme
|
pureBlackTheme = pureBlackTheme
|
||||||
) {
|
) {
|
||||||
ReVancedManager(vm)
|
ReVancedManager()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ReVancedManager(vm: MainViewModel) {
|
private fun ReVancedManager() {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
EventEffect(vm.appSelectFlow) { app ->
|
|
||||||
navController.navigateComplex(
|
|
||||||
SelectedApplicationInfo,
|
|
||||||
SelectedApplicationInfo.ViewModelParams(app)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = Dashboard,
|
startDestination = Dashboard,
|
||||||
@@ -142,7 +137,12 @@ private fun ReVancedManager(vm: MainViewModel) {
|
|||||||
val data = it.toRoute<InstalledApplicationInfo>()
|
val data = it.toRoute<InstalledApplicationInfo>()
|
||||||
|
|
||||||
InstalledAppInfoScreen(
|
InstalledAppInfoScreen(
|
||||||
onPatchClick = vm::selectApp,
|
onPatchClick = { packageName ->
|
||||||
|
navController.navigateComplex(
|
||||||
|
SelectedAppInfo,
|
||||||
|
SelectedAppInfo.ViewModelParams(packageName)
|
||||||
|
)
|
||||||
|
},
|
||||||
onBackClick = navController::popBackStack,
|
onBackClick = navController::popBackStack,
|
||||||
viewModel = koinViewModel { parametersOf(data.packageName) }
|
viewModel = koinViewModel { parametersOf(data.packageName) }
|
||||||
)
|
)
|
||||||
@@ -150,8 +150,20 @@ private fun ReVancedManager(vm: MainViewModel) {
|
|||||||
|
|
||||||
composable<AppSelector> {
|
composable<AppSelector> {
|
||||||
AppSelectorScreen(
|
AppSelectorScreen(
|
||||||
onSelect = vm::selectApp,
|
onSelect = { packageName ->
|
||||||
onStorageSelect = vm::selectApp,
|
navController.navigateComplex(
|
||||||
|
SelectedAppInfo,
|
||||||
|
SelectedAppInfo.ViewModelParams(packageName)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onStorageSelect = { packageName, localPath ->
|
||||||
|
navController.navigateComplex(
|
||||||
|
SelectedAppInfo,
|
||||||
|
SelectedAppInfo.ViewModelParams(
|
||||||
|
packageName, localPath
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
onBackClick = navController::popBackStack
|
onBackClick = navController::popBackStack
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -179,11 +191,11 @@ private fun ReVancedManager(vm: MainViewModel) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
navigation<SelectedApplicationInfo>(startDestination = SelectedApplicationInfo.Main) {
|
navigation<SelectedAppInfo>(startDestination = SelectedAppInfo.Main) {
|
||||||
composable<SelectedApplicationInfo.Main> {
|
composable<SelectedAppInfo.Main> {
|
||||||
val parentBackStackEntry = navController.navGraphEntry(it)
|
val parentBackStackEntry = navController.navGraphEntry(it)
|
||||||
val data =
|
val data =
|
||||||
parentBackStackEntry.getComplexArg<SelectedApplicationInfo.ViewModelParams>()
|
parentBackStackEntry.getComplexArg<SelectedAppInfo.ViewModelParams>()
|
||||||
val viewModel =
|
val viewModel =
|
||||||
koinNavViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
|
koinNavViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
|
||||||
parametersOf(data)
|
parametersOf(data)
|
||||||
@@ -199,23 +211,47 @@ private fun ReVancedManager(vm: MainViewModel) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPatchSelectorClick = { app, patches, options ->
|
onPatchSelectorClick = { packageName, version, patchSelection, options ->
|
||||||
navController.navigateComplex(
|
navController.navigateComplex(
|
||||||
SelectedApplicationInfo.PatchesSelector,
|
SelectedAppInfo.PatchesSelector,
|
||||||
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
|
SelectedAppInfo.PatchesSelector.ViewModelParams(
|
||||||
app,
|
packageName,
|
||||||
patches,
|
version,
|
||||||
options
|
patchSelection,
|
||||||
|
options,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onRequiredOptions = { app, patches, options ->
|
onRequiredOptions = { packageName, version, patchSelection, options ->
|
||||||
navController.navigateComplex(
|
navController.navigateComplex(
|
||||||
SelectedApplicationInfo.RequiredOptions,
|
SelectedAppInfo.RequiredOptions,
|
||||||
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
|
SelectedAppInfo.PatchesSelector.ViewModelParams(
|
||||||
app,
|
packageName,
|
||||||
patches,
|
version,
|
||||||
options
|
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,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -223,9 +259,9 @@ private fun ReVancedManager(vm: MainViewModel) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable<SelectedApplicationInfo.PatchesSelector> {
|
composable<SelectedAppInfo.PatchesSelector> {
|
||||||
val data =
|
val data =
|
||||||
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
it.getComplexArg<SelectedAppInfo.PatchesSelector.ViewModelParams>()
|
||||||
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
||||||
viewModelStoreOwner = navController.navGraphEntry(it)
|
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||||
)
|
)
|
||||||
@@ -240,9 +276,43 @@ private fun ReVancedManager(vm: MainViewModel) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable<SelectedApplicationInfo.RequiredOptions> {
|
composable<SelectedAppInfo.VersionSelector> {
|
||||||
val data =
|
val data =
|
||||||
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
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>()
|
||||||
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
||||||
viewModelStoreOwner = navController.navGraphEntry(it)
|
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ class ManagerApplication : Application() {
|
|||||||
workerModule,
|
workerModule,
|
||||||
viewModelModule,
|
viewModelModule,
|
||||||
databaseModule,
|
databaseModule,
|
||||||
rootModule
|
rootModule,
|
||||||
|
ackpineModule
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package app.revanced.manager.data.room.apps.downloaded
|
|||||||
|
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Delete
|
import androidx.room.Delete
|
||||||
import androidx.room.Insert
|
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Upsert
|
import androidx.room.Upsert
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -12,6 +11,9 @@ interface DownloadedAppDao {
|
|||||||
@Query("SELECT * FROM downloaded_app")
|
@Query("SELECT * FROM downloaded_app")
|
||||||
fun getAllApps(): Flow<List<DownloadedApp>>
|
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")
|
@Query("SELECT * FROM downloaded_app WHERE package_name = :packageName AND version = :version")
|
||||||
suspend fun get(packageName: String, version: String): DownloadedApp?
|
suspend fun get(packageName: String, version: String): DownloadedApp?
|
||||||
|
|
||||||
|
|||||||
19
app/src/main/java/app/revanced/manager/di/AckpineModule.kt
Normal file
19
app/src/main/java/app/revanced/manager/di/AckpineModule.kt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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,4 +24,6 @@ val viewModelModule = module {
|
|||||||
viewModelOf(::InstalledAppInfoViewModel)
|
viewModelOf(::InstalledAppInfoViewModel)
|
||||||
viewModelOf(::UpdatesSettingsViewModel)
|
viewModelOf(::UpdatesSettingsViewModel)
|
||||||
viewModelOf(::BundleListViewModel)
|
viewModelOf(::BundleListViewModel)
|
||||||
|
viewModelOf(::VersionSelectorViewModel)
|
||||||
|
viewModelOf(::SourceSelectorViewModel)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ class DownloadedAppRepository(
|
|||||||
|
|
||||||
fun getAll() = dao.getAllApps().distinctUntilChanged()
|
fun getAll() = dao.getAllApps().distinctUntilChanged()
|
||||||
|
|
||||||
|
fun get(packageName: String) = dao.get(packageName)
|
||||||
|
|
||||||
fun getApkFileForApp(app: DownloadedApp): File =
|
fun getApkFileForApp(app: DownloadedApp): File =
|
||||||
getApkFileForDir(dir.resolve(app.directory))
|
getApkFileForDir(dir.resolve(app.directory))
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import app.revanced.manager.domain.manager.PreferencesManager
|
|||||||
import app.revanced.manager.patcher.patch.PatchInfo
|
import app.revanced.manager.patcher.patch.PatchInfo
|
||||||
import app.revanced.manager.patcher.patch.PatchBundle
|
import app.revanced.manager.patcher.patch.PatchBundle
|
||||||
import app.revanced.manager.patcher.patch.PatchBundleInfo
|
import app.revanced.manager.patcher.patch.PatchBundleInfo
|
||||||
|
import app.revanced.manager.util.PatchSelection
|
||||||
import app.revanced.manager.util.simpleMessage
|
import app.revanced.manager.util.simpleMessage
|
||||||
import app.revanced.manager.util.tag
|
import app.revanced.manager.util.tag
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
@@ -74,6 +75,17 @@ class PatchBundleRepository(
|
|||||||
|
|
||||||
val patchCountsFlow = bundleInfoFlow.map { it.mapValues { (_, info) -> info.patches.size } }
|
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 suggestedVersions = bundleInfoFlow.map {
|
||||||
val allPatches =
|
val allPatches =
|
||||||
it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet()
|
it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet()
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class PatchSelectionRepository(db: AppDatabase) {
|
|||||||
packageName = packageName
|
packageName = packageName
|
||||||
).also { dao.createSelection(it) }.uid
|
).also { dao.createSelection(it) }.uid
|
||||||
|
|
||||||
suspend fun getSelection(packageName: String): Map<Int, Set<String>> =
|
suspend fun getSelection(packageName: String): app.revanced.manager.util.PatchSelection =
|
||||||
dao.getSelectedPatches(packageName).mapValues { it.value.toSet() }
|
dao.getSelectedPatches(packageName).mapValues { it.value.toSet() }
|
||||||
|
|
||||||
suspend fun updateSelection(packageName: String, selection: Map<Int, Set<String>>) =
|
suspend fun updateSelection(packageName: String, selection: Map<Int, Set<String>>) =
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
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,10 +1,9 @@
|
|||||||
package app.revanced.manager.patcher
|
package app.revanced.manager.patcher
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import app.revanced.library.ApkUtils.applyTo
|
import app.revanced.library.ApkUtils.applyTo
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.patcher.Session.Companion.component1
|
||||||
|
import app.revanced.manager.patcher.Session.Companion.component2
|
||||||
import app.revanced.manager.patcher.logger.Logger
|
import app.revanced.manager.patcher.logger.Logger
|
||||||
import app.revanced.manager.ui.model.State
|
|
||||||
import app.revanced.patcher.Patcher
|
import app.revanced.patcher.Patcher
|
||||||
import app.revanced.patcher.PatcherConfig
|
import app.revanced.patcher.PatcherConfig
|
||||||
import app.revanced.patcher.patch.Patch
|
import app.revanced.patcher.patch.Patch
|
||||||
@@ -22,15 +21,10 @@ class Session(
|
|||||||
cacheDir: String,
|
cacheDir: String,
|
||||||
frameworkDir: String,
|
frameworkDir: String,
|
||||||
aaptPath: String,
|
aaptPath: String,
|
||||||
private val androidContext: Context,
|
|
||||||
private val logger: Logger,
|
private val logger: Logger,
|
||||||
private val input: File,
|
private val input: File,
|
||||||
private val onPatchCompleted: suspend () -> Unit,
|
private val onEvent: (ProgressEvent) -> Unit,
|
||||||
private val onProgress: (name: String?, state: State?, message: String?) -> Unit
|
|
||||||
) : Closeable {
|
) : 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 tempDir = File(cacheDir).resolve("patcher").also { it.mkdirs() }
|
||||||
private val patcher = Patcher(
|
private val patcher = Patcher(
|
||||||
PatcherConfig(
|
PatcherConfig(
|
||||||
@@ -42,86 +36,68 @@ class Session(
|
|||||||
)
|
)
|
||||||
|
|
||||||
private suspend fun Patcher.applyPatchesVerbose(selectedPatches: PatchList) {
|
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) ->
|
this().collect { (patch, exception) ->
|
||||||
if (patch !in selectedPatches) return@collect
|
val index = selectedPatches.indexOf(patch)
|
||||||
|
if (index == -1) return@collect
|
||||||
|
|
||||||
if (exception != null) {
|
if (exception != null) {
|
||||||
updateProgress(
|
onEvent(
|
||||||
name = androidContext.getString(R.string.failed_to_execute_patch, patch.name),
|
ProgressEvent.Failed(
|
||||||
state = State.FAILED,
|
StepId.ExecutePatch(index),
|
||||||
message = exception.stackTraceToString()
|
exception.toRemoteError(),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.error("${patch.name} failed:")
|
logger.error("${patch.name} failed:")
|
||||||
logger.error(exception.stackTraceToString())
|
logger.error(exception.stackTraceToString())
|
||||||
throw exception
|
throw exception
|
||||||
}
|
}
|
||||||
|
|
||||||
nextPatchIndex++
|
onEvent(
|
||||||
|
ProgressEvent.Completed(
|
||||||
onPatchCompleted()
|
StepId.ExecutePatch(index),
|
||||||
|
|
||||||
selectedPatches.getOrNull(nextPatchIndex)?.let { nextPatch ->
|
|
||||||
updateProgress(
|
|
||||||
name = androidContext.getString(R.string.executing_patch, nextPatch.name)
|
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
|
|
||||||
logger.info("${patch.name} succeeded")
|
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) {
|
suspend fun run(output: File, selectedPatches: PatchList) {
|
||||||
updateProgress(state = State.COMPLETED) // Unpacking
|
runStep(StepId.ExecutePatches, onEvent) {
|
||||||
|
java.util.logging.Logger.getLogger("").apply {
|
||||||
|
handlers.forEach {
|
||||||
|
it.close()
|
||||||
|
removeHandler(it)
|
||||||
|
}
|
||||||
|
|
||||||
java.util.logging.Logger.getLogger("").apply {
|
addHandler(logger.handler)
|
||||||
handlers.forEach {
|
|
||||||
it.close()
|
|
||||||
removeHandler(it)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addHandler(logger.handler)
|
with(patcher) {
|
||||||
|
logger.info("Merging integrations")
|
||||||
|
this += selectedPatches.toSet()
|
||||||
|
|
||||||
|
logger.info("Applying patches...")
|
||||||
|
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
with(patcher) {
|
runStep(StepId.WriteAPK, onEvent) {
|
||||||
logger.info("Merging integrations")
|
logger.info("Writing patched files...")
|
||||||
this += selectedPatches.toSet()
|
val result = patcher.get()
|
||||||
|
|
||||||
logger.info("Applying patches...")
|
val patched = tempDir.resolve("result.apk")
|
||||||
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
|
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("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() {
|
override fun close() {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ data class PatchInfo(
|
|||||||
if (pkg.packageName != packageName) return@any false
|
if (pkg.packageName != packageName) return@any false
|
||||||
if (pkg.versions == null) return@any true
|
if (pkg.versions == null) return@any true
|
||||||
|
|
||||||
versionName != null && versionName in pkg.versions
|
versionName == null || versionName in pkg.versions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package app.revanced.manager.patcher.runtime
|
package app.revanced.manager.patcher.runtime
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import app.revanced.manager.patcher.ProgressEvent
|
||||||
import app.revanced.manager.patcher.Session
|
import app.revanced.manager.patcher.Session
|
||||||
|
import app.revanced.manager.patcher.StepId
|
||||||
import app.revanced.manager.patcher.logger.Logger
|
import app.revanced.manager.patcher.logger.Logger
|
||||||
import app.revanced.manager.patcher.patch.PatchBundle
|
import app.revanced.manager.patcher.patch.PatchBundle
|
||||||
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
import app.revanced.manager.patcher.runStep
|
||||||
import app.revanced.manager.ui.model.State
|
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -13,7 +14,7 @@ import java.io.File
|
|||||||
/**
|
/**
|
||||||
* Simple [Runtime] implementation that runs the patcher using coroutines.
|
* Simple [Runtime] implementation that runs the patcher using coroutines.
|
||||||
*/
|
*/
|
||||||
class CoroutineRuntime(private val context: Context) : Runtime(context) {
|
class CoroutineRuntime(context: Context) : Runtime(context) {
|
||||||
override suspend fun execute(
|
override suspend fun execute(
|
||||||
inputFile: String,
|
inputFile: String,
|
||||||
outputFile: String,
|
outputFile: String,
|
||||||
@@ -21,47 +22,50 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) {
|
|||||||
selectedPatches: PatchSelection,
|
selectedPatches: PatchSelection,
|
||||||
options: Options,
|
options: Options,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
onPatchCompleted: suspend () -> Unit,
|
onEvent: (ProgressEvent) -> Unit,
|
||||||
onProgress: ProgressEventHandler,
|
|
||||||
) {
|
) {
|
||||||
val selectedBundles = selectedPatches.keys
|
val patchList = runStep(StepId.LoadPatches, onEvent) {
|
||||||
val bundles = bundles()
|
val selectedBundles = selectedPatches.keys
|
||||||
val uids = bundles.entries.associate { (key, value) -> value to key }
|
val bundles = bundles()
|
||||||
|
val uids = bundles.entries.associate { (key, value) -> value to key }
|
||||||
|
|
||||||
val allPatches =
|
val allPatches =
|
||||||
PatchBundle.Loader.patches(bundles.values, packageName)
|
PatchBundle.Loader.patches(bundles.values, packageName)
|
||||||
.mapKeys { (b, _) -> uids[b]!! }
|
.mapKeys { (b, _) -> uids[b]!! }
|
||||||
.filterKeys { it in selectedBundles }
|
.filterKeys { it in selectedBundles }
|
||||||
|
|
||||||
val patchList = selectedPatches.flatMap { (bundle, selected) ->
|
val patchList = selectedPatches.flatMap { (bundle, selected) ->
|
||||||
allPatches[bundle]?.filter { it.name in selected }
|
allPatches[bundle]?.filter { it.name in selected }
|
||||||
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set all patch options.
|
// Set all patch options.
|
||||||
options.forEach { (bundle, bundlePatchOptions) ->
|
options.forEach { (bundle, bundlePatchOptions) ->
|
||||||
val patches = allPatches[bundle] ?: return@forEach
|
val patches = allPatches[bundle] ?: return@forEach
|
||||||
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
|
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
|
||||||
val patchOptions = patches.single { it.name == patchName }.options
|
val patchOptions = patches.single { it.name == patchName }.options
|
||||||
configuredPatchOptions.forEach { (key, value) ->
|
configuredPatchOptions.forEach { (key, value) ->
|
||||||
patchOptions[key] = value
|
patchOptions[key] = value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
patchList
|
||||||
}
|
}
|
||||||
|
|
||||||
onProgress(null, State.COMPLETED, null) // Loading patches
|
val session = runStep(StepId.ReadAPK, onEvent) {
|
||||||
|
Session(
|
||||||
|
cacheDir,
|
||||||
|
frameworkPath,
|
||||||
|
aaptPath,
|
||||||
|
logger,
|
||||||
|
File(inputFile),
|
||||||
|
onEvent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Session(
|
session.use { s ->
|
||||||
cacheDir,
|
s.run(
|
||||||
frameworkPath,
|
|
||||||
aaptPath,
|
|
||||||
context,
|
|
||||||
logger,
|
|
||||||
File(inputFile),
|
|
||||||
onPatchCompleted = onPatchCompleted,
|
|
||||||
onProgress
|
|
||||||
).use { session ->
|
|
||||||
session.run(
|
|
||||||
File(outputFile),
|
File(outputFile),
|
||||||
patchList
|
patchList
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ import app.revanced.manager.BuildConfig
|
|||||||
import app.revanced.manager.patcher.runtime.process.IPatcherEvents
|
import app.revanced.manager.patcher.runtime.process.IPatcherEvents
|
||||||
import app.revanced.manager.patcher.runtime.process.IPatcherProcess
|
import app.revanced.manager.patcher.runtime.process.IPatcherProcess
|
||||||
import app.revanced.manager.patcher.LibraryResolver
|
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.logger.Logger
|
||||||
import app.revanced.manager.patcher.runtime.process.Parameters
|
import app.revanced.manager.patcher.runtime.process.Parameters
|
||||||
import app.revanced.manager.patcher.runtime.process.PatchConfiguration
|
import app.revanced.manager.patcher.runtime.process.PatchConfiguration
|
||||||
import app.revanced.manager.patcher.runtime.process.PatcherProcess
|
import app.revanced.manager.patcher.runtime.process.PatcherProcess
|
||||||
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
import app.revanced.manager.patcher.toEvent
|
||||||
import app.revanced.manager.ui.model.State
|
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
@@ -66,8 +67,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
|||||||
selectedPatches: PatchSelection,
|
selectedPatches: PatchSelection,
|
||||||
options: Options,
|
options: Options,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
onPatchCompleted: suspend () -> Unit,
|
onEvent: (ProgressEvent) -> Unit,
|
||||||
onProgress: ProgressEventHandler,
|
|
||||||
) = coroutineScope {
|
) = coroutineScope {
|
||||||
// Get the location of our own Apk.
|
// Get the location of our own Apk.
|
||||||
val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo!!.sourceDir
|
val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo!!.sourceDir
|
||||||
@@ -111,7 +111,6 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val patching = CompletableDeferred<Unit>()
|
val patching = CompletableDeferred<Unit>()
|
||||||
val scope = this
|
|
||||||
|
|
||||||
launch(Dispatchers.IO) {
|
launch(Dispatchers.IO) {
|
||||||
val binder = awaitBinderConnection()
|
val binder = awaitBinderConnection()
|
||||||
@@ -124,13 +123,10 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
|||||||
val eventHandler = object : IPatcherEvents.Stub() {
|
val eventHandler = object : IPatcherEvents.Stub() {
|
||||||
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg)
|
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg)
|
||||||
|
|
||||||
override fun patchSucceeded() {
|
override fun event(event: ProgressEventParcel?) {
|
||||||
scope.launch { onPatchCompleted() }
|
event?.let { onEvent(it.toEvent()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun progress(name: String?, state: String?, msg: String?) =
|
|
||||||
onProgress(name, state?.let { enumValueOf<State>(it) }, msg)
|
|
||||||
|
|
||||||
override fun finished(exceptionStackTrace: String?) {
|
override fun finished(exceptionStackTrace: String?) {
|
||||||
binder.exit()
|
binder.exit()
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import android.content.Context
|
|||||||
import app.revanced.manager.data.platform.Filesystem
|
import app.revanced.manager.data.platform.Filesystem
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
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.aapt.Aapt
|
||||||
import app.revanced.manager.patcher.logger.Logger
|
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.Options
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
@@ -34,7 +34,6 @@ sealed class Runtime(context: Context) : KoinComponent {
|
|||||||
selectedPatches: PatchSelection,
|
selectedPatches: PatchSelection,
|
||||||
options: Options,
|
options: Options,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
onPatchCompleted: suspend () -> Unit,
|
onEvent: (ProgressEvent) -> Unit,
|
||||||
onProgress: ProgressEventHandler,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -8,12 +8,15 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import app.revanced.manager.BuildConfig
|
import app.revanced.manager.BuildConfig
|
||||||
|
import app.revanced.manager.patcher.ProgressEvent
|
||||||
import app.revanced.manager.patcher.Session
|
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.LogLevel
|
||||||
import app.revanced.manager.patcher.logger.Logger
|
import app.revanced.manager.patcher.logger.Logger
|
||||||
import app.revanced.manager.patcher.patch.PatchBundle
|
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.runtime.ProcessRuntime
|
||||||
import app.revanced.manager.ui.model.State
|
import app.revanced.manager.patcher.toParcel
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -24,7 +27,7 @@ import kotlin.system.exitProcess
|
|||||||
/**
|
/**
|
||||||
* The main class that runs inside the runner process launched by [ProcessRuntime].
|
* The main class that runs inside the runner process launched by [ProcessRuntime].
|
||||||
*/
|
*/
|
||||||
class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
class PatcherProcess() : IPatcherProcess.Stub() {
|
||||||
private var eventBinder: IPatcherEvents? = null
|
private var eventBinder: IPatcherEvents? = null
|
||||||
|
|
||||||
private val scope =
|
private val scope =
|
||||||
@@ -46,6 +49,8 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
|||||||
override fun exit() = exitProcess(0)
|
override fun exit() = exitProcess(0)
|
||||||
|
|
||||||
override fun start(parameters: Parameters, events: IPatcherEvents) {
|
override fun start(parameters: Parameters, events: IPatcherEvents) {
|
||||||
|
fun onEvent(event: ProgressEvent) = events.event(event.toParcel())
|
||||||
|
|
||||||
eventBinder = events
|
eventBinder = events
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -56,38 +61,42 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
|||||||
|
|
||||||
logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
|
logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
|
||||||
|
|
||||||
val allPatches = PatchBundle.Loader.patches(parameters.configurations.map { it.bundle }, parameters.packageName)
|
val patchList = runStep(StepId.LoadPatches, ::onEvent) {
|
||||||
val patchList = parameters.configurations.flatMap { config ->
|
val allPatches = PatchBundle.Loader.patches(
|
||||||
val patches = (allPatches[config.bundle] ?: return@flatMap emptyList())
|
parameters.configurations.map { it.bundle },
|
||||||
|
parameters.packageName
|
||||||
|
)
|
||||||
|
|
||||||
|
parameters.configurations.flatMap { config ->
|
||||||
|
val patches = (allPatches[config.bundle] ?: return@flatMap emptyList())
|
||||||
.filter { it.name in config.patches }
|
.filter { it.name in config.patches }
|
||||||
.associateBy { it.name }
|
.associateBy { it.name }
|
||||||
|
|
||||||
config.options.forEach { (patchName, opts) ->
|
config.options.forEach { (patchName, opts) ->
|
||||||
val patchOptions = patches[patchName]?.options
|
val patchOptions = patches[patchName]?.options
|
||||||
?: throw Exception("Patch with name $patchName does not exist.")
|
?: throw Exception("Patch with name $patchName does not exist.")
|
||||||
|
|
||||||
opts.forEach { (key, value) ->
|
opts.forEach { (key, value) ->
|
||||||
patchOptions[key] = value
|
patchOptions[key] = value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
patches.values
|
patches.values
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
events.progress(null, State.COMPLETED.name, null) // Loading patches
|
val session = runStep(StepId.ReadAPK, ::onEvent) {
|
||||||
|
Session(
|
||||||
|
cacheDir = parameters.cacheDir,
|
||||||
|
aaptPath = parameters.aaptPath,
|
||||||
|
frameworkDir = parameters.frameworkDir,
|
||||||
|
logger = logger,
|
||||||
|
input = File(parameters.inputFile),
|
||||||
|
onEvent = ::onEvent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Session(
|
session.use {
|
||||||
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)
|
it.run(File(parameters.outputFile), patchList)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +128,7 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val ipcInterface = PatcherProcess(appContext)
|
val ipcInterface = PatcherProcess()
|
||||||
|
|
||||||
appContext.sendBroadcast(Intent().apply {
|
appContext.sendBroadcast(Intent().apply {
|
||||||
action = ProcessRuntime.CONNECT_TO_APP_ACTION
|
action = ProcessRuntime.CONNECT_TO_APP_ACTION
|
||||||
|
|||||||
@@ -29,14 +29,17 @@ import app.revanced.manager.domain.repository.InstalledAppRepository
|
|||||||
import app.revanced.manager.domain.worker.Worker
|
import app.revanced.manager.domain.worker.Worker
|
||||||
import app.revanced.manager.domain.worker.WorkerRepository
|
import app.revanced.manager.domain.worker.WorkerRepository
|
||||||
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
import app.revanced.manager.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.logger.Logger
|
||||||
|
import app.revanced.manager.patcher.runStep
|
||||||
import app.revanced.manager.patcher.runtime.CoroutineRuntime
|
import app.revanced.manager.patcher.runtime.CoroutineRuntime
|
||||||
import app.revanced.manager.patcher.runtime.ProcessRuntime
|
import app.revanced.manager.patcher.runtime.ProcessRuntime
|
||||||
|
import app.revanced.manager.patcher.toRemoteError
|
||||||
import app.revanced.manager.plugin.downloader.GetScope
|
import app.revanced.manager.plugin.downloader.GetScope
|
||||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||||
import app.revanced.manager.plugin.downloader.UserInteractionException
|
import app.revanced.manager.plugin.downloader.UserInteractionException
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedSource
|
||||||
import app.revanced.manager.ui.model.State
|
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
@@ -48,8 +51,6 @@ import org.koin.core.component.KoinComponent
|
|||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
typealias ProgressEventHandler = (name: String?, state: State?, message: String?) -> Unit
|
|
||||||
|
|
||||||
@OptIn(PluginHostApi::class)
|
@OptIn(PluginHostApi::class)
|
||||||
class PatcherWorker(
|
class PatcherWorker(
|
||||||
context: Context,
|
context: Context,
|
||||||
@@ -66,19 +67,17 @@ class PatcherWorker(
|
|||||||
private val rootInstaller: RootInstaller by inject()
|
private val rootInstaller: RootInstaller by inject()
|
||||||
|
|
||||||
class Args(
|
class Args(
|
||||||
val input: SelectedApp,
|
val packageName: String,
|
||||||
|
val version: String?,
|
||||||
|
val source: SelectedSource,
|
||||||
val output: String,
|
val output: String,
|
||||||
val selectedPatches: PatchSelection,
|
val selectedPatches: PatchSelection,
|
||||||
val options: Options,
|
val options: Options,
|
||||||
val logger: Logger,
|
val logger: Logger,
|
||||||
val onDownloadProgress: suspend (Pair<Long, Long?>?) -> Unit,
|
|
||||||
val onPatchCompleted: suspend () -> Unit,
|
|
||||||
val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult,
|
val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult,
|
||||||
val setInputFile: suspend (File) -> Unit,
|
val setInputFile: suspend (File) -> Unit,
|
||||||
val onProgress: ProgressEventHandler
|
val onEvent: (ProgressEvent) -> Unit,
|
||||||
) {
|
)
|
||||||
val packageName get() = input.packageName
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getForegroundInfo() =
|
override suspend fun getForegroundInfo() =
|
||||||
ForegroundInfo(
|
ForegroundInfo(
|
||||||
@@ -140,14 +139,10 @@ class PatcherWorker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun runPatcher(args: Args): Result {
|
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")
|
val patchedApk = fs.tempDir.resolve("patched.apk")
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
if (args.input is SelectedApp.Installed) {
|
if (args.source is SelectedSource.Installed) {
|
||||||
installedAppRepository.get(args.packageName)?.let {
|
installedAppRepository.get(args.packageName)?.let {
|
||||||
if (it.installType == InstallType.MOUNT) {
|
if (it.installType == InstallType.MOUNT) {
|
||||||
rootInstaller.unmount(args.packageName)
|
rootInstaller.unmount(args.packageName)
|
||||||
@@ -160,58 +155,65 @@ class PatcherWorker(
|
|||||||
plugin,
|
plugin,
|
||||||
data,
|
data,
|
||||||
args.packageName,
|
args.packageName,
|
||||||
args.input.version,
|
args.version,
|
||||||
prefs.suggestedVersionSafeguard.get(),
|
prefs.suggestedVersionSafeguard.get(),
|
||||||
!prefs.disablePatchVersionCompatCheck.get(),
|
!prefs.disablePatchVersionCompatCheck.get(),
|
||||||
onDownload = args.onDownloadProgress
|
) { progress ->
|
||||||
).also {
|
args.onEvent(
|
||||||
args.setInputFile(it)
|
ProgressEvent.Progress(
|
||||||
updateProgress(state = State.COMPLETED) // Download APK
|
stepId = StepId.DownloadAPK,
|
||||||
}
|
current = progress.first,
|
||||||
|
total = progress.second
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}.also { args.setInputFile(it) }
|
||||||
|
|
||||||
val inputFile = when (val selectedApp = args.input) {
|
val inputFile = when (val source = args.source) {
|
||||||
is SelectedApp.Download -> {
|
is SelectedSource.Auto -> throw Exception("Auto source is not supported in worker.")
|
||||||
val (plugin, data) = downloaderPluginRepository.unwrapParceledData(selectedApp.data)
|
|
||||||
|
|
||||||
download(plugin, data)
|
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
|
||||||
|
|
||||||
is SelectedApp.Search -> {
|
override suspend fun requestStartActivity(intent: Intent): Intent? {
|
||||||
downloaderPluginRepository.loadedPluginsFlow.first()
|
val result =
|
||||||
.firstNotNullOfOrNull { plugin ->
|
args.handleStartActivityRequest(plugin, intent)
|
||||||
try {
|
return when (result.resultCode) {
|
||||||
val getScope = object : GetScope {
|
Activity.RESULT_OK -> result.data
|
||||||
override val pluginPackageName = plugin.packageName
|
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
|
||||||
override val hostPackageName = applicationContext.packageName
|
else -> throw UserInteractionException.Activity.NotCompleted(
|
||||||
override suspend fun requestStartActivity(intent: Intent): Intent? {
|
result.resultCode,
|
||||||
val result = args.handleStartActivityRequest(plugin, intent)
|
result.data
|
||||||
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) {
|
||||||
withContext(Dispatchers.IO) {
|
plugin.get(
|
||||||
plugin.get(
|
getScope,
|
||||||
getScope,
|
args.packageName,
|
||||||
selectedApp.packageName,
|
args.version
|
||||||
selectedApp.version
|
)
|
||||||
)
|
}?.takeIf { (_, version) -> args.version == null || version == args.version }
|
||||||
}?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
|
} catch (e: UserInteractionException.Activity.NotCompleted) {
|
||||||
} catch (e: UserInteractionException.Activity.NotCompleted) {
|
throw e
|
||||||
throw e
|
} catch (_: UserInteractionException) {
|
||||||
} catch (_: UserInteractionException) {
|
null
|
||||||
null
|
}?.let { (data, _) -> download(plugin, data) }
|
||||||
}?.let { (data, _) -> download(plugin, data) }
|
} ?: throw Exception("App is not available.")
|
||||||
} ?: throw Exception("App is not available.")
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
|
is SelectedSource.Downloaded -> File(source.path)
|
||||||
is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo!!.sourceDir)
|
is SelectedSource.Local -> File(source.path)
|
||||||
|
|
||||||
|
is SelectedSource.Installed -> File(pm.getPackageInfo(args.packageName)!!.applicationInfo!!.sourceDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
val runtime = if (prefs.useProcessRuntime.get()) {
|
val runtime = if (prefs.useProcessRuntime.get()) {
|
||||||
@@ -227,12 +229,12 @@ class PatcherWorker(
|
|||||||
args.selectedPatches,
|
args.selectedPatches,
|
||||||
args.options,
|
args.options,
|
||||||
args.logger,
|
args.logger,
|
||||||
args.onPatchCompleted,
|
args.onEvent,
|
||||||
args.onProgress
|
|
||||||
)
|
)
|
||||||
|
|
||||||
keystoreManager.sign(patchedApk, File(args.output))
|
runStep(StepId.SignAPK, args.onEvent) {
|
||||||
updateProgress(state = State.COMPLETED) // Signing
|
keystoreManager.sign(patchedApk, File(args.output))
|
||||||
|
}
|
||||||
|
|
||||||
Log.i(tag, "Patching succeeded".logFmt())
|
Log.i(tag, "Patching succeeded".logFmt())
|
||||||
Result.success()
|
Result.success()
|
||||||
@@ -241,17 +243,15 @@ class PatcherWorker(
|
|||||||
tag,
|
tag,
|
||||||
"An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt()
|
"An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt()
|
||||||
)
|
)
|
||||||
updateProgress(state = State.FAILED, message = e.originalStackTrace)
|
args.onEvent(ProgressEvent.Failed(null, e.toRemoteError())) // Fallback if exception doesn't occur within step
|
||||||
Result.failure()
|
Result.failure()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(tag, "An exception occurred while patching".logFmt(), e)
|
Log.e(tag, "An exception occurred while patching".logFmt(), e)
|
||||||
updateProgress(state = State.FAILED, message = e.stackTraceToString())
|
args.onEvent(ProgressEvent.Failed(null, e.toRemoteError())) // Fallback if exception doesn't occur within step
|
||||||
Result.failure()
|
Result.failure()
|
||||||
} finally {
|
} finally {
|
||||||
patchedApk.delete()
|
patchedApk.delete()
|
||||||
if (args.input is SelectedApp.Local && args.input.temporary) {
|
if (args.source is SelectedSource.Local) File(args.source.path).delete()
|
||||||
args.input.file.delete()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
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,6 +15,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import io.github.fornewid.placeholder.material3.placeholder
|
import io.github.fornewid.placeholder.material3.placeholder
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -47,6 +48,8 @@ fun AppLabel(
|
|||||||
shape = RoundedCornerShape(100)
|
shape = RoundedCornerShape(100)
|
||||||
)
|
)
|
||||||
.then(modifier),
|
.then(modifier),
|
||||||
style = style
|
style = style,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.revanced.manager.ui.component
|
package app.revanced.manager.ui.component
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.pm.PackageInstaller
|
import android.content.pm.PackageInstaller
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
@@ -79,7 +80,7 @@ private fun installerStatusDialogButton(
|
|||||||
enum class DialogKind(
|
enum class DialogKind(
|
||||||
val flag: Int,
|
val flag: Int,
|
||||||
val title: Int,
|
val title: Int,
|
||||||
@StringRes val contentStringResId: Int,
|
@param:StringRes val contentStringResId: Int,
|
||||||
val icon: ImageVector = Icons.Outlined.ErrorOutline,
|
val icon: ImageVector = Icons.Outlined.ErrorOutline,
|
||||||
val confirmButton: InstallerStatusDialogButton = installerStatusDialogButton(R.string.ok),
|
val confirmButton: InstallerStatusDialogButton = installerStatusDialogButton(R.string.ok),
|
||||||
val dismissButton: InstallerStatusDialogButton? = null,
|
val dismissButton: InstallerStatusDialogButton? = null,
|
||||||
@@ -133,10 +134,8 @@ enum class DialogKind(
|
|||||||
title = R.string.installation_storage_issue_dialog_title,
|
title = R.string.installation_storage_issue_dialog_title,
|
||||||
contentStringResId = R.string.installation_storage_issue_description,
|
contentStringResId = R.string.installation_storage_issue_description,
|
||||||
),
|
),
|
||||||
|
|
||||||
@RequiresApi(34)
|
|
||||||
FAILURE_TIMEOUT(
|
FAILURE_TIMEOUT(
|
||||||
flag = PackageInstaller.STATUS_FAILURE_TIMEOUT,
|
flag = @SuppressLint("InlinedApi") PackageInstaller.STATUS_FAILURE_TIMEOUT,
|
||||||
title = R.string.installation_timeout_dialog_title,
|
title = R.string.installation_timeout_dialog_title,
|
||||||
contentStringResId = R.string.installation_timeout_description,
|
contentStringResId = R.string.installation_timeout_description,
|
||||||
confirmButton = installerStatusDialogButton(R.string.install_app) { model ->
|
confirmButton = installerStatusDialogButton(R.string.install_app) { model ->
|
||||||
|
|||||||
@@ -39,11 +39,9 @@ import androidx.compose.ui.unit.dp
|
|||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.ArrowButton
|
import app.revanced.manager.ui.component.ArrowButton
|
||||||
import app.revanced.manager.ui.component.LoadingIndicator
|
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.State
|
||||||
import app.revanced.manager.ui.model.Step
|
|
||||||
import app.revanced.manager.ui.model.StepCategory
|
import app.revanced.manager.ui.model.StepCategory
|
||||||
import app.revanced.manager.ui.model.StepProgressProvider
|
import app.revanced.manager.ui.model.Step
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
|
|
||||||
@@ -52,8 +50,6 @@ import kotlin.math.floor
|
|||||||
fun Steps(
|
fun Steps(
|
||||||
category: StepCategory,
|
category: StepCategory,
|
||||||
steps: List<Step>,
|
steps: List<Step>,
|
||||||
stepCount: Pair<Int, Int>? = null,
|
|
||||||
stepProgressProvider: StepProgressProvider,
|
|
||||||
isExpanded: Boolean = false,
|
isExpanded: Boolean = false,
|
||||||
onExpand: () -> Unit,
|
onExpand: () -> Unit,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
@@ -67,8 +63,17 @@ 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) {
|
LaunchedEffect(state) {
|
||||||
if (state == State.RUNNING)
|
if (state == State.RUNNING || state == State.FAILED)
|
||||||
onExpand()
|
onExpand()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,13 +97,8 @@ fun Steps(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
val stepProgress = remember(stepCount, steps) {
|
|
||||||
stepCount?.let { (current, total) -> "$current/$total" }
|
|
||||||
?: "${steps.count { it.state == State.COMPLETED }}/${steps.size}"
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stepProgress,
|
text = "${filteredSteps.count { it.state == State.COMPLETED }}/${filteredSteps.size}",
|
||||||
style = MaterialTheme.typography.labelSmall
|
style = MaterialTheme.typography.labelSmall
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -112,23 +112,20 @@ fun Steps(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = 10.dp)
|
.padding(top = 10.dp)
|
||||||
) {
|
) {
|
||||||
steps.forEachIndexed { index, step ->
|
filteredSteps.forEachIndexed { index, step ->
|
||||||
val (progress, progressText) = when (step.progressKey) {
|
val (progress, progressText) = step.progress?.let { (current, total) ->
|
||||||
null -> null
|
if (total != null) current.toFloat() / total.toFloat() to "${current.megaBytes}/${total.megaBytes} MB"
|
||||||
ProgressKey.DOWNLOAD -> stepProgressProvider.downloadProgress?.let { (downloaded, total) ->
|
else null to "${current.megaBytes} MB"
|
||||||
if (total != null) downloaded.toFloat() / total.toFloat() to "${downloaded.megaBytes}/${total.megaBytes} MB"
|
|
||||||
else null to "${downloaded.megaBytes} MB"
|
|
||||||
}
|
|
||||||
} ?: (null to null)
|
} ?: (null to null)
|
||||||
|
|
||||||
SubStep(
|
SubStep(
|
||||||
name = step.name,
|
name = step.title,
|
||||||
state = step.state,
|
state = step.state,
|
||||||
message = step.message,
|
message = step.message,
|
||||||
progress = progress,
|
progress = progress,
|
||||||
progressText = progressText,
|
progressText = progressText,
|
||||||
isFirst = index == 0,
|
isFirst = index == 0,
|
||||||
isLast = index == steps.lastIndex,
|
isLast = index == filteredSteps.lastIndex,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package app.revanced.manager.ui.model
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.patcher.StepId
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
enum class StepCategory(@StringRes val displayName: Int) {
|
enum class StepCategory(@StringRes val displayName: Int) {
|
||||||
@@ -15,19 +16,20 @@ enum class State {
|
|||||||
WAITING, RUNNING, FAILED, COMPLETED
|
WAITING, RUNNING, FAILED, COMPLETED
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ProgressKey {
|
|
||||||
DOWNLOAD
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StepProgressProvider {
|
|
||||||
val downloadProgress: Pair<Long, Long?>?
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Step(
|
data class Step(
|
||||||
val name: String,
|
val id: StepId,
|
||||||
|
val title: String,
|
||||||
val category: StepCategory,
|
val category: StepCategory,
|
||||||
val state: State = State.WAITING,
|
val state: State = State.WAITING,
|
||||||
val message: String? = null,
|
val message: String? = null,
|
||||||
val progressKey: ProgressKey? = null
|
val progress: Pair<Long, Long?>? = null,
|
||||||
) : Parcelable
|
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)
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package app.revanced.manager.ui.model
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import app.revanced.manager.network.downloader.ParceledDownloaderData
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
sealed interface SelectedApp : Parcelable {
|
|
||||||
val packageName: String
|
|
||||||
val version: String?
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Download(
|
|
||||||
override val packageName: String,
|
|
||||||
override val version: String?,
|
|
||||||
val data: ParceledDownloaderData
|
|
||||||
) : SelectedApp
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Search(override val packageName: String, override val version: String?) : SelectedApp
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Local(
|
|
||||||
override val packageName: String,
|
|
||||||
override val version: String,
|
|
||||||
val file: File,
|
|
||||||
val temporary: Boolean
|
|
||||||
) : SelectedApp
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Installed(
|
|
||||||
override val packageName: String,
|
|
||||||
override val version: String
|
|
||||||
) : SelectedApp
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package app.revanced.manager.ui.model
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
sealed class SelectedVersion : Parcelable {
|
||||||
|
data object Auto : SelectedVersion()
|
||||||
|
data object Any : SelectedVersion()
|
||||||
|
data class Specific(val version: String) : SelectedVersion()
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
package app.revanced.manager.ui.model.navigation
|
package app.revanced.manager.ui.model.navigation
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedSource
|
||||||
|
import app.revanced.manager.ui.model.SelectedVersion
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
@@ -23,10 +24,11 @@ data class InstalledApplicationInfo(val packageName: String)
|
|||||||
data class Update(val downloadOnScreenEntry: Boolean = false)
|
data class Update(val downloadOnScreenEntry: Boolean = false)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object SelectedApplicationInfo : ComplexParameter<SelectedApplicationInfo.ViewModelParams> {
|
data object SelectedAppInfo : ComplexParameter<SelectedAppInfo.ViewModelParams> {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class ViewModelParams(
|
data class ViewModelParams(
|
||||||
val app: SelectedApp,
|
val packageName: String,
|
||||||
|
val localPath: String? = null,
|
||||||
val patches: PatchSelection? = null
|
val patches: PatchSelection? = null
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
@@ -37,12 +39,35 @@ data object SelectedApplicationInfo : ComplexParameter<SelectedApplicationInfo.V
|
|||||||
data object PatchesSelector : ComplexParameter<PatchesSelector.ViewModelParams> {
|
data object PatchesSelector : ComplexParameter<PatchesSelector.ViewModelParams> {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class ViewModelParams(
|
data class ViewModelParams(
|
||||||
val app: SelectedApp,
|
val packageName: String,
|
||||||
val currentSelection: PatchSelection?,
|
val version: String?,
|
||||||
|
val patchSelection: PatchSelection?,
|
||||||
val options: @RawValue Options,
|
val options: @RawValue Options,
|
||||||
) : Parcelable
|
) : 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
|
@Serializable
|
||||||
data object RequiredOptions : ComplexParameter<PatchesSelector.ViewModelParams>
|
data object RequiredOptions : ComplexParameter<PatchesSelector.ViewModelParams>
|
||||||
}
|
}
|
||||||
@@ -51,7 +76,9 @@ data object SelectedApplicationInfo : ComplexParameter<SelectedApplicationInfo.V
|
|||||||
data object Patcher : ComplexParameter<Patcher.ViewModelParams> {
|
data object Patcher : ComplexParameter<Patcher.ViewModelParams> {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class ViewModelParams(
|
data class ViewModelParams(
|
||||||
val selectedApp: SelectedApp,
|
val packageName: String,
|
||||||
|
val version: String?,
|
||||||
|
val selectedSource: SelectedSource,
|
||||||
val selectedPatches: PatchSelection,
|
val selectedPatches: PatchSelection,
|
||||||
val options: @RawValue Options
|
val options: @RawValue Options
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ import app.revanced.manager.ui.component.LazyColumnWithScrollbar
|
|||||||
import app.revanced.manager.ui.component.LoadingIndicator
|
import app.revanced.manager.ui.component.LoadingIndicator
|
||||||
import app.revanced.manager.ui.component.NonSuggestedVersionDialog
|
import app.revanced.manager.ui.component.NonSuggestedVersionDialog
|
||||||
import app.revanced.manager.ui.component.SearchView
|
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.ui.viewmodel.AppSelectorViewModel
|
||||||
import app.revanced.manager.util.APK_MIMETYPE
|
import app.revanced.manager.util.APK_MIMETYPE
|
||||||
import app.revanced.manager.util.EventEffect
|
import app.revanced.manager.util.EventEffect
|
||||||
@@ -54,13 +53,13 @@ import org.koin.androidx.compose.koinViewModel
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AppSelectorScreen(
|
fun AppSelectorScreen(
|
||||||
onSelect: (String) -> Unit,
|
onSelect: (packageName: String) -> Unit,
|
||||||
onStorageSelect: (SelectedApp.Local) -> Unit,
|
onStorageSelect: (packageName: String, path: String) -> Unit,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
vm: AppSelectorViewModel = koinViewModel()
|
vm: AppSelectorViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
EventEffect(flow = vm.storageSelectionFlow) {
|
EventEffect(flow = vm.storageSelectionFlow) {
|
||||||
onStorageSelect(it)
|
onStorageSelect(it.first, it.second)
|
||||||
}
|
}
|
||||||
|
|
||||||
val pickApkLauncher =
|
val pickApkLauncher =
|
||||||
@@ -83,12 +82,12 @@ fun AppSelectorScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
vm.nonSuggestedVersionDialogSubject?.let {
|
// vm.nonSuggestedVersionDialogSubject?.let {
|
||||||
NonSuggestedVersionDialog(
|
// NonSuggestedVersionDialog(
|
||||||
suggestedVersion = suggestedVersions[it.packageName].orEmpty(),
|
// suggestedVersion = suggestedVersions[it.packageName].orEmpty(),
|
||||||
onDismiss = vm::dismissNonSuggestedVersionDialog
|
// onDismiss = vm::dismissNonSuggestedVersionDialog
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (search)
|
if (search)
|
||||||
SearchView(
|
SearchView(
|
||||||
@@ -115,8 +114,7 @@ fun AppSelectorScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
headlineContent = { AppLabel(app.packageInfo) },
|
headlineContent = { AppLabel(app.packageInfo) },
|
||||||
supportingContent = { Text(app.packageName) },
|
supportingContent = app.patches?.let {
|
||||||
trailingContent = app.patches?.let {
|
|
||||||
{
|
{
|
||||||
Text(
|
Text(
|
||||||
pluralStringResource(
|
pluralStringResource(
|
||||||
@@ -214,12 +212,7 @@ fun AppSelectorScreen(
|
|||||||
defaultText = app.packageName
|
defaultText = app.packageName
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
supportingContent = {
|
supportingContent = app.patches?.let {
|
||||||
suggestedVersions[app.packageName]?.let {
|
|
||||||
Text(stringResource(R.string.suggested_version_info, it))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
trailingContent = app.patches?.let {
|
|
||||||
{
|
{
|
||||||
Text(
|
Text(
|
||||||
pluralStringResource(
|
pluralStringResource(
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ fun PatcherScreen(
|
|||||||
|
|
||||||
val steps by remember {
|
val steps by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
viewModel.steps.groupBy { it.category }
|
viewModel.steps.groupBy { it.category }.toList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,14 +230,12 @@ fun PatcherScreen(
|
|||||||
contentPadding = PaddingValues(16.dp)
|
contentPadding = PaddingValues(16.dp)
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
items = steps.toList(),
|
items = steps,
|
||||||
key = { it.first }
|
key = { it.first }
|
||||||
) { (category, steps) ->
|
) { (category, steps) ->
|
||||||
Steps(
|
Steps(
|
||||||
category = category,
|
category = category,
|
||||||
steps = steps,
|
steps = steps,
|
||||||
stepCount = if (category == StepCategory.PATCHING) viewModel.patchesProgress else null,
|
|
||||||
stepProgressProvider = viewModel,
|
|
||||||
isExpanded = expandedCategory == category,
|
isExpanded = expandedCategory == category,
|
||||||
onExpand = { expandCategory(category) },
|
onExpand = { expandCategory(category) },
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|||||||
@@ -498,7 +498,7 @@ private fun PatchItem(
|
|||||||
leadingContent = {
|
leadingContent = {
|
||||||
HapticCheckbox(
|
HapticCheckbox(
|
||||||
checked = selected,
|
checked = selected,
|
||||||
onCheckedChange = { onToggle() },
|
onCheckedChange = null,
|
||||||
enabled = compatible
|
enabled = compatible
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
package app.revanced.manager.ui.screen
|
package app.revanced.manager.ui.screen
|
||||||
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
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.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
import androidx.compose.material.icons.automirrored.outlined.ArrowRight
|
||||||
import androidx.compose.material.icons.filled.AutoFixHigh
|
import androidx.compose.material.icons.filled.AutoFixHigh
|
||||||
@@ -21,11 +17,9 @@ import androidx.compose.material3.ListItem
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
@@ -37,33 +31,30 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.data.platform.NetworkInfo
|
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.AppInfo
|
||||||
import app.revanced.manager.ui.component.AppTopBar
|
import app.revanced.manager.ui.component.AppTopBar
|
||||||
import app.revanced.manager.ui.component.ColumnWithScrollbar
|
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.NotificationCard
|
||||||
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
import app.revanced.manager.ui.component.haptics.HapticExtendedFloatingActionButton
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedSource
|
||||||
|
import app.revanced.manager.ui.model.SelectedVersion
|
||||||
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
|
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
|
||||||
import app.revanced.manager.util.EventEffect
|
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
import app.revanced.manager.util.enabled
|
import app.revanced.manager.util.enabled
|
||||||
|
import app.revanced.manager.util.patchCount
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
import app.revanced.manager.util.transparentListItemColors
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SelectedAppInfoScreen(
|
fun SelectedAppInfoScreen(
|
||||||
onPatchSelectorClick: (SelectedApp, PatchSelection?, Options) -> Unit,
|
onPatchSelectorClick: (packageName: String, version: String?, PatchSelection?, Options) -> Unit,
|
||||||
onRequiredOptions: (SelectedApp, PatchSelection?, Options) -> Unit,
|
onRequiredOptions: (packageName: String, version: String?, PatchSelection?, Options) -> Unit,
|
||||||
onPatchClick: () -> Unit,
|
onPatchClick: () -> Unit,
|
||||||
|
onVersionClick: (packageName: String, patchSelection: PatchSelection, selectedVersion: SelectedVersion, localPath: String?) -> Unit,
|
||||||
|
onSourceClick: (packageName: String, version: String?, SelectedSource, localPath: String?) -> Unit,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
vm: SelectedAppInfoViewModel
|
vm: SelectedAppInfoViewModel
|
||||||
) {
|
) {
|
||||||
@@ -72,29 +63,23 @@ fun SelectedAppInfoScreen(
|
|||||||
val networkConnected = remember { networkInfo.isConnected() }
|
val networkConnected = remember { networkInfo.isConnected() }
|
||||||
val networkMetered = remember { !networkInfo.isUnmetered() }
|
val networkMetered = remember { !networkInfo.isUnmetered() }
|
||||||
|
|
||||||
val packageName = vm.selectedApp.packageName
|
val packageName = vm.packageName
|
||||||
val version = vm.selectedApp.version
|
|
||||||
val bundles by vm.bundleInfoFlow.collectAsStateWithLifecycle(emptyList())
|
|
||||||
|
|
||||||
val allowIncompatiblePatches by vm.prefs.disablePatchVersionCompatCheck.getAsState()
|
|
||||||
val patches by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
vm.getPatches(bundles, allowIncompatiblePatches)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val selectedPatchCount = patches.values.sumOf { it.size }
|
|
||||||
|
|
||||||
val launcher = rememberLauncherForActivityResult(
|
|
||||||
contract = ActivityResultContracts.StartActivityForResult(),
|
|
||||||
onResult = vm::handlePluginActivityResult
|
|
||||||
)
|
|
||||||
EventEffect(flow = vm.launchActivityFlow) { intent ->
|
|
||||||
launcher.launch(intent)
|
|
||||||
}
|
|
||||||
val composableScope = rememberCoroutineScope()
|
val composableScope = rememberCoroutineScope()
|
||||||
|
|
||||||
val error by vm.errorFlow.collectAsStateWithLifecycle(null)
|
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())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -117,18 +102,18 @@ fun SelectedAppInfoScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = patchClick@{
|
onClick = patchClick@{
|
||||||
if (selectedPatchCount == 0) {
|
if (patchCount == 0) {
|
||||||
context.toast(context.getString(R.string.no_patches_selected))
|
context.toast(context.getString(R.string.no_patches_selected))
|
||||||
|
|
||||||
return@patchClick
|
return@patchClick
|
||||||
}
|
}
|
||||||
|
|
||||||
composableScope.launch {
|
composableScope.launch {
|
||||||
if (!vm.hasSetRequiredOptions(patches)) {
|
if (!vm.hasSetRequiredOptions(fullPatchSelection)) {
|
||||||
onRequiredOptions(
|
onRequiredOptions(
|
||||||
vm.selectedApp,
|
vm.packageName,
|
||||||
vm.getCustomPatches(bundles, allowIncompatiblePatches),
|
resolvedVersion,
|
||||||
vm.options
|
customSelection,
|
||||||
|
vm.options,
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
@@ -140,94 +125,96 @@ fun SelectedAppInfoScreen(
|
|||||||
},
|
},
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
) { paddingValues ->
|
) { 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(
|
ColumnWithScrollbar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
) {
|
) {
|
||||||
AppInfo(vm.selectedAppInfo, placeholderLabel = packageName) {
|
AppInfo(vm.selectedAppInfo, placeholderLabel = packageName) {
|
||||||
Text(
|
vm.selectedAppInfo?.let {
|
||||||
version ?: stringResource(R.string.selected_app_meta_any_version),
|
Text(
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
it.packageName,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PageItem(
|
PageItem(
|
||||||
R.string.patch_selector_item,
|
R.string.patch_selector_item,
|
||||||
stringResource(
|
stringResource(R.string.patch_selector_item_description, patchCount),
|
||||||
R.string.patch_selector_item_description,
|
|
||||||
selectedPatchCount
|
|
||||||
),
|
|
||||||
onClick = {
|
onClick = {
|
||||||
onPatchSelectorClick(
|
onPatchSelectorClick(
|
||||||
vm.selectedApp,
|
vm.packageName,
|
||||||
vm.getCustomPatches(
|
resolvedVersion,
|
||||||
bundles,
|
customSelection,
|
||||||
allowIncompatiblePatches
|
|
||||||
),
|
|
||||||
vm.options
|
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(
|
PageItem(
|
||||||
R.string.apk_source_selector_item,
|
R.string.apk_source_selector_item,
|
||||||
when (val app = vm.selectedApp) {
|
sourceDescription,
|
||||||
is SelectedApp.Search -> stringResource(R.string.apk_source_auto)
|
onClick = { onSourceClick(
|
||||||
is SelectedApp.Installed -> stringResource(R.string.apk_source_installed)
|
packageName,
|
||||||
is SelectedApp.Download -> stringResource(
|
resolvedVersion,
|
||||||
R.string.apk_source_downloader,
|
selectedSource,
|
||||||
plugins.find { it.packageName == app.data.pluginPackageName }?.name
|
vm.localPath,
|
||||||
?: app.data.pluginPackageName
|
) },
|
||||||
)
|
|
||||||
|
|
||||||
is SelectedApp.Local -> stringResource(R.string.apk_source_local)
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
vm.showSourceSelector()
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
error?.let {
|
error?.let {
|
||||||
Text(
|
Text(
|
||||||
stringResource(it.resourceId),
|
stringResource(it.resourceId),
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
modifier = Modifier.padding(horizontal = 24.dp)
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
if (resolvedSource is SelectedSource.Plugin) Column(
|
||||||
modifier = Modifier.padding(horizontal = 24.dp),
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
val needsInternet =
|
|
||||||
vm.selectedApp.let { it is SelectedApp.Search || it is SelectedApp.Download }
|
|
||||||
|
|
||||||
when {
|
when {
|
||||||
!needsInternet -> {}
|
|
||||||
!networkConnected -> {
|
!networkConnected -> {
|
||||||
NotificationCard(
|
NotificationCard(
|
||||||
isWarning = true,
|
isWarning = true,
|
||||||
@@ -236,7 +223,6 @@ fun SelectedAppInfoScreen(
|
|||||||
onDismiss = null
|
onDismiss = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
networkMetered -> {
|
networkMetered -> {
|
||||||
NotificationCard(
|
NotificationCard(
|
||||||
isWarning = true,
|
isWarning = true,
|
||||||
@@ -252,11 +238,17 @@ fun SelectedAppInfoScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PageItem(@StringRes title: Int, description: String, onClick: () -> Unit) {
|
private fun PageItem(
|
||||||
|
@StringRes title: Int,
|
||||||
|
description: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
extraDescription: @Composable (ColumnScope.() -> Unit)? = null,
|
||||||
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(onClick = onClick)
|
.clickable(enabled, onClick = onClick)
|
||||||
.padding(start = 8.dp),
|
.enabled(enabled),
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(
|
Text(
|
||||||
stringResource(title),
|
stringResource(title),
|
||||||
@@ -265,99 +257,17 @@ private fun PageItem(@StringRes title: Int, description: String, onClick: () ->
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
Text(
|
Column {
|
||||||
description,
|
Text(
|
||||||
color = MaterialTheme.colorScheme.outline,
|
description,
|
||||||
style = MaterialTheme.typography.bodyMedium
|
color = MaterialTheme.colorScheme.outline,
|
||||||
)
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
extraDescription?.invoke(this)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
Icon(Icons.AutoMirrored.Outlined.ArrowRight, null)
|
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,9 +3,6 @@ package app.revanced.manager.ui.viewmodel
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.net.Uri
|
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.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -14,7 +11,6 @@ import androidx.lifecycle.viewmodel.compose.saveable
|
|||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.data.platform.Filesystem
|
import app.revanced.manager.data.platform.Filesystem
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
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.PM
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -31,7 +27,7 @@ class AppSelectorViewModel(
|
|||||||
private val app: Application,
|
private val app: Application,
|
||||||
private val pm: PM,
|
private val pm: PM,
|
||||||
fs: Filesystem,
|
fs: Filesystem,
|
||||||
private val patchBundleRepository: PatchBundleRepository,
|
patchBundleRepository: PatchBundleRepository,
|
||||||
savedStateHandle: SavedStateHandle,
|
savedStateHandle: SavedStateHandle,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val inputFile = savedStateHandle.saveable(key = "inputFile") {
|
private val inputFile = savedStateHandle.saveable(key = "inputFile") {
|
||||||
@@ -42,19 +38,19 @@ class AppSelectorViewModel(
|
|||||||
}
|
}
|
||||||
val appList = pm.appList
|
val appList = pm.appList
|
||||||
|
|
||||||
private val storageSelectionChannel = Channel<SelectedApp.Local>()
|
private val storageSelectionChannel = Channel<Pair<String, String>>()
|
||||||
val storageSelectionFlow = storageSelectionChannel.receiveAsFlow()
|
val storageSelectionFlow = storageSelectionChannel.receiveAsFlow()
|
||||||
|
|
||||||
val suggestedAppVersions = patchBundleRepository.suggestedVersions.flowOn(Dispatchers.Default)
|
val suggestedAppVersions = patchBundleRepository.suggestedVersions.flowOn(Dispatchers.Default)
|
||||||
|
|
||||||
var nonSuggestedVersionDialogSubject by mutableStateOf<SelectedApp.Local?>(null)
|
// var nonSuggestedVersionDialogSubject by mutableStateOf<SelectedApp.Local?>(null)
|
||||||
private set
|
// private set
|
||||||
|
|
||||||
fun loadLabel(app: PackageInfo?) = with(pm) { app?.label() ?: "Not installed" }
|
fun loadLabel(app: PackageInfo?) = with(pm) { app?.label() ?: "Not installed" }
|
||||||
|
|
||||||
fun dismissNonSuggestedVersionDialog() {
|
// fun dismissNonSuggestedVersionDialog() {
|
||||||
nonSuggestedVersionDialogSubject = null
|
// nonSuggestedVersionDialogSubject = null
|
||||||
}
|
// }
|
||||||
|
|
||||||
fun handleStorageResult(uri: Uri) = viewModelScope.launch {
|
fun handleStorageResult(uri: Uri) = viewModelScope.launch {
|
||||||
val selectedApp = withContext(Dispatchers.IO) {
|
val selectedApp = withContext(Dispatchers.IO) {
|
||||||
@@ -66,11 +62,8 @@ class AppSelectorViewModel(
|
|||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
if (patchBundleRepository.isVersionAllowed(selectedApp.packageName, selectedApp.version)) {
|
// TODO: Disallow if 0 patches are compatible
|
||||||
storageSelectionChannel.send(selectedApp)
|
storageSelectionChannel.send(selectedApp)
|
||||||
} else {
|
|
||||||
nonSuggestedVersionDialogSubject = selectedApp
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadSelectedFile(uri: Uri) =
|
private fun loadSelectedFile(uri: Uri) =
|
||||||
@@ -80,12 +73,7 @@ class AppSelectorViewModel(
|
|||||||
Files.copy(stream, toPath())
|
Files.copy(stream, toPath())
|
||||||
|
|
||||||
pm.getPackageInfo(this)?.let { packageInfo ->
|
pm.getPackageInfo(this)?.let { packageInfo ->
|
||||||
SelectedApp.Local(
|
Pair(packageInfo.packageName, path)
|
||||||
packageName = packageInfo.packageName,
|
|
||||||
version = packageInfo.versionName!!,
|
|
||||||
file = this,
|
|
||||||
temporary = true
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
import android.app.Application
|
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.PackageInfo
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
@@ -19,7 +13,6 @@ import app.revanced.manager.data.room.apps.installed.InstallType
|
|||||||
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
||||||
import app.revanced.manager.domain.installer.RootInstaller
|
import app.revanced.manager.domain.installer.RootInstaller
|
||||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||||
import app.revanced.manager.service.UninstallService
|
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
import app.revanced.manager.util.simpleMessage
|
import app.revanced.manager.util.simpleMessage
|
||||||
@@ -30,6 +23,8 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
import ru.solrudev.ackpine.session.Session
|
||||||
|
import ru.solrudev.ackpine.uninstaller.UninstallFailure
|
||||||
|
|
||||||
class InstalledAppInfoViewModel(
|
class InstalledAppInfoViewModel(
|
||||||
packageName: String
|
packageName: String
|
||||||
@@ -87,51 +82,28 @@ class InstalledAppInfoViewModel(
|
|||||||
|
|
||||||
fun uninstall() {
|
fun uninstall() {
|
||||||
val app = installedApp ?: return
|
val app = installedApp ?: return
|
||||||
when (app.installType) {
|
viewModelScope.launch {
|
||||||
InstallType.DEFAULT -> pm.uninstallPackage(app.currentPackageName)
|
when (app.installType) {
|
||||||
|
InstallType.DEFAULT -> {
|
||||||
InstallType.MOUNT -> viewModelScope.launch {
|
when (val result = pm.uninstallPackage(app.currentPackageName)) {
|
||||||
rootInstaller.uninstall(app.currentPackageName)
|
is Session.State.Failed<UninstallFailure> -> {
|
||||||
installedAppRepository.delete(app)
|
val msg = result.failure.message.orEmpty()
|
||||||
onBackClick()
|
context.toast(
|
||||||
}
|
this@InstalledAppInfoViewModel.context.getString(
|
||||||
}
|
R.string.uninstall_app_fail,
|
||||||
}
|
msg
|
||||||
|
)
|
||||||
private val uninstallBroadcastReceiver = object : BroadcastReceiver() {
|
)
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
return@launch
|
||||||
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) {
|
Session.State.Succeeded -> {}
|
||||||
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() {
|
InstallType.MOUNT -> rootInstaller.uninstall(app.currentPackageName)
|
||||||
super.onCleared()
|
}
|
||||||
context.unregisterReceiver(uninstallBroadcastReceiver)
|
installedAppRepository.delete(app)
|
||||||
|
onBackClick()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,11 +12,9 @@ import app.revanced.manager.R
|
|||||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
|
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.asRemoteOrNull
|
||||||
import app.revanced.manager.domain.manager.KeystoreManager
|
import app.revanced.manager.domain.manager.KeystoreManager
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
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.PatchBundleRepository
|
||||||
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
||||||
import app.revanced.manager.domain.repository.SerializedSelection
|
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.ui.theme.Theme
|
||||||
import app.revanced.manager.util.tag
|
import app.revanced.manager.util.tag
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
@@ -31,44 +29,14 @@ import kotlinx.serialization.json.Json
|
|||||||
class MainViewModel(
|
class MainViewModel(
|
||||||
private val patchBundleRepository: PatchBundleRepository,
|
private val patchBundleRepository: PatchBundleRepository,
|
||||||
private val patchSelectionRepository: PatchSelectionRepository,
|
private val patchSelectionRepository: PatchSelectionRepository,
|
||||||
private val downloadedAppRepository: DownloadedAppRepository,
|
|
||||||
private val keystoreManager: KeystoreManager,
|
private val keystoreManager: KeystoreManager,
|
||||||
private val app: Application,
|
private val app: Application,
|
||||||
val prefs: PreferencesManager,
|
val prefs: PreferencesManager,
|
||||||
private val json: Json
|
private val json: Json
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val appSelectChannel = Channel<SelectedApp>()
|
|
||||||
val appSelectFlow = appSelectChannel.receiveAsFlow()
|
|
||||||
private val legacyImportActivityChannel = Channel<Intent>()
|
private val legacyImportActivityChannel = Channel<Intent>()
|
||||||
val legacyImportActivityFlow = legacyImportActivityChannel.receiveAsFlow()
|
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 {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (!prefs.firstLaunch.get()) return@launch
|
if (!prefs.firstLaunch.get()) return@launch
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.pm.PackageInstaller as AndroidPackageInstaller
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.ParcelUuid
|
import android.os.ParcelUuid
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -16,7 +14,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.saveable.autoSaver
|
import androidx.compose.runtime.saveable.autoSaver
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.toMutableStateList
|
import androidx.compose.runtime.toMutableStateList
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.map
|
import androidx.lifecycle.map
|
||||||
@@ -32,32 +29,35 @@ import app.revanced.manager.data.room.apps.installed.InstalledApp
|
|||||||
import app.revanced.manager.domain.installer.RootInstaller
|
import app.revanced.manager.domain.installer.RootInstaller
|
||||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||||
import app.revanced.manager.domain.worker.WorkerRepository
|
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.LogLevel
|
||||||
import app.revanced.manager.patcher.logger.Logger
|
import app.revanced.manager.patcher.logger.Logger
|
||||||
import app.revanced.manager.patcher.worker.PatcherWorker
|
import app.revanced.manager.patcher.worker.PatcherWorker
|
||||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||||
import app.revanced.manager.plugin.downloader.UserInteractionException
|
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.InstallerModel
|
||||||
import app.revanced.manager.ui.model.ProgressKey
|
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.ui.model.State
|
||||||
import app.revanced.manager.ui.model.Step
|
|
||||||
import app.revanced.manager.ui.model.StepCategory
|
import app.revanced.manager.ui.model.StepCategory
|
||||||
import app.revanced.manager.ui.model.StepProgressProvider
|
import app.revanced.manager.ui.model.Step
|
||||||
import app.revanced.manager.ui.model.navigation.Patcher
|
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.PM
|
||||||
|
import app.revanced.manager.util.PatchSelection
|
||||||
|
import app.revanced.manager.util.asCode
|
||||||
import app.revanced.manager.util.saveableVar
|
import app.revanced.manager.util.saveableVar
|
||||||
import app.revanced.manager.util.saver.snapshotStateListSaver
|
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.toast
|
||||||
import app.revanced.manager.util.uiSafe
|
import app.revanced.manager.util.uiSafe
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.NonCancellable
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -66,6 +66,15 @@ import kotlinx.coroutines.withContext
|
|||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
import org.koin.core.component.inject
|
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.io.File
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@@ -73,7 +82,7 @@ import java.time.Duration
|
|||||||
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
|
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
|
||||||
class PatcherViewModel(
|
class PatcherViewModel(
|
||||||
private val input: Patcher.ViewModelParams
|
private val input: Patcher.ViewModelParams
|
||||||
) : ViewModel(), KoinComponent, StepProgressProvider, InstallerModel {
|
) : ViewModel(), KoinComponent, InstallerModel {
|
||||||
private val app: Application by inject()
|
private val app: Application by inject()
|
||||||
private val fs: Filesystem by inject()
|
private val fs: Filesystem by inject()
|
||||||
private val pm: PM by inject()
|
private val pm: PM by inject()
|
||||||
@@ -81,11 +90,11 @@ class PatcherViewModel(
|
|||||||
private val installedAppRepository: InstalledAppRepository by inject()
|
private val installedAppRepository: InstalledAppRepository by inject()
|
||||||
private val rootInstaller: RootInstaller by inject()
|
private val rootInstaller: RootInstaller by inject()
|
||||||
private val savedStateHandle: SavedStateHandle = get()
|
private val savedStateHandle: SavedStateHandle = get()
|
||||||
|
private val ackpineInstaller: PackageInstaller = get()
|
||||||
|
|
||||||
private var installedApp: InstalledApp? = null
|
private var installedApp: InstalledApp? = null
|
||||||
private val selectedApp = input.selectedApp
|
val packageName = input.packageName
|
||||||
val packageName = selectedApp.packageName
|
val version = input.version
|
||||||
val version = selectedApp.version
|
|
||||||
|
|
||||||
var installedPackageName by savedStateHandle.saveable(
|
var installedPackageName by savedStateHandle.saveable(
|
||||||
key = "installedPackageName",
|
key = "installedPackageName",
|
||||||
@@ -95,7 +104,6 @@ class PatcherViewModel(
|
|||||||
mutableStateOf<String?>(null)
|
mutableStateOf<String?>(null)
|
||||||
}
|
}
|
||||||
private set
|
private set
|
||||||
private var ongoingPmSession: Boolean by savedStateHandle.saveableVar { false }
|
|
||||||
var packageInstallerStatus: Int? by savedStateHandle.saveable(
|
var packageInstallerStatus: Int? by savedStateHandle.saveable(
|
||||||
key = "packageInstallerStatus",
|
key = "packageInstallerStatus",
|
||||||
stateSaver = autoSaver()
|
stateSaver = autoSaver()
|
||||||
@@ -104,7 +112,7 @@ class PatcherViewModel(
|
|||||||
}
|
}
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var isInstalling by mutableStateOf(ongoingPmSession)
|
var isInstalling by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private var currentActivityRequest: Pair<CompletableDeferred<Boolean>, String>? by mutableStateOf(
|
private var currentActivityRequest: Pair<CompletableDeferred<Boolean>, String>? by mutableStateOf(
|
||||||
@@ -123,6 +131,18 @@ 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 var inputFile: File? by savedStateHandle.saveableVar()
|
||||||
private val outputFile = tempDir.resolve("output.apk")
|
private val outputFile = tempDir.resolve("output.apk")
|
||||||
|
|
||||||
@@ -138,35 +158,15 @@ class PatcherViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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()) {
|
val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) {
|
||||||
generateSteps(
|
generateSteps(app, input.selectedSource, input.selectedPatches).toMutableStateList()
|
||||||
app,
|
|
||||||
input.selectedApp
|
|
||||||
).toMutableStateList()
|
|
||||||
}
|
}
|
||||||
private var currentStepIndex = 0
|
|
||||||
|
|
||||||
val progress by derivedStateOf {
|
val progress by derivedStateOf {
|
||||||
val current = steps.count {
|
val steps = steps.filter { it.id != StepId.ExecutePatches }
|
||||||
it.state == State.COMPLETED && it.category != StepCategory.PATCHING
|
|
||||||
} + completedPatchCount
|
|
||||||
|
|
||||||
val total = steps.size - 1 + patchCount
|
val current = steps.count { it.state == State.COMPLETED }
|
||||||
|
val total = steps.size
|
||||||
|
|
||||||
current.toFloat() / total.toFloat()
|
current.toFloat() / total.toFloat()
|
||||||
}
|
}
|
||||||
@@ -174,67 +174,48 @@ class PatcherViewModel(
|
|||||||
private val workManager = WorkManager.getInstance(app)
|
private val workManager = WorkManager.getInstance(app)
|
||||||
|
|
||||||
private val patcherWorkerId by savedStateHandle.saveable<ParcelUuid> {
|
private val patcherWorkerId by savedStateHandle.saveable<ParcelUuid> {
|
||||||
ParcelUuid(workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
ParcelUuid(
|
||||||
"patching", PatcherWorker.Args(
|
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
||||||
input.selectedApp,
|
"patching", PatcherWorker.Args(
|
||||||
outputFile.path,
|
input.packageName,
|
||||||
input.selectedPatches,
|
input.version,
|
||||||
input.options,
|
input.selectedSource,
|
||||||
logger,
|
outputFile.path,
|
||||||
onDownloadProgress = {
|
input.selectedPatches,
|
||||||
withContext(Dispatchers.Main) {
|
input.options,
|
||||||
downloadProgress = it
|
logger,
|
||||||
}
|
setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
|
||||||
},
|
handleStartActivityRequest = { plugin, intent ->
|
||||||
onPatchCompleted = { withContext(Dispatchers.Main) { completedPatchCount += 1 } },
|
withContext(Dispatchers.Main) {
|
||||||
setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
|
if (currentActivityRequest != null) throw Exception("Another request is already pending.")
|
||||||
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 {
|
try {
|
||||||
with(CompletableDeferred<ActivityResult>()) {
|
// Wait for the dialog interaction.
|
||||||
launchedActivity = this
|
val accepted = with(CompletableDeferred<Boolean>()) {
|
||||||
launchActivityChannel.send(intent)
|
currentActivityRequest = this to plugin.name
|
||||||
|
|
||||||
await()
|
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 {
|
} finally {
|
||||||
launchedActivity = null
|
currentActivityRequest = 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 =
|
val patcherSucceeded =
|
||||||
@@ -246,64 +227,26 @@ class PatcherViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val installerBroadcastReceiver = object : BroadcastReceiver() {
|
init {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
// TODO: detect system-initiated process death during the patching process.
|
||||||
when (intent?.action) {
|
|
||||||
InstallService.APP_INSTALL_ACTION -> {
|
|
||||||
val pmStatus = intent.getIntExtra(
|
|
||||||
InstallService.EXTRA_INSTALL_STATUS,
|
|
||||||
PackageInstaller.STATUS_FAILURE
|
|
||||||
)
|
|
||||||
|
|
||||||
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
installerSessionId?.uuid?.let { id ->
|
||||||
?.let(logger::trace)
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
|
isInstalling = true
|
||||||
app.toast(app.getString(R.string.install_app_success))
|
uiSafe(app, R.string.install_app_fail, "Failed to install") {
|
||||||
installedPackageName =
|
// The process was killed during installation. Await the session again.
|
||||||
intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
|
withContext(Dispatchers.IO) {
|
||||||
viewModelScope.launch {
|
ackpineInstaller.getSession(id)
|
||||||
installedAppRepository.addOrUpdate(
|
}?.let {
|
||||||
installedPackageName!!,
|
awaitInstallation(it)
|
||||||
packageName,
|
|
||||||
input.selectedApp.version
|
|
||||||
?: pm.getPackageInfo(outputFile)?.versionName!!,
|
|
||||||
InstallType.DEFAULT,
|
|
||||||
input.selectedPatches
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else packageInstallerStatus = pmStatus
|
}
|
||||||
|
} finally {
|
||||||
isInstalling = false
|
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 {
|
viewModelScope.launch {
|
||||||
installedApp = installedAppRepository.get(packageName)
|
installedApp = installedAppRepository.get(packageName)
|
||||||
@@ -313,10 +256,9 @@ class PatcherViewModel(
|
|||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
app.unregisterReceiver(installerBroadcastReceiver)
|
|
||||||
workManager.cancelWorkById(patcherWorkerId.uuid)
|
workManager.cancelWorkById(patcherWorkerId.uuid)
|
||||||
|
|
||||||
if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) {
|
if (input.selectedSource is SelectedSource.Installed && installedApp?.installType == InstallType.MOUNT) {
|
||||||
GlobalScope.launch(Dispatchers.Main) {
|
GlobalScope.launch(Dispatchers.Main) {
|
||||||
uiSafe(app, R.string.failed_to_mount, "Failed to mount") {
|
uiSafe(app, R.string.failed_to_mount, "Failed to mount") {
|
||||||
withTimeout(Duration.ofMinutes(1L)) {
|
withTimeout(Duration.ofMinutes(1L)) {
|
||||||
@@ -327,7 +269,37 @@ 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() {
|
fun onBack() {
|
||||||
|
installerCoroutineScope.cancel()
|
||||||
// tempDir cannot be deleted inside onCleared because it gets called on system-initiated process death.
|
// tempDir cannot be deleted inside onCleared because it gets called on system-initiated process death.
|
||||||
tempDir.deleteRecursively()
|
tempDir.deleteRecursively()
|
||||||
}
|
}
|
||||||
@@ -372,44 +344,93 @@ class PatcherViewModel(
|
|||||||
|
|
||||||
fun open() = installedPackageName?.let(pm::launch)
|
fun open() = installedPackageName?.let(pm::launch)
|
||||||
|
|
||||||
fun install(installType: InstallType) = viewModelScope.launch {
|
private suspend fun startInstallation(file: File, packageName: String) {
|
||||||
var pmInstallStarted = false
|
val session = withContext(Dispatchers.IO) {
|
||||||
try {
|
ackpineInstaller.createSession(Uri.fromFile(file)) {
|
||||||
isInstalling = true
|
confirmation = Confirmation.IMMEDIATE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
installerPkgName = packageName
|
||||||
|
}
|
||||||
|
awaitInstallation(session)
|
||||||
|
}
|
||||||
|
|
||||||
val currentPackageInfo = pm.getPackageInfo(outputFile)
|
private suspend fun awaitInstallation(session: ProgressSession<InstallFailure>) = withContext(
|
||||||
?: throw Exception("Failed to load application info")
|
Dispatchers.Main
|
||||||
|
) {
|
||||||
// If the app is currently installed
|
val result = installerCoroutineScope.async {
|
||||||
val existingPackageInfo = pm.getPackageInfo(currentPackageInfo.packageName)
|
try {
|
||||||
if (existingPackageInfo != null) {
|
installerSessionId = ParcelUuid(session.id)
|
||||||
// Check if the app version is less than the installed version
|
withContext(Dispatchers.IO) {
|
||||||
if (pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(existingPackageInfo)) {
|
session.await()
|
||||||
// Exit if the selected app version is less than the installed version
|
|
||||||
packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT
|
|
||||||
return@launch
|
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
installerSessionId = null
|
||||||
|
}
|
||||||
|
}.await()
|
||||||
|
|
||||||
|
when (result) {
|
||||||
|
is Session.State.Failed<InstallFailure> -> {
|
||||||
|
result.failure.message?.let(logger::trace)
|
||||||
|
packageInstallerStatus = result.failure.asCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
when (installType) {
|
Session.State.Succeeded -> {
|
||||||
InstallType.DEFAULT -> {
|
app.toast(app.getString(R.string.install_app_success))
|
||||||
// Check if the app is mounted as root
|
installedPackageName = installerPkgName
|
||||||
// If it is, unmount it first, silently
|
installedAppRepository.addOrUpdate(
|
||||||
if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) {
|
installerPkgName,
|
||||||
rootInstaller.unmount(packageName)
|
packageName,
|
||||||
}
|
input.version
|
||||||
|
?: withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile)?.versionName!! },
|
||||||
|
InstallType.DEFAULT,
|
||||||
|
input.selectedPatches
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Install regularly
|
fun install(installType: InstallType) = viewModelScope.launch {
|
||||||
pm.installApp(listOf(outputFile))
|
isInstalling = true
|
||||||
pmInstallStarted = true
|
var needsRootUninstall = 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")
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
InstallType.MOUNT -> {
|
when (installType) {
|
||||||
try {
|
InstallType.DEFAULT -> {
|
||||||
val packageInfo = pm.getPackageInfo(outputFile)
|
// Check if the app is mounted as root
|
||||||
?: throw Exception("Failed to load application info")
|
// If it is, unmount it first, silently
|
||||||
|
if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) {
|
||||||
|
rootInstaller.unmount(packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install regularly
|
||||||
|
startInstallation(outputFile, currentPackageInfo.packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
InstallType.MOUNT -> {
|
||||||
val label = with(pm) {
|
val label = with(pm) {
|
||||||
packageInfo.label()
|
currentPackageInfo.label()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for base APK, first check if the app is already installed
|
// Check for base APK, first check if the app is already installed
|
||||||
@@ -417,15 +438,17 @@ class PatcherViewModel(
|
|||||||
// If the app is not installed, check if the output file is a base apk
|
// If the app is not installed, check if the output file is a base apk
|
||||||
if (currentPackageInfo.splitNames.isNotEmpty()) {
|
if (currentPackageInfo.splitNames.isNotEmpty()) {
|
||||||
// Exit if there is no base APK package
|
// Exit if there is no base APK package
|
||||||
packageInstallerStatus = PackageInstaller.STATUS_FAILURE_INVALID
|
packageInstallerStatus =
|
||||||
|
AndroidPackageInstaller.STATUS_FAILURE_INVALID
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val inputVersion = input.selectedApp.version
|
val inputVersion = input.version
|
||||||
?: inputFile?.let(pm::getPackageInfo)?.versionName
|
?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName }
|
||||||
?: throw Exception("Failed to determine input APK version")
|
?: throw Exception("Failed to determine input APK version")
|
||||||
|
|
||||||
|
needsRootUninstall = true
|
||||||
// Install as root
|
// Install as root
|
||||||
rootInstaller.install(
|
rootInstaller.install(
|
||||||
outputFile,
|
outputFile,
|
||||||
@@ -436,7 +459,7 @@ class PatcherViewModel(
|
|||||||
)
|
)
|
||||||
|
|
||||||
installedAppRepository.addOrUpdate(
|
installedAppRepository.addOrUpdate(
|
||||||
packageInfo.packageName,
|
currentPackageInfo.packageName,
|
||||||
packageName,
|
packageName,
|
||||||
inputVersion,
|
inputVersion,
|
||||||
InstallType.MOUNT,
|
InstallType.MOUNT,
|
||||||
@@ -448,21 +471,20 @@ class PatcherViewModel(
|
|||||||
installedPackageName = packageName
|
installedPackageName = packageName
|
||||||
|
|
||||||
app.toast(app.getString(R.string.install_app_success))
|
app.toast(app.getString(R.string.install_app_success))
|
||||||
} catch (e: Exception) {
|
needsRootUninstall = false
|
||||||
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 {
|
} finally {
|
||||||
if (!pmInstallStarted) isInstalling = false
|
isInstalling = false
|
||||||
|
if (needsRootUninstall) {
|
||||||
|
try {
|
||||||
|
withContext(NonCancellable) {
|
||||||
|
rootInstaller.uninstall(packageName)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,12 +495,27 @@ class PatcherViewModel(
|
|||||||
|
|
||||||
override fun reinstall() {
|
override fun reinstall() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") {
|
try {
|
||||||
pm.getPackageInfo(outputFile)?.packageName?.let { pm.uninstallPackage(it) }
|
|
||||||
?: throw Exception("Failed to load application info")
|
|
||||||
|
|
||||||
pm.installApp(listOf(outputFile))
|
|
||||||
isInstalling = true
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -497,34 +534,66 @@ class PatcherViewModel(
|
|||||||
LogLevel.ERROR -> Log.e(TAG, msg)
|
LogLevel.ERROR -> Log.e(TAG, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun generateSteps(context: Context, selectedApp: SelectedApp): List<Step> {
|
fun generateSteps(
|
||||||
val needsDownload =
|
context: Context,
|
||||||
selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search
|
selectedSource: SelectedSource,
|
||||||
|
selectedPatches: PatchSelection
|
||||||
|
): List<Step> = buildList {
|
||||||
|
if (selectedSource is SelectedSource.Plugin)
|
||||||
|
add(
|
||||||
|
Step(
|
||||||
|
StepId.DownloadAPK,
|
||||||
|
context.getString(R.string.download_apk),
|
||||||
|
StepCategory.PREPARING
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return listOfNotNull(
|
add(
|
||||||
Step(
|
|
||||||
context.getString(R.string.download_apk),
|
|
||||||
StepCategory.PREPARING,
|
|
||||||
state = State.RUNNING,
|
|
||||||
progressKey = ProgressKey.DOWNLOAD,
|
|
||||||
).takeIf { needsDownload },
|
|
||||||
Step(
|
Step(
|
||||||
|
StepId.LoadPatches,
|
||||||
context.getString(R.string.patcher_step_load_patches),
|
context.getString(R.string.patcher_step_load_patches),
|
||||||
StepCategory.PREPARING,
|
StepCategory.PREPARING
|
||||||
state = if (needsDownload) State.WAITING else State.RUNNING,
|
)
|
||||||
),
|
)
|
||||||
|
add(
|
||||||
Step(
|
Step(
|
||||||
|
StepId.ReadAPK,
|
||||||
context.getString(R.string.patcher_step_unpack),
|
context.getString(R.string.patcher_step_unpack),
|
||||||
StepCategory.PREPARING
|
StepCategory.PREPARING
|
||||||
),
|
)
|
||||||
|
)
|
||||||
|
add(
|
||||||
Step(
|
Step(
|
||||||
|
StepId.ExecutePatches,
|
||||||
context.getString(R.string.execute_patches),
|
context.getString(R.string.execute_patches),
|
||||||
StepCategory.PATCHING
|
StepCategory.PATCHING,
|
||||||
),
|
hide = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
Step(context.getString(R.string.patcher_step_write_patched), StepCategory.SAVING),
|
selectedPatches.values.asSequence().flatten().sorted().forEachIndexed { index, name ->
|
||||||
Step(context.getString(R.string.patcher_step_sign_apk), StepCategory.SAVING)
|
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
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection
|
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection
|
||||||
import app.revanced.manager.patcher.patch.PatchInfo
|
import app.revanced.manager.patcher.patch.PatchInfo
|
||||||
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
import app.revanced.manager.ui.model.navigation.SelectedAppInfo
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
import app.revanced.manager.util.saver.Nullable
|
import app.revanced.manager.util.saver.Nullable
|
||||||
@@ -45,14 +45,14 @@ import org.koin.core.component.KoinComponent
|
|||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
|
|
||||||
@OptIn(SavedStateHandleSaveableApi::class)
|
@OptIn(SavedStateHandleSaveableApi::class)
|
||||||
class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.ViewModelParams) :
|
class PatchesSelectorViewModel(input: SelectedAppInfo.PatchesSelector.ViewModelParams) :
|
||||||
ViewModel(), KoinComponent {
|
ViewModel(), KoinComponent {
|
||||||
private val app: Application = get()
|
private val app: Application = get()
|
||||||
private val savedStateHandle: SavedStateHandle = get()
|
private val savedStateHandle: SavedStateHandle = get()
|
||||||
private val prefs: PreferencesManager = get()
|
private val prefs: PreferencesManager = get()
|
||||||
|
|
||||||
private val packageName = input.app.packageName
|
private val packageName = input.packageName
|
||||||
val appVersion = input.app.version
|
val appVersion = input.version
|
||||||
|
|
||||||
var selectionWarningEnabled by mutableStateOf(true)
|
var selectionWarningEnabled by mutableStateOf(true)
|
||||||
private set
|
private set
|
||||||
@@ -62,7 +62,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
|||||||
val allowIncompatiblePatches =
|
val allowIncompatiblePatches =
|
||||||
get<PreferencesManager>().disablePatchVersionCompatCheck.getBlocking()
|
get<PreferencesManager>().disablePatchVersionCompatCheck.getBlocking()
|
||||||
val bundlesFlow =
|
val bundlesFlow =
|
||||||
get<PatchBundleRepository>().scopedBundleInfoFlow(packageName, input.app.version)
|
get<PatchBundleRepository>().scopedBundleInfoFlow(packageName, input.version)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -88,7 +88,7 @@ class PatchesSelectorViewModel(input: SelectedApplicationInfo.PatchesSelector.Vi
|
|||||||
key = "selection",
|
key = "selection",
|
||||||
stateSaver = selectionSaver,
|
stateSaver = selectionSaver,
|
||||||
) {
|
) {
|
||||||
mutableStateOf(input.currentSelection?.toPersistentPatchSelection())
|
mutableStateOf(input.patchSelection?.toPersistentPatchSelection())
|
||||||
}
|
}
|
||||||
|
|
||||||
private val patchOptions: PersistentOptions by savedStateHandle.saveable(
|
private val patchOptions: PersistentOptions by savedStateHandle.saveable(
|
||||||
|
|||||||
@@ -1,130 +1,188 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
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.content.pm.PackageInfo
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.util.Log
|
|
||||||
import androidx.activity.result.ActivityResult
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshotFlow
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
|
import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
|
||||||
import androidx.lifecycle.viewmodel.compose.saveable
|
import androidx.lifecycle.viewmodel.compose.saveable
|
||||||
import app.revanced.manager.R
|
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.manager.PreferencesManager
|
||||||
|
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
||||||
import app.revanced.manager.domain.repository.DownloaderPluginRepository
|
import app.revanced.manager.domain.repository.DownloaderPluginRepository
|
||||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.domain.repository.PatchOptionsRepository
|
import app.revanced.manager.domain.repository.PatchOptionsRepository
|
||||||
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
||||||
import app.revanced.manager.patcher.patch.PatchBundleInfo
|
import app.revanced.manager.patcher.patch.PatchBundleInfo
|
||||||
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.patcher.patch.PatchBundleInfo.Extensions.requiredOptionsSet
|
||||||
import app.revanced.manager.plugin.downloader.GetScope
|
import app.revanced.manager.patcher.patch.PatchBundleInfo.Extensions.toPatchSelection
|
||||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
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.SelectedVersion
|
||||||
import app.revanced.manager.ui.model.navigation.Patcher
|
import app.revanced.manager.ui.model.navigation.Patcher
|
||||||
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
import app.revanced.manager.ui.model.navigation.SelectedAppInfo
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
import app.revanced.manager.util.simpleMessage
|
import app.revanced.manager.util.patchCount
|
||||||
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.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
|
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
|
||||||
class SelectedAppInfoViewModel(
|
class SelectedAppInfoViewModel(
|
||||||
input: SelectedApplicationInfo.ViewModelParams
|
private val input: SelectedAppInfo.ViewModelParams
|
||||||
) : ViewModel(), KoinComponent {
|
) : ViewModel(), KoinComponent {
|
||||||
private val app: Application = get()
|
|
||||||
private val bundleRepository: PatchBundleRepository = get()
|
private val bundleRepository: PatchBundleRepository = get()
|
||||||
private val selectionRepository: PatchSelectionRepository = get()
|
private val selectionRepository: PatchSelectionRepository = get()
|
||||||
private val optionsRepository: PatchOptionsRepository = get()
|
private val optionsRepository: PatchOptionsRepository = get()
|
||||||
private val pluginsRepository: DownloaderPluginRepository = get()
|
private val pluginsRepository: DownloaderPluginRepository = get()
|
||||||
private val installedAppRepository: InstalledAppRepository = get()
|
private val installedAppRepository: InstalledAppRepository = get()
|
||||||
private val rootInstaller: RootInstaller = get()
|
private val downloadedAppRepository: DownloadedAppRepository = get()
|
||||||
private val pm: PM = get()
|
private val pm: PM = get()
|
||||||
private val savedStateHandle: SavedStateHandle = get()
|
private val savedStateHandle: SavedStateHandle = get()
|
||||||
val prefs: PreferencesManager = get()
|
private val prefs: PreferencesManager = get()
|
||||||
val plugins = pluginsRepository.loadedPluginsFlow
|
val plugins = pluginsRepository.loadedPluginsFlow
|
||||||
val desiredVersion = input.app.version
|
val packageName = input.packageName
|
||||||
val packageName = input.app.packageName
|
val localPath = input.localPath
|
||||||
|
|
||||||
private val persistConfiguration = input.patches == null
|
private val persistConfiguration = input.patches == null
|
||||||
|
|
||||||
val hasRoot = rootInstaller.hasRootAccess()
|
|
||||||
var installedAppData: Pair<SelectedApp.Installed, InstalledApp?>? by mutableStateOf(null)
|
|
||||||
private set
|
|
||||||
|
|
||||||
private var _selectedApp by savedStateHandle.saveable {
|
// User selection
|
||||||
mutableStateOf(input.app)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var selectedAppInfo: PackageInfo? by mutableStateOf(null)
|
|
||||||
private set
|
|
||||||
|
|
||||||
var selectedApp
|
// All patches for package
|
||||||
get() = _selectedApp
|
val bundles = bundleRepository.scopedBundleInfoFlow(packageName, null)
|
||||||
set(value) {
|
|
||||||
_selectedApp = value
|
// Selection derived from selectionFlow
|
||||||
invalidateSelectedAppInfo()
|
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
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
invalidateSelectedAppInfo()
|
val scopedBundles = resolvedVersion.flatMapLatest { version ->
|
||||||
viewModelScope.launch(Dispatchers.Main) {
|
bundleRepository.scopedBundleInfoFlow(packageName, version)
|
||||||
val packageInfo = async(Dispatchers.IO) { pm.getPackageInfo(packageName) }
|
}
|
||||||
val installedAppDeferred =
|
|
||||||
async(Dispatchers.IO) { installedAppRepository.get(packageName) }
|
|
||||||
|
|
||||||
installedAppData =
|
val incompatiblePatchCount = scopedBundles.map { bundles ->
|
||||||
packageInfo.await()?.let {
|
bundles.sumOf { bundle ->
|
||||||
SelectedApp.Installed(
|
bundle.incompatible.size
|
||||||
packageName,
|
}
|
||||||
it.versionName!!
|
}
|
||||||
) to installedAppDeferred.await()
|
|
||||||
|
// 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 requiredVersion = combine(
|
|
||||||
prefs.suggestedVersionSafeguard.flow,
|
|
||||||
bundleRepository.suggestedVersions
|
|
||||||
) { suggestedVersionSafeguard, suggestedVersions ->
|
|
||||||
if (!suggestedVersionSafeguard) return@combine null
|
|
||||||
|
|
||||||
suggestedVersions[input.app.packageName]
|
|
||||||
}
|
|
||||||
|
|
||||||
val bundleInfoFlow by derivedStateOf {
|
val bundleInfoFlow by derivedStateOf {
|
||||||
bundleRepository.scopedBundleInfoFlow(packageName, selectedApp.version)
|
bundleRepository.scopedBundleInfoFlow(packageName, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
var options: Options by savedStateHandle.saveable {
|
var options: Options by savedStateHandle.saveable {
|
||||||
@@ -142,121 +200,42 @@ class SelectedAppInfoViewModel(
|
|||||||
}
|
}
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private var selectionState: SelectionState by savedStateHandle.saveable {
|
|
||||||
if (input.patches != null)
|
|
||||||
return@saveable mutableStateOf(SelectionState.Customized(input.patches))
|
|
||||||
|
|
||||||
// Try to get the previous selection if customization is enabled.
|
val errorFlow = combine(
|
||||||
viewModelScope.launch {
|
plugins,
|
||||||
if (!prefs.disableSelectionWarning.get()) return@launch
|
resolvedSource,
|
||||||
|
) { pluginsList, source ->
|
||||||
val previous = selectionRepository.getSelection(packageName)
|
|
||||||
if (previous.values.sumOf { it.size } == 0) return@launch
|
|
||||||
selectionState = SelectionState.Customized(previous)
|
|
||||||
}
|
|
||||||
|
|
||||||
mutableStateOf(SelectionState.Default)
|
|
||||||
}
|
|
||||||
|
|
||||||
var showSourceSelector by mutableStateOf(false)
|
|
||||||
private set
|
|
||||||
private var 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 {
|
when {
|
||||||
app is SelectedApp.Search && pluginsList.isEmpty() -> Error.NoPlugins
|
source is SelectedSource.Plugin && pluginsList.isEmpty() -> Error.NoPlugins
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showSourceSelector() {
|
|
||||||
dismissSourceSelector()
|
|
||||||
showSourceSelector = true
|
// var installedAppData: Pair<SelectedApp.Installed, InstalledApp?>? by mutableStateOf(null)
|
||||||
|
// private set
|
||||||
|
|
||||||
|
private var _selectedApp by savedStateHandle.saveable {
|
||||||
|
mutableStateOf(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelPluginAction() {
|
var selectedAppInfo: PackageInfo? by mutableStateOf(null)
|
||||||
pluginAction?.second?.cancel()
|
private set
|
||||||
pluginAction = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dismissSourceSelector() {
|
var selectedApp
|
||||||
cancelPluginAction()
|
get() = _selectedApp
|
||||||
showSourceSelector = false
|
set(value) {
|
||||||
}
|
_selectedApp = value
|
||||||
|
invalidateSelectedAppInfo()
|
||||||
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 {
|
private fun invalidateSelectedAppInfo() = viewModelScope.launch {
|
||||||
val info = when (val app = selectedApp) {
|
selectedAppInfo = pm.getPackageInfo(packageName)
|
||||||
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)
|
fun getOptionsFiltered(bundles: List<PatchBundleInfo.Scoped>) = options.filtered(bundles)
|
||||||
@@ -272,37 +251,50 @@ class SelectedAppInfoViewModel(
|
|||||||
val allowIncompatible = prefs.disablePatchVersionCompatCheck.get()
|
val allowIncompatible = prefs.disablePatchVersionCompatCheck.get()
|
||||||
val bundles = bundleInfoFlow.first()
|
val bundles = bundleInfoFlow.first()
|
||||||
return Patcher.ViewModelParams(
|
return Patcher.ViewModelParams(
|
||||||
selectedApp,
|
input.packageName,
|
||||||
getPatches(bundles, allowIncompatible),
|
resolvedVersion.first(),
|
||||||
|
resolvedSource.first(),
|
||||||
|
patchSelection.first(),
|
||||||
getOptionsFiltered(bundles)
|
getOptionsFiltered(bundles)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPatches(bundles: List<PatchBundleInfo.Scoped>, allowIncompatible: Boolean) =
|
init {
|
||||||
selectionState.patches(bundles, allowIncompatible)
|
invalidateSelectedAppInfo()
|
||||||
|
|
||||||
fun getCustomPatches(
|
input.localPath?.let { local ->
|
||||||
bundles: List<PatchBundleInfo.Scoped>,
|
viewModelScope.launch {
|
||||||
allowIncompatible: Boolean
|
val packageInfo = pm.getPackageInfo(File(local))
|
||||||
): PatchSelection? =
|
|
||||||
(selectionState as? SelectionState.Customized)?.patches(bundles, allowIncompatible)
|
|
||||||
|
|
||||||
|
_selectedVersion.value = SelectedVersion.Specific(
|
||||||
|
packageInfo?.versionName ?: return@launch
|
||||||
|
)
|
||||||
|
_selectedSource.value = SelectedSource.Local(local)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun updateConfiguration(
|
// Get the previous selection if customization is enabled.
|
||||||
selection: PatchSelection?,
|
viewModelScope.launch {
|
||||||
options: Options
|
if (prefs.disableSelectionWarning.get()) {
|
||||||
) = viewModelScope.launch {
|
val previous = selectionRepository.getSelection(packageName)
|
||||||
selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default
|
if (previous.patchCount == 0) return@launch
|
||||||
|
selectionFlow.value = SelectionState.Customized(previous)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val filteredOptions = options.filtered(bundleInfoFlow.first())
|
// Get installed app info
|
||||||
this@SelectedAppInfoViewModel.options = filteredOptions
|
viewModelScope.launch {
|
||||||
|
val packageInfo = async(Dispatchers.IO) { pm.getPackageInfo(packageName) }
|
||||||
|
val installedAppDeferred =
|
||||||
|
async(Dispatchers.IO) { installedAppRepository.get(packageName) }
|
||||||
|
|
||||||
if (!persistConfiguration) return@launch
|
// installedAppData =
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
// packageInfo.await()?.let {
|
||||||
selection?.let { selectionRepository.updateSelection(packageName, it) }
|
// SelectedApp.Installed(
|
||||||
?: selectionRepository.resetSelectionForPackage(packageName)
|
// packageName,
|
||||||
|
// it.versionName!!
|
||||||
optionsRepository.saveOptions(packageName, filteredOptions)
|
// ) to installedAppDeferred.await()
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
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,18 +1,13 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.BroadcastReceiver
|
import android.net.Uri
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
@@ -21,8 +16,6 @@ import app.revanced.manager.data.platform.NetworkInfo
|
|||||||
import app.revanced.manager.network.api.ReVancedAPI
|
import app.revanced.manager.network.api.ReVancedAPI
|
||||||
import app.revanced.manager.network.dto.ReVancedAsset
|
import app.revanced.manager.network.dto.ReVancedAsset
|
||||||
import app.revanced.manager.network.service.HttpService
|
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.toast
|
||||||
import app.revanced.manager.util.uiSafe
|
import app.revanced.manager.util.uiSafe
|
||||||
import io.ktor.client.plugins.onDownload
|
import io.ktor.client.plugins.onDownload
|
||||||
@@ -31,7 +24,14 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.get
|
||||||
import org.koin.core.component.inject
|
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(
|
class UpdateViewModel(
|
||||||
private val downloadOnScreenEntry: Boolean
|
private val downloadOnScreenEntry: Boolean
|
||||||
@@ -39,10 +39,11 @@ class UpdateViewModel(
|
|||||||
private val app: Application by inject()
|
private val app: Application by inject()
|
||||||
private val reVancedAPI: ReVancedAPI by inject()
|
private val reVancedAPI: ReVancedAPI by inject()
|
||||||
private val http: HttpService by inject()
|
private val http: HttpService by inject()
|
||||||
private val pm: PM by inject()
|
|
||||||
private val networkInfo: NetworkInfo by inject()
|
private val networkInfo: NetworkInfo by inject()
|
||||||
private val fs: Filesystem 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)
|
var downloadedSize by mutableLongStateOf(0L)
|
||||||
private set
|
private set
|
||||||
var totalSize by mutableLongStateOf(0L)
|
var totalSize by mutableLongStateOf(0L)
|
||||||
@@ -62,14 +63,17 @@ class UpdateViewModel(
|
|||||||
private set
|
private set
|
||||||
|
|
||||||
private val location = fs.tempDir.resolve("updater.apk")
|
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")
|
|
||||||
|
|
||||||
if (downloadOnScreenEntry) {
|
init {
|
||||||
downloadUpdate()
|
viewModelScope.launch {
|
||||||
} else {
|
uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
|
||||||
state = State.CAN_DOWNLOAD
|
releaseInfo = reVancedAPI.getAppUpdate() ?: throw Exception("No update available")
|
||||||
|
|
||||||
|
if (downloadOnScreenEntry) {
|
||||||
|
downloadUpdate()
|
||||||
|
} else {
|
||||||
|
state = State.CAN_DOWNLOAD
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,50 +102,36 @@ class UpdateViewModel(
|
|||||||
|
|
||||||
fun installUpdate() = viewModelScope.launch {
|
fun installUpdate() = viewModelScope.launch {
|
||||||
state = State.INSTALLING
|
state = State.INSTALLING
|
||||||
|
val result = withContext(Dispatchers.IO) {
|
||||||
|
ackpineInstaller.createSession(Uri.fromFile(location)) {
|
||||||
|
confirmation = Confirmation.IMMEDIATE
|
||||||
|
}.await()
|
||||||
|
}
|
||||||
|
|
||||||
pm.installApp(listOf(location))
|
when (result) {
|
||||||
}
|
is Session.State.Failed<InstallFailure> -> when (val failure = result.failure) {
|
||||||
|
is InstallFailure.Aborted -> state = State.CAN_INSTALL
|
||||||
private val installBroadcastReceiver = object : BroadcastReceiver() {
|
else -> {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
val msg = failure.message.orEmpty()
|
||||||
intent?.let {
|
app.toast(app.getString(R.string.install_app_fail, msg))
|
||||||
val pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999)
|
installError = msg
|
||||||
val extra =
|
state = State.FAILED
|
||||||
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() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
app.unregisterReceiver(installBroadcastReceiver)
|
|
||||||
|
|
||||||
job.cancel()
|
|
||||||
location.delete()
|
location.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class State(@StringRes val title: Int) {
|
enum class State(@param:StringRes val title: Int) {
|
||||||
CAN_DOWNLOAD(R.string.update_available),
|
CAN_DOWNLOAD(R.string.update_available),
|
||||||
DOWNLOADING(R.string.downloading_manager_update),
|
DOWNLOADING(R.string.downloading_manager_update),
|
||||||
CAN_INSTALL(R.string.ready_to_install_update),
|
CAN_INSTALL(R.string.ready_to_install_update),
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
30
app/src/main/java/app/revanced/manager/util/Ackpine.kt
Normal file
30
app/src/main/java/app/revanced/manager/util/Ackpine.kt
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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,11 +2,8 @@ package app.revanced.manager.util
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.pm.PackageManager.PackageInfoFlags
|
import android.content.pm.PackageManager.PackageInfoFlags
|
||||||
import android.content.pm.PackageManager.NameNotFoundException
|
import android.content.pm.PackageManager.NameNotFoundException
|
||||||
@@ -16,8 +13,6 @@ import android.os.Build
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
@@ -25,10 +20,13 @@ import kotlinx.coroutines.flow.flowOn
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.parcelize.Parcelize
|
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
|
import java.io.File
|
||||||
|
|
||||||
private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class AppInfo(
|
data class AppInfo(
|
||||||
@@ -40,7 +38,8 @@ data class AppInfo(
|
|||||||
@SuppressLint("QueryPermissionsNeeded")
|
@SuppressLint("QueryPermissionsNeeded")
|
||||||
class PM(
|
class PM(
|
||||||
private val app: Application,
|
private val app: Application,
|
||||||
patchBundleRepository: PatchBundleRepository
|
patchBundleRepository: PatchBundleRepository,
|
||||||
|
private val uninstaller: PackageUninstaller
|
||||||
) {
|
) {
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
@@ -145,17 +144,11 @@ class PM(
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun installApp(apks: List<File>) = withContext(Dispatchers.IO) {
|
suspend fun uninstallPackage(pkg: String, config: UninstallParametersDsl.() -> Unit = {}) = withContext(Dispatchers.IO) {
|
||||||
val packageInstaller = app.packageManager.packageInstaller
|
uninstaller.createSession(pkg) {
|
||||||
packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
|
confirmation = Confirmation.IMMEDIATE
|
||||||
apks.forEach { apk -> session.writeApk(apk) }
|
config()
|
||||||
session.commit(app.installIntentSender)
|
}.await()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun uninstallPackage(pkg: String) {
|
|
||||||
val packageInstaller = app.packageManager.packageInstaller
|
|
||||||
packageInstaller.uninstall(pkg, app.uninstallIntentSender)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launch(pkg: String) = app.packageManager.getLaunchIntentForPackage(pkg)?.let {
|
fun launch(pkg: String) = app.packageManager.getLaunchIntentForPackage(pkg)?.let {
|
||||||
@@ -164,44 +157,4 @@ class PM(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun canInstallPackages() = app.packageManager.canRequestPackageInstalls()
|
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,6 +33,7 @@ import androidx.lifecycle.SavedStateHandle
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -57,6 +58,9 @@ import kotlin.reflect.KProperty
|
|||||||
typealias PatchSelection = Map<Int, Set<String>>
|
typealias PatchSelection = Map<Int, Set<String>>
|
||||||
typealias Options = Map<Int, Map<String, Map<String, Any?>>>
|
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
|
val Context.isDebuggable get() = 0 != applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
|
||||||
|
|
||||||
fun Context.openUrl(url: String) {
|
fun Context.openUrl(url: String) {
|
||||||
@@ -82,6 +86,8 @@ fun Context.toast(string: String, duration: Int = Toast.LENGTH_SHORT) {
|
|||||||
inline fun uiSafe(context: Context, @StringRes toastMsg: Int, logMsg: String, block: () -> Unit) {
|
inline fun uiSafe(context: Context, @StringRes toastMsg: Int, logMsg: String, block: () -> Unit) {
|
||||||
try {
|
try {
|
||||||
block()
|
block()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (error: Exception) {
|
} catch (error: Exception) {
|
||||||
// You can only toast on the main thread.
|
// You can only toast on the main thread.
|
||||||
GlobalScope.launch(Dispatchers.Main) {
|
GlobalScope.launch(Dispatchers.Main) {
|
||||||
|
|||||||
@@ -61,14 +61,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="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_item_description">Start patching the application</string>
|
||||||
<string name="patch_selector_item">Select patches</string>
|
<string name="patch_selector_item">Patches</string>
|
||||||
<string name="patch_selector_item_description">%d patches selected</string>
|
<string name="patch_selector_item_description">%d selected</string>
|
||||||
<string name="no_patches_selected">No 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_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="network_metered_warning">You are currently on a metered connection. Data charges from your service provider may apply.</string>
|
||||||
|
|
||||||
<string name="apk_source_selector_item">Select APK source</string>
|
<string name="apk_source_selector_item">APK source</string>
|
||||||
<string name="apk_source_auto">Using all APK downloaders</string>
|
<string name="apk_source_auto">Using all APK downloaders</string>
|
||||||
<string name="apk_source_downloader">Using %s</string>
|
<string name="apk_source_downloader">Using %s</string>
|
||||||
<string name="apk_source_installed">Using installed APK</string>
|
<string name="apk_source_installed">Using installed APK</string>
|
||||||
@@ -202,7 +203,7 @@ You will not be able to update the previously installed apps from this source."<
|
|||||||
<string name="share">Share</string>
|
<string name="share">Share</string>
|
||||||
<string name="patch">Patch</string>
|
<string name="patch">Patch</string>
|
||||||
<string name="select_from_storage">Select from storage</string>
|
<string name="select_from_storage">Select from storage</string>
|
||||||
<string name="select_from_storage_description">Select an APK file from storage using file picker</string>
|
<string name="select_from_storage_description">Select an APK file from storage</string>
|
||||||
<string name="suggested_version_info">Suggested version: %s</string>
|
<string name="suggested_version_info">Suggested version: %s</string>
|
||||||
<string name="type_anything">Type anything to continue</string>
|
<string name="type_anything">Type anything to continue</string>
|
||||||
<string name="search">Search patches…</string>
|
<string name="search">Search patches…</string>
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ kotlin-process = "1.5.1"
|
|||||||
hidden-api-stub = "4.3.3"
|
hidden-api-stub = "4.3.3"
|
||||||
binary-compatibility-validator = "0.17.0"
|
binary-compatibility-validator = "0.17.0"
|
||||||
semver-parser = "3.0.0"
|
semver-parser = "3.0.0"
|
||||||
|
ackpine = "0.18.5"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
# AndroidX Core
|
# AndroidX Core
|
||||||
@@ -133,6 +134,10 @@ compose-icons-fontawesome = { group = "com.github.BenjaminHalko.compose-icons",
|
|||||||
# Semantic versioning parser
|
# Semantic versioning parser
|
||||||
semver-parser = { module = "io.github.z4kn4fein:semver", version.ref = "semver-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]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
|
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
|
||||||
android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" }
|
android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" }
|
||||||
|
|||||||
Reference in New Issue
Block a user