Compare commits

..

2 Commits

Author SHA1 Message Date
1bfdaa9def Removed preview component 2025-07-07 22:44:05 +02:00
47129cd00c feat: Redesign trust dialog 2025-05-30 12:08:51 +02:00
29 changed files with 163 additions and 8766 deletions

View File

@ -22,12 +22,4 @@ jobs:
- name: Build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew assembleRelease --no-daemon
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: revanced-manager
path: |
app/build/outputs/apk/release/revanced-manager*.apk
app/build/outputs/apk/release/revanced-manager*.apk.asc
run: ./gradlew build --no-daemon

View File

@ -44,13 +44,6 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
fingerprint: ${{ vars.GPG_FINGERPRINT }}
- name: Setup keystore
run: |
echo "${{ secrets.KEYSTORE }}" | base64 --decode > "app/keystore.jks"
@ -69,4 +62,4 @@ jobs:
uses: actions/attest-build-provenance@v2
with:
subject-name: 'ReVanced Manager ${{ steps.release.outputs.new_release_git_tag }}'
subject-path: app/build/outputs/apk/release/revanced-manager*.apk
subject-path: build/app/outputs/apk/release/revanced-manager-*.apk

View File

@ -23,7 +23,6 @@
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ENFORCE_UPDATE_OWNERSHIP" />
<application
android:name=".ManagerApplication"

View File

@ -12,7 +12,7 @@ interface PatchBundleDao {
fun getPropsById(uid: Int): Flow<BundleProperties?>
@Query("UPDATE patch_bundles SET version = :patches WHERE uid = :uid")
suspend fun updateVersionHash(uid: Int, patches: String?)
suspend fun updateVersion(uid: Int, patches: String?)
@Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid")
suspend fun setAutoUpdate(uid: Int, value: Boolean)
@ -26,7 +26,7 @@ interface PatchBundleDao {
@Transaction
suspend fun reset() {
purgeCustomBundles()
updateVersionHash(0, null) // Reset the main source
updateVersion(0, null) // Reset the main source
}
@Query("DELETE FROM patch_bundles WHERE uid = :uid")

View File

@ -33,12 +33,12 @@ sealed class Source {
data class PatchBundleEntity(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "version") val versionHash: String? = null,
@ColumnInfo(name = "version") val version: String? = null,
@ColumnInfo(name = "source") val source: Source,
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
)
data class BundleProperties(
@ColumnInfo(name = "version") val versionHash: String? = null,
@ColumnInfo(name = "version") val version: String? = null,
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
)

View File

@ -27,10 +27,10 @@ abstract class OptionDao {
abstract suspend fun createOptionGroup(group: OptionGroup)
@Query("DELETE FROM option_groups WHERE patch_bundle = :uid")
abstract suspend fun resetOptionsForPatchBundle(uid: Int)
abstract suspend fun clearForPatchBundle(uid: Int)
@Query("DELETE FROM option_groups WHERE package_name = :packageName")
abstract suspend fun resetOptionsForPackage(packageName: String)
abstract suspend fun clearForPackage(packageName: String)
@Query("DELETE FROM option_groups")
abstract suspend fun reset()

View File

@ -5,7 +5,6 @@ import androidx.room.Insert
import androidx.room.MapColumn
import androidx.room.Query
import androidx.room.Transaction
import kotlinx.coroutines.flow.Flow
@Dao
abstract class SelectionDao {
@ -35,14 +34,11 @@ abstract class SelectionDao {
@Insert
abstract suspend fun createSelection(selection: PatchSelection)
@Query("SELECT package_name FROM patch_selections")
abstract fun getPackagesWithSelection(): Flow<List<String>>
@Query("DELETE FROM patch_selections WHERE patch_bundle = :uid")
abstract suspend fun resetForPatchBundle(uid: Int)
abstract suspend fun clearForPatchBundle(uid: Int)
@Query("DELETE FROM patch_selections WHERE package_name = :packageName")
abstract suspend fun resetForPackage(packageName: String)
abstract suspend fun clearForPackage(packageName: String)
@Query("DELETE FROM patch_selections")
abstract suspend fun reset()

View File

@ -15,7 +15,7 @@ class LocalPatchBundle(name: String, id: Int, directory: File) :
}
reload()?.also {
saveVersionHash(it.readManifestAttribute("Version"))
saveVersion(it.readManifestAttribute("Version"))
}
}
}

View File

@ -38,9 +38,6 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil
suspend fun getName() = nameFlow.first()
val versionFlow = state.map { it.patchBundleOrNull()?.readManifestAttribute("Version") }
val patchCountFlow = state.map { it.patchBundleOrNull()?.patches?.size ?: 0 }
/**
* Returns true if the bundle has been downloaded to local storage.
*/
@ -87,9 +84,9 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil
fun propsFlow() = configRepository.getProps(uid).flowOn(Dispatchers.Default)
suspend fun getProps() = propsFlow().first()!!
suspend fun currentVersionHash() = getProps().versionHash
protected suspend fun saveVersionHash(version: String?) =
configRepository.updateVersionHash(uid, version)
suspend fun currentVersion() = getProps().version
protected suspend fun saveVersion(version: String?) =
configRepository.updateVersion(uid, version)
suspend fun setName(name: String) {
configRepository.setName(uid, name)

View File

@ -25,7 +25,7 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
}
}
saveVersionHash(info.version)
saveVersion(info.version)
reload()
}
@ -35,7 +35,7 @@ sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpo
suspend fun update(): Boolean = withContext(Dispatchers.IO) {
val info = getLatestInfo()
if (hasInstalled() && info.version == currentVersionHash())
if (hasInstalled() && info.version == currentVersion())
return@withContext false
download(info)

View File

@ -25,7 +25,7 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
PatchBundleEntity(
uid = generateUid(),
name = name,
versionHash = null,
version = null,
source = source,
autoUpdate = autoUpdate
).also {
@ -34,11 +34,8 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
suspend fun delete(uid: Int) = dao.remove(uid)
/**
* Sets the version hash used for updates.
*/
suspend fun updateVersionHash(uid: Int, versionHash: String?) =
dao.updateVersionHash(uid, versionHash)
suspend fun updateVersion(uid: Int, version: String?) =
dao.updateVersion(uid, version)
suspend fun setAutoUpdate(uid: Int, value: Boolean) = dao.setAutoUpdate(uid, value)
@ -50,7 +47,7 @@ class PatchBundlePersistenceRepository(db: AppDatabase) {
val defaultSource = PatchBundleEntity(
uid = 0,
name = "",
versionHash = null,
version = null,
source = Source.API,
autoUpdate = false
)

View File

@ -76,7 +76,7 @@ class PatchOptionsRepository(db: AppDatabase) {
fun getPackagesWithSavedOptions() =
dao.getPackagesWithOptions().map(Iterable<String>::toSet).distinctUntilChanged()
suspend fun resetOptionsForPackage(packageName: String) = dao.resetOptionsForPackage(packageName)
suspend fun resetOptionsForPatchBundle(uid: Int) = dao.resetOptionsForPatchBundle(uid)
suspend fun clearOptionsForPackage(packageName: String) = dao.clearForPackage(packageName)
suspend fun clearOptionsForPatchBundle(uid: Int) = dao.clearForPatchBundle(uid)
suspend fun reset() = dao.reset()
}

View File

@ -3,8 +3,6 @@ package app.revanced.manager.domain.repository
import app.revanced.manager.data.room.AppDatabase
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
import app.revanced.manager.data.room.selection.PatchSelection
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
class PatchSelectionRepository(db: AppDatabase) {
private val dao = db.selectionDao()
@ -27,15 +25,8 @@ class PatchSelectionRepository(db: AppDatabase) {
)
})
fun getPackagesWithSavedSelection() =
dao.getPackagesWithSelection().map(Iterable<String>::toSet).distinctUntilChanged()
suspend fun resetSelectionForPackage(packageName: String) {
dao.resetForPackage(packageName)
}
suspend fun resetSelectionForPatchBundle(uid: Int) {
dao.resetForPatchBundle(uid)
suspend fun clearSelection(packageName: String) {
dao.clearForPackage(packageName)
}
suspend fun reset() = dao.reset()
@ -43,7 +34,7 @@ class PatchSelectionRepository(db: AppDatabase) {
suspend fun export(bundleUid: Int): SerializedSelection = dao.exportSelection(bundleUid)
suspend fun import(bundleUid: Int, selection: SerializedSelection) {
dao.resetForPatchBundle(bundleUid)
dao.clearForPatchBundle(bundleUid)
dao.updateSelections(selection.entries.associate { (packageName, patches) ->
getOrCreateSelection(bundleUid, packageName) to patches.toSet()
})

View File

@ -42,8 +42,9 @@ fun BundleInformationDialog(
val props by remember(bundle) {
bundle.propsFlow()
}.collectAsStateWithLifecycle(null)
val patchCount by bundle.patchCountFlow.collectAsStateWithLifecycle(0)
val version by bundle.versionFlow.collectAsStateWithLifecycle(null)
val patchCount = remember(state) {
state.patchBundleOrNull()?.patches?.size ?: 0
}
if (viewCurrentBundlePatches) {
BundlePatchesDialog(
@ -97,8 +98,8 @@ fun BundleInformationDialog(
name = bundleName,
remoteUrl = bundle.asRemoteOrNull?.endpoint,
patchCount = patchCount,
version = version,
autoUpdate = props?.autoUpdate == true,
version = props?.version,
autoUpdate = props?.autoUpdate ?: false,
onAutoUpdateChange = {
composableScope.launch {
bundle.asRemoteOrNull?.setAutoUpdate(it)

View File

@ -47,8 +47,9 @@ fun BundleItem(
var showDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) }
val state by bundle.state.collectAsStateWithLifecycle()
val version by bundle.versionFlow.collectAsStateWithLifecycle(null)
val patchCount by bundle.patchCountFlow.collectAsStateWithLifecycle(0)
val version by remember(bundle) {
bundle.propsFlow().map { props -> props?.version }
}.collectAsStateWithLifecycle(null)
val name by bundle.nameState
if (viewBundleDialogPage) {
@ -92,7 +93,7 @@ fun BundleItem(
headlineContent = { Text(name) },
supportingContent = {
if (state is PatchBundleSource.State.Loaded) {
state.patchBundleOrNull()?.patches?.size?.let { patchCount ->
Text(pluralStringResource(R.plurals.patch_count, patchCount, patchCount))
}
},

View File

@ -13,14 +13,11 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.nameState
import kotlinx.coroutines.flow.map
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -57,8 +54,6 @@ fun BundleSelector(bundles: List<PatchBundleSource>, onFinish: (PatchBundleSourc
}
bundles.forEach {
val name by it.nameState
val version by it.versionFlow.collectAsStateWithLifecycle(null)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
@ -70,7 +65,7 @@ fun BundleSelector(bundles: List<PatchBundleSource>, onFinish: (PatchBundleSourc
}
) {
Text(
"$name $version",
name,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface
)

View File

@ -1,17 +1,6 @@
package app.revanced.manager.ui.component.settings
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItemColors
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
@ -21,10 +10,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.material3.ListItem
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
fun SettingsListItem(
@ -82,48 +67,4 @@ fun SettingsListItem(
colors = colors,
tonalElevation = tonalElevation,
shadowElevation = shadowElevation
)
@Composable
fun ExpandableSettingListItem(
headlineContent: String,
supportingContent: String,
expandableContent: @Composable () -> Unit
) {
var expanded by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxWidth()
.animateContentSize()
) {
SettingsListItem(
modifier = Modifier
.clickable{ expanded = !expanded },
headlineContent = headlineContent,
supportingContent = supportingContent,
trailingContent = {
Icon(
imageVector = if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
contentDescription = null
)
}
)
AnimatedVisibility(visible = expanded) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp, start = 16.dp, end = 16.dp)
.animateContentSize(
animationSpec = tween(
durationMillis = 500,
easing = FastOutSlowInEasing
)
)
) {
expandableContent()
}
}
}
}
)

View File

@ -79,7 +79,7 @@ data class BundleInfo(
targetList.add(it)
}
BundleInfo(source.getName(), bundle.readManifestAttribute("Version"), source.uid, compatible, incompatible, universal)
BundleInfo(source.getName(), source.currentVersion(), source.uid, compatible, incompatible, universal)
}
}

View File

@ -62,10 +62,6 @@ fun PatcherScreen(
onBackClick: () -> Unit,
viewModel: PatcherViewModel
) {
fun onLeave() {
viewModel.onBack()
onBackClick()
}
val context = LocalContext.current
val exportApkLauncher =
@ -76,14 +72,7 @@ fun PatcherScreen(
var showInstallPicker by rememberSaveable { mutableStateOf(false) }
var showDismissConfirmationDialog by rememberSaveable { mutableStateOf(false) }
fun onPageBack() {
if(patcherSucceeded == null)
showDismissConfirmationDialog = true
else
onLeave()
}
BackHandler(onBack = ::onPageBack)
BackHandler(onBack = { showDismissConfirmationDialog = true })
val steps by remember {
derivedStateOf {
@ -110,7 +99,10 @@ fun PatcherScreen(
if (showDismissConfirmationDialog) {
ConfirmDialog(
onDismiss = { showDismissConfirmationDialog = false },
onConfirm = ::onLeave,
onConfirm = {
viewModel.onBack()
onBackClick()
},
title = stringResource(R.string.patcher_stop_confirm_title),
description = stringResource(R.string.patcher_stop_confirm_description),
icon = Icons.Outlined.Cancel
@ -158,7 +150,7 @@ fun PatcherScreen(
AppTopBar(
title = stringResource(R.string.patcher),
scrollBehavior = scrollBehavior,
onBackClick = ::onPageBack
onBackClick = { showDismissConfirmationDialog = true }
)
},
bottomBar = {
@ -237,4 +229,4 @@ fun PatcherScreen(
}
}
}
}
}

View File

@ -2,7 +2,9 @@ package app.revanced.manager.ui.screen.settings
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@ -11,10 +13,13 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@ -32,6 +37,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@ -57,6 +63,7 @@ fun DownloadsSettingsScreen(
onBackClick: () -> Unit,
viewModel: DownloadsViewModel = koinViewModel()
) {
val context = LocalContext.current
val pullRefreshState = rememberPullToRefreshState()
val downloadedApps by viewModel.downloadedApps.collectAsStateWithLifecycle(emptyList())
val pluginStates by viewModel.downloaderPluginStates.collectAsStateWithLifecycle()
@ -81,7 +88,7 @@ fun DownloadsSettingsScreen(
onBackClick = onBackClick,
actions = {
if (viewModel.appSelection.isNotEmpty()) {
IconButton(onClick = { showDeleteConfirmationDialog = true }) {
IconButton(onClick = { viewModel.deleteApps() }) {
Icon(Icons.Default.Delete, stringResource(R.string.delete))
}
}
@ -142,15 +149,20 @@ fun DownloadsSettingsScreen(
.digest(androidSignature.toByteArray())
hash.toHexString(format = HexFormat.UpperCase)
}
val appName = remember {
packageInfo.applicationInfo?.loadLabel(context.packageManager)
?.toString()
?: packageName
}
when (state) {
is DownloaderPluginState.Loaded -> TrustDialog(
title = R.string.downloader_plugin_revoke_trust_dialog_title,
body = stringResource(
R.string.downloader_plugin_trust_dialog_body,
packageName,
signature
),
pluginName = appName,
signature = signature,
onDismiss = ::dismiss,
onConfirm = {
viewModel.revokePluginTrust(packageName)
@ -165,19 +177,20 @@ fun DownloadsSettingsScreen(
onDismiss = ::dismiss
)
is DownloaderPluginState.Untrusted -> TrustDialog(
title = R.string.downloader_plugin_trust_dialog_title,
body = stringResource(
R.string.downloader_plugin_trust_dialog_body,
packageName,
signature
),
onDismiss = ::dismiss,
onConfirm = {
viewModel.trustPlugin(packageName)
dismiss()
}
)
is DownloaderPluginState.Untrusted ->
TrustDialog(
title = R.string.downloader_plugin_trust_dialog_title,
body = stringResource(
R.string.downloader_plugin_trust_dialog_body
),
pluginName = appName,
signature = signature,
onDismiss = ::dismiss,
onConfirm = {
viewModel.trustPlugin(packageName)
dismiss()
}
)
}
}
@ -246,6 +259,8 @@ fun DownloadsSettingsScreen(
private fun TrustDialog(
@StringRes title: Int,
body: String,
pluginName: String,
signature: String,
onDismiss: () -> Unit,
onConfirm: () -> Unit
) {
@ -262,6 +277,35 @@ private fun TrustDialog(
}
},
title = { Text(stringResource(title)) },
text = { Text(body) }
text = {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text(body)
Card {
Column(
Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
stringResource(
R.string.downloader_plugin_trust_dialog_plugin,
pluginName
),
)
OutlinedCard(
colors = CardDefaults.outlinedCardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerHighest
)
) {
Text(
stringResource(
R.string.downloader_plugin_trust_dialog_signature,
signature.chunked(2).joinToString(" ")
), modifier = Modifier.padding(12.dp)
)
}
}
}
}
}
)
}

View File

@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Key
import androidx.compose.material.icons.outlined.WarningAmber
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
@ -29,7 +28,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
@ -44,14 +42,11 @@ import androidx.lifecycle.viewModelScope
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.ConfirmDialog
import app.revanced.manager.ui.component.GroupHeader
import app.revanced.manager.ui.component.PasswordField
import app.revanced.manager.ui.component.bundle.BundleSelector
import app.revanced.manager.ui.component.settings.ExpandableSettingListItem
import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.viewmodel.ImportExportViewModel
import app.revanced.manager.ui.viewmodel.ResetDialogState
import app.revanced.manager.util.toast
import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.launch
@ -64,8 +59,6 @@ fun ImportExportSettingsScreen(
vm: ImportExportViewModel = koinViewModel()
) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
var selectorDialog by rememberSaveable { mutableStateOf<(@Composable () -> Unit)?>(null) }
val importKeystoreLauncher =
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) {
@ -77,7 +70,6 @@ fun ImportExportSettingsScreen(
}
val patchBundles by vm.patchBundles.collectAsStateWithLifecycle(initialValue = emptyList())
val packagesWithSelections by vm.packagesWithSelection.collectAsStateWithLifecycle(initialValue = emptySet())
val packagesWithOptions by vm.packagesWithOptions.collectAsStateWithLifecycle(initialValue = emptySet())
vm.selectionAction?.let { action ->
@ -115,20 +107,6 @@ fun ImportExportSettingsScreen(
)
}
vm.resetDialogState?.let {
with(vm.resetDialogState!!) {
ConfirmDialog(
onDismiss = { vm.resetDialogState = null },
onConfirm = onConfirm,
title = stringResource(titleResId),
description = dialogOptionName?.let {
stringResource(descriptionResId, it)
} ?: stringResource(descriptionResId),
icon = Icons.Outlined.WarningAmber
)
}
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold(
@ -146,7 +124,28 @@ fun ImportExportSettingsScreen(
.fillMaxSize()
.padding(paddingValues)
) {
selectorDialog?.invoke()
var showPackageSelector by rememberSaveable {
mutableStateOf(false)
}
var showBundleSelector by rememberSaveable {
mutableStateOf(false)
}
if (showPackageSelector) {
PackageSelector(packages = packagesWithOptions) { selected ->
selected?.let(vm::resetOptionsForPackage)
showPackageSelector = false
}
}
if (showBundleSelector) {
BundleSelector(bundles = patchBundles) { bundle ->
bundle?.let(vm::clearOptionsForBundle)
showBundleSelector = false
}
}
GroupHeader(stringResource(R.string.import_))
GroupItem(
@ -182,126 +181,32 @@ fun ImportExportSettingsScreen(
GroupHeader(stringResource(R.string.reset))
GroupItem(
onClick = {
vm.resetDialogState = ResetDialogState.Keystore {
vm.regenerateKeystore()
}
},
onClick = vm::regenerateKeystore,
headline = R.string.regenerate_keystore,
description = R.string.regenerate_keystore_description
)
ExpandableSettingListItem(
headlineContent = stringResource(R.string.reset_patch_selection),
supportingContent = stringResource(R.string.reset_patch_selection_description),
expandableContent = {
GroupItem(
onClick = {
vm.resetDialogState = ResetDialogState.PatchSelectionAll {
vm.resetSelection()
}
},
headline = R.string.patch_selection_reset_all,
description = R.string.patch_selection_reset_all_description
)
GroupItem(
onClick = {
selectorDialog = {
PackageSelector(packages = packagesWithSelections) { packageName ->
packageName?.also {
vm.resetDialogState =
ResetDialogState.PatchSelectionPackage(packageName) {
vm.resetSelectionForPackage(packageName)
}
}
selectorDialog = null
}
}
},
headline = R.string.patch_selection_reset_package,
description = R.string.patch_selection_reset_package_description
)
if (patchBundles.isNotEmpty()) {
GroupItem(
onClick = {
selectorDialog = {
BundleSelector(bundles = patchBundles) { bundle ->
bundle?.also {
coroutineScope.launch {
vm.resetDialogState =
ResetDialogState.PatchSelectionBundle(bundle.getName()) {
vm.resetSelectionForPatchBundle(bundle)
}
}
}
selectorDialog = null
}
}
},
headline = R.string.patch_selection_reset_bundle,
description = R.string.patch_selection_reset_bundle_description
)
}
}
GroupItem(
onClick = vm::resetSelection, // TODO: allow resetting selection for specific bundle or package name.
headline = R.string.reset_patch_selection,
description = R.string.reset_patch_selection_description
)
ExpandableSettingListItem(
headlineContent = stringResource(R.string.reset_patch_options),
supportingContent = stringResource(R.string.reset_patch_options_description),
expandableContent = {
GroupItem(
onClick = {
vm.resetDialogState = ResetDialogState.PatchOptionsAll {
vm.resetOptions()
}
}, // TODO: patch options import/export.
headline = R.string.patch_options_reset_all,
description = R.string.patch_options_reset_all_description,
)
GroupItem(
onClick = {
selectorDialog = {
PackageSelector(packages = packagesWithOptions) { packageName ->
packageName?.also {
vm.resetDialogState =
ResetDialogState.PatchOptionPackage(packageName) {
vm.resetOptionsForPackage(packageName)
}
}
selectorDialog = null
}
}
},
headline = R.string.patch_options_reset_package,
description = R.string.patch_options_reset_package_description
)
if (patchBundles.isNotEmpty()) {
GroupItem(
onClick = {
selectorDialog = {
BundleSelector(bundles = patchBundles) { bundle ->
bundle?.also {
coroutineScope.launch {
vm.resetDialogState =
ResetDialogState.PatchOptionBundle(bundle.getName()) {
vm.resetOptionsForBundle(bundle)
}
}
}
selectorDialog = null
}
}
},
headline = R.string.patch_options_reset_bundle,
description = R.string.patch_options_reset_bundle_description,
)
}
}
GroupItem(
onClick = vm::resetOptions, // TODO: patch options import/export.
headline = R.string.patch_options_reset_all,
description = R.string.patch_options_reset_all_description,
)
GroupItem(
onClick = { showPackageSelector = true },
headline = R.string.patch_options_reset_package,
description = R.string.patch_options_reset_package_description
)
if (patchBundles.size > 1) {
GroupItem(
onClick = { showBundleSelector = true },
headline = R.string.patch_options_reset_bundle,
description = R.string.patch_options_reset_bundle_description,
)
}
}
}
}

View File

@ -87,7 +87,7 @@ fun UpdatesSettingsScreen(
BooleanItem(
preference = vm.showManagerUpdateDialogOnLaunch,
headline = R.string.show_manager_update_dialog_on_launch,
description = R.string.show_manager_update_dialog_on_launch_description
description = R.string.update_checking_manager_description
)
}
}

View File

@ -4,7 +4,6 @@ import android.app.Application
import android.net.Uri
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -35,59 +34,6 @@ import java.nio.file.StandardCopyOption
import kotlin.io.path.deleteExisting
import kotlin.io.path.inputStream
sealed class ResetDialogState(
@StringRes val titleResId: Int,
@StringRes val descriptionResId: Int,
val onConfirm: () -> Unit,
val dialogOptionName: String? = null
) {
class Keystore(onConfirm: () -> Unit) : ResetDialogState(
titleResId = R.string.regenerate_keystore,
descriptionResId = R.string.regenerate_keystore_dialog_description,
onConfirm = onConfirm
)
class PatchSelectionAll(onConfirm: () -> Unit) : ResetDialogState(
titleResId = R.string.patch_selection_reset_all,
descriptionResId = R.string.patch_selection_reset_all_dialog_description,
onConfirm = onConfirm
)
class PatchSelectionPackage(dialogOptionName:String, onConfirm: () -> Unit) : ResetDialogState(
titleResId = R.string.patch_selection_reset_package,
descriptionResId = R.string.patch_selection_reset_package_dialog_description,
onConfirm = onConfirm,
dialogOptionName = dialogOptionName
)
class PatchSelectionBundle(dialogOptionName: String, onConfirm: () -> Unit) : ResetDialogState(
titleResId = R.string.patch_selection_reset_bundle,
descriptionResId = R.string.patch_selection_reset_bundle_dialog_description,
onConfirm = onConfirm,
dialogOptionName = dialogOptionName
)
class PatchOptionsAll(onConfirm: () -> Unit) : ResetDialogState(
titleResId = R.string.patch_options_reset_all,
descriptionResId = R.string.patch_options_reset_all_dialog_description,
onConfirm = onConfirm
)
class PatchOptionPackage(dialogOptionName:String, onConfirm: () -> Unit) : ResetDialogState(
titleResId = R.string.patch_options_reset_package,
descriptionResId = R.string.patch_options_reset_package_dialog_description,
onConfirm = onConfirm,
dialogOptionName = dialogOptionName
)
class PatchOptionBundle(dialogOptionName: String, onConfirm: () -> Unit) : ResetDialogState(
titleResId = R.string.patch_options_reset_bundle,
descriptionResId = R.string.patch_options_reset_bundle_dialog_description,
onConfirm = onConfirm,
dialogOptionName = dialogOptionName
)
}
@OptIn(ExperimentalSerializationApi::class)
class ImportExportViewModel(
private val app: Application,
@ -105,18 +51,15 @@ class ImportExportViewModel(
private var keystoreImportPath by mutableStateOf<Path?>(null)
val showCredentialsDialog by derivedStateOf { keystoreImportPath != null }
var resetDialogState by mutableStateOf<ResetDialogState?>(null)
val packagesWithOptions = optionsRepository.getPackagesWithSavedOptions()
val packagesWithSelection = selectionRepository.getPackagesWithSavedSelection()
fun resetOptionsForPackage(packageName: String) = viewModelScope.launch {
optionsRepository.resetOptionsForPackage(packageName)
optionsRepository.clearOptionsForPackage(packageName)
app.toast(app.getString(R.string.patch_options_reset_toast))
}
fun resetOptionsForBundle(patchBundle: PatchBundleSource) = viewModelScope.launch {
optionsRepository.resetOptionsForPatchBundle(patchBundle.uid)
fun clearOptionsForBundle(patchBundle: PatchBundleSource) = viewModelScope.launch {
optionsRepository.clearOptionsForPatchBundle(patchBundle.uid)
app.toast(app.getString(R.string.patch_options_reset_toast))
}
@ -192,16 +135,6 @@ class ImportExportViewModel(
app.toast(app.getString(R.string.reset_patch_selection_success))
}
fun resetSelectionForPackage(packageName: String) = viewModelScope.launch {
selectionRepository.resetSelectionForPackage(packageName)
app.toast(app.getString(R.string.reset_patch_selection_success))
}
fun resetSelectionForPatchBundle(patchBundle: PatchBundleSource) = viewModelScope.launch {
selectionRepository.resetSelectionForPatchBundle(patchBundle.uid)
app.toast(app.getString(R.string.reset_patch_selection_success))
}
fun executeSelectionAction(target: Uri) = viewModelScope.launch {
val source = selectedBundle!!
val action = selectionAction!!

View File

@ -131,7 +131,7 @@ class SelectedAppInfoViewModel(
viewModelScope.launch {
if (!persistConfiguration) return@launch // TODO: save options for patched apps.
options = withContext(Dispatchers.Default) {
state.value = withContext(Dispatchers.Default) {
val bundlePatches = bundleRepository.bundles.first()
.mapValues { (_, bundle) -> bundle.patches.associateBy { it.name } }
@ -143,7 +143,7 @@ class SelectedAppInfoViewModel(
}
private set
private var selectionState: SelectionState by savedStateHandle.saveable {
private var selectionState by savedStateHandle.saveable {
if (input.patches != null)
return@saveable mutableStateOf(SelectionState.Customized(input.patches))
@ -155,7 +155,7 @@ class SelectedAppInfoViewModel(
val previous = selectionRepository.getSelection(packageName)
if (previous.values.sumOf { it.size } == 0) return@launch
selectionState = SelectionState.Customized(previous)
selection.value = SelectionState.Customized(previous)
}
selection
@ -305,7 +305,7 @@ class SelectedAppInfoViewModel(
if (!persistConfiguration) return@launch
viewModelScope.launch(Dispatchers.Default) {
selection?.let { selectionRepository.updateSelection(packageName, it) }
?: selectionRepository.resetSelectionForPackage(packageName)
?: selectionRepository.clearSelection(packageName)
optionsRepository.saveOptions(packageName, filteredOptions)
}

View File

@ -112,7 +112,7 @@ class PM(
app.packageManager.getPackageInfo(packageName, PackageInfoFlags.of(flags.toLong()))
else
app.packageManager.getPackageInfo(packageName, flags)
} catch (_: NameNotFoundException) {
} catch (e: NameNotFoundException) {
null
}
@ -184,8 +184,6 @@ class PM(
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)
}

View File

@ -118,7 +118,6 @@
<string name="export_keystore_success">Exported keystore</string>
<string name="regenerate_keystore">Regenerate keystore</string>
<string name="regenerate_keystore_description">Generate a new keystore</string>
<string name="regenerate_keystore_dialog_description">You are about to regenerate your keystore the manager will use during the patching process.\n\nYou will not be able to update the previously installed apps from this source.</string>
<string name="regenerate_keystore_success">The keystore has been successfully replaced</string>
<string name="import_patch_selection">Import patch selection</string>
<string name="import_patch_selection_description">Import patch selection from a JSON file</string>
@ -130,26 +129,12 @@
<string name="export_patch_selection_success">Exported patch selection</string>
<string name="reset_patch_selection">Reset patch selection</string>
<string name="reset_patch_selection_description">Reset the stored patch selection</string>
<string name="reset_patch_options">Reset patch options</string>
<string name="reset_patch_options_description">Reset the stored patch options</string>
<string name="reset_patch_selection_success">Patch selection has been reset</string>
<string name="patch_selection_reset_all">Reset all patch selection</string>
<string name="patch_selection_reset_all_dialog_description">You are about to reset all the patch selections. You will need to manually select each patch again.</string>
<string name="patch_selection_reset_all_description">Reset all the patch selections</string>
<string name="patch_selection_reset_package">Reset patch selection for app</string>
<string name="patch_selection_reset_package_dialog_description">You are about to reset the patch selection for the app \"%s\". You will have to manually select each patch again.</string>
<string name="patch_selection_reset_package_description">Resets patch selection for a single app</string>
<string name="patch_selection_reset_bundle">Resets patch selection for bundle</string>
<string name="patch_selection_reset_bundle_dialog_description">You are about to reset the patch selection for the bundle \"%s\". You will have to manually select each patch again.</string>
<string name="patch_selection_reset_bundle_description">Resets the patch selection for all patches in a bundle</string>
<string name="patch_options_reset_package">Reset patch options for app</string>
<string name="patch_options_reset_package_dialog_description">You are about to reset the patch options for the app \"%s\". You will have to reapply each option again.</string>
<string name="patch_options_reset_package_description">Resets patch options for a single app</string>
<string name="patch_options_reset_bundle">Resets patch options for bundle</string>
<string name="patch_options_reset_bundle_dialog_description">You are about to reset the patch options for the bundle \"%s\". You will have to reapply each option again.</string>
<string name="patch_options_reset_bundle_description">Resets patch options for all patches in a bundle</string>
<string name="patch_options_reset_all">Reset patch options</string>
<string name="patch_options_reset_all_dialog_description">You are about to reset patch options. You will have to reapply each option again.</string>
<string name="patch_options_reset_all_description">Resets all patch options</string>
<string name="downloader_plugins">Plugins</string>
<string name="downloader_plugin_state_trusted">Trusted</string>
@ -157,7 +142,9 @@
<string name="downloader_plugin_state_untrusted">Untrusted</string>
<string name="downloader_plugin_trust_dialog_title">Trust plugin?</string>
<string name="downloader_plugin_revoke_trust_dialog_title">Revoke trust?</string>
<string name="downloader_plugin_trust_dialog_body">Package name: %1$s\nSignature (SHA-256): %2$s</string>
<string name="downloader_plugin_trust_dialog_body">By continuing you\'ve agreed to running external plugins.\n\nDo not allow any suspicious plugin(s) to be installed as they can run arbitrary code.</string>
<string name="downloader_plugin_trust_dialog_signature">Signature:\n\n%s</string>
<string name="downloader_plugin_trust_dialog_plugin">Plugin:\n%s</string>
<string name="downloader_plugin_delete_apps_title">Delete selected apps</string>
<string name="downloader_plugin_delete_apps_description">Are you sure you want to delete the selected apps?</string>
<string name="downloader_settings_no_apps">No downloaded apps found</string>

View File

@ -9,7 +9,7 @@ appcompat = "1.7.0"
preferences-datastore = "1.1.2"
work-runtime = "2.10.1"
compose-bom = "2025.05.00"
navigation = "2.8.6"
navigation = "2.9.0"
accompanist = "0.37.0"
placeholder = "1.1.2"
reorderable = "2.4.3"

8365
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,10 @@
{
"devDependencies": {
"@anolilab/multi-semantic-release": "^1.1.10",
"gradle-semantic-release-plugin": "^1.10.1",
"@revanced/gradle-semantic-release-plugin": "^1.10.1",
"@saithodev/semantic-release-backmerge": "^4.0.1",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/exec": "^6.0.3",
"@semantic-release/git": "^10.0.1"
"@semantic-release/git": "^10.0.1",
}
}