diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index f912fedb..4ca4a565 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -24,7 +24,7 @@ jobs: distribution: 'temurin' - name: Set up Gradle - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v4 - name: Build with Gradle env: diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 9574e59b..c057a644 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -21,7 +21,7 @@ jobs: distribution: 'temurin' - name: Set up Gradle - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v4 - name: Build with Gradle env: diff --git a/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json b/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json index ce36924a..c339f1c4 100644 --- a/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json +++ b/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "ab113134d89f2c5e412e87775510b327", + "identityHash": "393045599eb516fc7ef99f142485e9a2", "entities": [ { "tableName": "patch_bundles", @@ -51,17 +51,7 @@ "uid" ] }, - "indices": [ - { - "name": "index_patch_bundles_name", - "unique": true, - "columnNames": [ - "name" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_patch_bundles_name` ON `${TABLE_NAME}` (`name`)" - } - ], + "indices": [], "foreignKeys": [] }, { @@ -433,7 +423,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ab113134d89f2c5e412e87775510b327')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '393045599eb516fc7ef99f142485e9a2')" ] } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt b/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt index ec01f09b..3afbe6e8 100644 --- a/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt +++ b/app/src/main/java/app/revanced/manager/data/platform/Filesystem.kt @@ -1,10 +1,11 @@ package app.revanced.manager.data.platform +import android.Manifest import android.app.Application +import android.content.Context +import android.content.pm.PackageManager import android.os.Build import android.os.Environment -import android.Manifest -import android.content.pm.PackageManager import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContracts import app.revanced.manager.util.RequestManageStorageContract @@ -16,7 +17,7 @@ class Filesystem(private val app: Application) { * A directory that gets cleared when the app restarts. * Do not store paths to this directory in a parcel. */ - val tempDir = app.cacheDir.resolve("ephemeral").apply { + val tempDir = app.getDir("ephemeral", Context.MODE_PRIVATE).apply { deleteRecursively() mkdirs() } diff --git a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt index e9869de9..d120abf5 100644 --- a/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt +++ b/app/src/main/java/app/revanced/manager/data/room/bundles/PatchBundleEntity.kt @@ -21,7 +21,7 @@ sealed class Source { } companion object { - fun from(value: String) = when(value) { + fun from(value: String) = when (value) { Local.SENTINEL -> Local API.SENTINEL -> API else -> Remote(Url(value)) @@ -34,7 +34,7 @@ data class VersionInfo( @ColumnInfo(name = "integrations_version") val integrations: String? = null, ) -@Entity(tableName = "patch_bundles", indices = [Index(value = ["name"], unique = true)]) +@Entity(tableName = "patch_bundles") data class PatchBundleEntity( @PrimaryKey val uid: Int, @ColumnInfo(name = "name") val name: String, diff --git a/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt index 87127e42..5b8bed3b 100644 --- a/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt +++ b/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt @@ -21,6 +21,7 @@ class PreferencesManager( val firstLaunch = booleanPreference("first_launch", true) val managerAutoUpdates = booleanPreference("manager_auto_updates", false) + val showManagerUpdateDialogOnLaunch = booleanPreference("show_manager_update_dialog_on_launch", true) val disablePatchVersionCompatCheck = booleanPreference("disable_patch_version_compatibility_check", false) val disableSelectionWarning = booleanPreference("disable_selection_warning", false) diff --git a/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt index e78d26a9..06f75465 100644 --- a/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt +++ b/app/src/main/java/app/revanced/manager/domain/manager/base/BasePreferencesManager.kt @@ -63,7 +63,7 @@ class EditorContext(private val prefs: MutablePreferences) { abstract class Preference( private val dataStore: DataStore, - protected val default: T + val default: T ) { internal abstract fun Preferences.read(): T internal abstract fun MutablePreferences.write(value: T) diff --git a/app/src/main/java/app/revanced/manager/patcher/aapt/Aapt.kt b/app/src/main/java/app/revanced/manager/patcher/aapt/Aapt.kt index f81ba2f4..406c9e9d 100644 --- a/app/src/main/java/app/revanced/manager/patcher/aapt/Aapt.kt +++ b/app/src/main/java/app/revanced/manager/patcher/aapt/Aapt.kt @@ -4,7 +4,7 @@ import android.content.Context import app.revanced.manager.patcher.LibraryResolver import android.os.Build.SUPPORTED_ABIS as DEVICE_ABIS object Aapt : LibraryResolver() { - private val WORKING_ABIS = setOf("arm64-v8a", "x86", "x86_64") + private val WORKING_ABIS = setOf("arm64-v8a", "x86", "x86_64", "armeabi-v7a") fun supportsDevice() = (DEVICE_ABIS intersect WORKING_ABIS).isNotEmpty() diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt index 2c836f60..729e4381 100644 --- a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt +++ b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt @@ -234,6 +234,9 @@ class PatcherWorker( Result.failure() } finally { patchedApk.delete() + if (args.input is SelectedApp.Local && args.input.temporary) { + args.input.file.delete() + } } } diff --git a/app/src/main/java/app/revanced/manager/ui/component/AlertDialogExtended.kt b/app/src/main/java/app/revanced/manager/ui/component/AlertDialogExtended.kt index cceb189f..c2089d58 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/AlertDialogExtended.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/AlertDialogExtended.kt @@ -55,7 +55,7 @@ fun AlertDialogExtended( ) { Column(modifier = Modifier.padding(vertical = 24.dp)) { Column( - modifier = Modifier.padding(horizontal = 24.dp) + modifier = Modifier.padding(horizontal = 24.dp).fillMaxWidth() ) { icon?.let { ContentStyle(color = iconContentColor) { diff --git a/app/src/main/java/app/revanced/manager/ui/component/AppIcon.kt b/app/src/main/java/app/revanced/manager/ui/component/AppIcon.kt index a308f174..0d8cd822 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/AppIcon.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/AppIcon.kt @@ -33,11 +33,9 @@ fun AppIcon( Image( image, contentDescription, - Modifier.placeholder(visible = showPlaceHolder, color = MaterialTheme.colorScheme.inverseOnSurface, shape = RoundedCornerShape(100)).then(modifier), + modifier, colorFilter = colorFilter ) - - showPlaceHolder = false } else { AsyncImage( packageInfo, diff --git a/app/src/main/java/app/revanced/manager/ui/component/ArrowButton.kt b/app/src/main/java/app/revanced/manager/ui/component/ArrowButton.kt index d97749c0..aed7e0c7 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/ArrowButton.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/ArrowButton.kt @@ -13,10 +13,16 @@ import androidx.compose.ui.res.stringResource import app.revanced.manager.R @Composable -fun ArrowButton(modifier: Modifier = Modifier, expanded: Boolean, onClick: (() -> Unit)?) { +fun ArrowButton( + modifier: Modifier = Modifier, + expanded: Boolean, + onClick: (() -> Unit)?, + rotationInitial: Float = 0f, + rotationFinal: Float = 180f +) { val description = if (expanded) R.string.collapse_content else R.string.expand_content val rotation by animateFloatAsState( - targetValue = if (expanded) 0f else 180f, + targetValue = if (expanded) rotationInitial else rotationFinal, label = "rotation" ) diff --git a/app/src/main/java/app/revanced/manager/ui/component/AvailableUpdateDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/AvailableUpdateDialog.kt new file mode 100644 index 00000000..7059ad0d --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/AvailableUpdateDialog.kt @@ -0,0 +1,81 @@ +package app.revanced.manager.ui.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Update +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import app.revanced.manager.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AvailableUpdateDialog( + onDismiss: () -> Unit, + onConfirm: () -> Unit, + setShowManagerUpdateDialogOnLaunch: (Boolean) -> Unit, + newVersion: String +) { + var dontShowAgain by rememberSaveable { mutableStateOf(false) } + val dismissDialog = { + setShowManagerUpdateDialogOnLaunch(!dontShowAgain) + onDismiss() + } + + AlertDialogExtended( + onDismissRequest = dismissDialog, + confirmButton = { + TextButton( + onClick = { + dismissDialog() + onConfirm() + } + ) { + Text(stringResource(R.string.show)) + } + }, + dismissButton = { + TextButton( + onClick = dismissDialog + ) { + Text(stringResource(R.string.dismiss)) + } + }, + icon = { + Icon(imageVector = Icons.Outlined.Update, contentDescription = null) + }, + title = { + Text(stringResource(R.string.update_available)) + }, + text = { + Column( + modifier = Modifier.padding(horizontal = 8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = stringResource(R.string.update_available_dialog_description, newVersion) + ) + ListItem( + modifier = Modifier.clickable { dontShowAgain = !dontShowAgain }, + headlineContent = { + Text(stringResource(R.string.never_show_again)) + }, + leadingContent = { + CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) { + Checkbox(checked = dontShowAgain, onCheckedChange = { dontShowAgain = it }) + } + } + ) + } + }, + textHorizontalPadding = PaddingValues(0.dp) + ) +} diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt index f2bb3de5..2b678603 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BaseBundleDialog.kt @@ -46,11 +46,6 @@ fun BaseBundleDialog( ColumnWithScrollbar( modifier = Modifier .fillMaxWidth() - .padding( - start = 8.dp, - top = 8.dp, - end = 4.dp, - ) .then(modifier) ) { if (name != null) { diff --git a/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt b/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt index 42a8119a..25254d95 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt @@ -572,7 +572,12 @@ private class ListOptionEditor(private val elementEditor: Opti ExtendedFloatingActionButton( text = { Text(stringResource(R.string.add)) }, - icon = { Icon(Icons.Outlined.Add, null) }, + icon = { + Icon( + Icons.Outlined.Add, + stringResource(R.string.add) + ) + }, expanded = lazyListState.isScrollingUp, onClick = { items.add(Item(null)) } ) diff --git a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt index 933f2f35..3c7ac522 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/DashboardScreen.kt @@ -6,44 +6,17 @@ import android.net.Uri import android.provider.Settings import androidx.activity.compose.BackHandler import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.BatteryAlert import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.outlined.Apps -import androidx.compose.material.icons.outlined.DeleteOutline -import androidx.compose.material.icons.outlined.Download -import androidx.compose.material.icons.outlined.Refresh -import androidx.compose.material.icons.outlined.Settings -import androidx.compose.material.icons.outlined.Source -import androidx.compose.material.icons.outlined.Update -import androidx.compose.material.icons.outlined.WarningAmber -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Tab -import androidx.compose.material3.TabRow -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.material3.surfaceColorAtElevation -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.material.icons.outlined.* +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext @@ -56,6 +29,7 @@ import app.revanced.manager.domain.bundles.PatchBundleSource.Extensions.isDefaul import app.revanced.manager.patcher.aapt.Aapt import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AutoUpdatesDialog +import app.revanced.manager.ui.component.AvailableUpdateDialog import app.revanced.manager.ui.component.NotificationCard import app.revanced.manager.ui.component.bundle.BundleItem import app.revanced.manager.ui.component.bundle.BundleTopBar @@ -118,6 +92,20 @@ fun DashboardScreen( ) } + var showDialog by rememberSaveable { mutableStateOf(vm.prefs.showManagerUpdateDialogOnLaunch.getBlocking()) } + val availableUpdate by remember { + derivedStateOf { vm.updatedManagerVersion.takeIf { showDialog } } + } + + availableUpdate?.let { version -> + AvailableUpdateDialog( + onDismiss = { showDialog = false }, + setShowManagerUpdateDialogOnLaunch = vm::setShowManagerUpdateDialogOnLaunch, + onConfirm = onUpdateClick, + newVersion = version + ) + } + Scaffold( topBar = { if (bundlesSelectable) { @@ -159,6 +147,23 @@ fun DashboardScreen( AppTopBar( title = stringResource(R.string.app_name), actions = { + if (!vm.updatedManagerVersion.isNullOrEmpty()) { + IconButton( + onClick = onUpdateClick, + ) { + BadgedBox( + badge = { + Badge( + // A size value above 6.dp forces the Badge icon to be closer to the center, fixing a clipping issue + modifier = Modifier.size(7.dp), + containerColor = MaterialTheme.colorScheme.primary, + ) + } + ) { + Icon(Icons.Outlined.Update, stringResource(R.string.update)) + } + } + } IconButton(onClick = onSettingsClick) { Icon(Icons.Outlined.Settings, stringResource(R.string.settings)) } @@ -236,22 +241,6 @@ fun DashboardScreen( ) } } else null, - vm.updatedManagerVersion?.let { - { - NotificationCard( - text = stringResource(R.string.update_available_dialog_description, it), - icon = Icons.Outlined.Update, - actions = { - TextButton(onClick = vm::dismissUpdateDialog) { - Text(stringResource(R.string.dismiss)) - } - TextButton(onClick = onUpdateClick) { - Text(stringResource(R.string.show)) - } - } - ) - } - }, if (showNewDownloaderPluginsNotification) { { NotificationCard( diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt index 681344bc..07ada19c 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt @@ -2,12 +2,7 @@ package app.revanced.manager.ui.screen import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items @@ -15,28 +10,8 @@ import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.HelpOutline -import androidx.compose.material.icons.outlined.FilterList -import androidx.compose.material.icons.outlined.Restore -import androidx.compose.material.icons.outlined.Save -import androidx.compose.material.icons.outlined.Search -import androidx.compose.material.icons.outlined.Settings -import androidx.compose.material.icons.outlined.WarningAmber -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Checkbox -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExtendedFloatingActionButton -import androidx.compose.material3.FilterChip -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.Scaffold -import androidx.compose.material3.ScrollableTabRow -import androidx.compose.material3.Tab -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.material.icons.outlined.* +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -57,8 +32,8 @@ import app.revanced.manager.R import app.revanced.manager.patcher.patch.Option import app.revanced.manager.patcher.patch.PatchInfo import app.revanced.manager.ui.component.AppTopBar -import app.revanced.manager.ui.component.SafeguardDialog import app.revanced.manager.ui.component.LazyColumnWithScrollbar +import app.revanced.manager.ui.component.SafeguardDialog import app.revanced.manager.ui.component.SearchView import app.revanced.manager.ui.component.patches.OptionItem import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel @@ -94,6 +69,21 @@ fun PatchesSelectorScreen( derivedStateOf { vm.selectionIsValid(bundles) } } + val availablePatchCount by remember { + derivedStateOf { + bundles.sumOf { it.patchCount } + } + } + + val defaultPatchSelectionCount by vm.defaultSelectionCount + .collectAsStateWithLifecycle(initialValue = 0) + + val selectedPatchCount by remember { + derivedStateOf { + vm.customPatchSelection?.values?.sumOf { it.size } ?: defaultPatchSelectionCount + } + } + val patchLazyListStates = remember(bundles) { List(bundles.size) { LazyListState() } } if (showBottomSheet) { @@ -142,12 +132,21 @@ fun PatchesSelectorScreen( } } + // TODO: properly handle appVersion == null if (vm.compatibleVersions.isNotEmpty() && vm.appVersion != null) - UnsupportedDialog( + UnsupportedPatchDialog( appVersion = vm.appVersion, supportedVersions = vm.compatibleVersions, onDismissRequest = vm::dismissDialogs ) + var showUnsupportedPatchesDialog by rememberSaveable { + mutableStateOf(false) + } + if (showUnsupportedPatchesDialog && vm.appVersion != null) + UnsupportedPatchesDialog( + appVersion = vm.appVersion, + onDismissRequest = { showUnsupportedPatchesDialog = false } + ) vm.optionsDialog?.let { (bundle, patch) -> OptionsDialog( @@ -165,7 +164,7 @@ fun PatchesSelectorScreen( if (showSelectionWarning) { SelectionWarningDialog(onDismiss = { showSelectionWarning = false }) } - vm.pendingUniversalPatchAction?.let { + vm.pendingUniversalPatchAction?.let { UniversalPatchWarningDialog( onCancel = vm::dismissUniversalPatchWarning, onConfirm = vm::confirmUniversalPatchWarning @@ -200,12 +199,20 @@ fun PatchesSelectorScreen( patch ), onToggle = { - if (vm.selectionWarningEnabled) { - showSelectionWarning = true - } else if (vm.universalPatchWarningEnabled && patch.compatiblePackages == null) { - vm.pendingUniversalPatchAction = { vm.togglePatch(uid, patch) } - } else { - vm.togglePatch(uid, patch) + when { + // Open unsupported dialog if the patch is not supported + !supported -> vm.openUnsupportedDialog(patch) + + // Show selection warning if enabled + vm.selectionWarningEnabled -> showSelectionWarning = true + + // Set pending universal patch action if the universal patch warning is enabled and there are no compatible packages + vm.universalPatchWarningEnabled && patch.compatiblePackages == null -> { + vm.pendingUniversalPatchAction = { vm.togglePatch(uid, patch) } + } + + // Toggle the patch otherwise + else -> vm.togglePatch(uid, patch) } }, supported = supported @@ -256,7 +263,7 @@ fun PatchesSelectorScreen( ) { ListHeader( title = stringResource(R.string.unsupported_patches), - onHelpClick = { vm.openUnsupportedDialog(bundle.unsupported) } + onHelpClick = { showUnsupportedPatchesDialog = true } ) } } @@ -266,7 +273,11 @@ fun PatchesSelectorScreen( Scaffold( topBar = { AppTopBar( - title = stringResource(R.string.select_patches), + title = stringResource( + R.string.patches_selected, + selectedPatchCount, + availablePatchCount + ), onBackClick = onBackClick, actions = { IconButton(onClick = vm::reset) { @@ -290,8 +301,14 @@ fun PatchesSelectorScreen( ExtendedFloatingActionButton( text = { Text(stringResource(R.string.save)) }, - icon = { Icon(Icons.Outlined.Save, null) }, - expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp ?: true, + icon = { + Icon( + Icons.Outlined.Save, + stringResource(R.string.save) + ) + }, + expanded = patchLazyListStates.getOrNull(pagerState.currentPage)?.isScrollingUp + ?: true, onClick = { // TODO: only allow this if all required options have been set. onSave(vm.getCustomSelection(), vm.getOptions()) @@ -363,7 +380,7 @@ fun PatchesSelectorScreen( ) { ListHeader( title = stringResource(R.string.unsupported_patches), - onHelpClick = { vm.openUnsupportedDialog(bundle.unsupported) } + onHelpClick = { showUnsupportedPatchesDialog = true } ) } } @@ -374,7 +391,7 @@ fun PatchesSelectorScreen( } @Composable -fun SelectionWarningDialog(onDismiss: () -> Unit) { +private fun SelectionWarningDialog(onDismiss: () -> Unit) { SafeguardDialog( onDismiss = onDismiss, title = R.string.warning, @@ -383,7 +400,7 @@ fun SelectionWarningDialog(onDismiss: () -> Unit) { } @Composable -fun UniversalPatchWarningDialog( +private fun UniversalPatchWarningDialog( onCancel: () -> Unit, onConfirm: () -> Unit ) { @@ -415,7 +432,7 @@ fun UniversalPatchWarningDialog( } @Composable -fun PatchItem( +private fun PatchItem( patch: PatchInfo, onOptionsDialog: () -> Unit, selected: Boolean, @@ -424,7 +441,7 @@ fun PatchItem( ) = ListItem( modifier = Modifier .let { if (!supported) it.alpha(0.5f) else it } - .clickable(enabled = supported, onClick = onToggle) + .clickable(onClick = onToggle) .fillMaxSize(), leadingContent = { Checkbox( @@ -446,7 +463,7 @@ fun PatchItem( ) @Composable -fun ListHeader( +private fun ListHeader( title: String, onHelpClick: (() -> Unit)? = null ) { @@ -473,18 +490,46 @@ fun ListHeader( } @Composable -fun UnsupportedDialog( +private fun UnsupportedPatchesDialog( appVersion: String, - supportedVersions: List, onDismissRequest: () -> Unit ) = AlertDialog( + icon = { + Icon(Icons.Outlined.WarningAmber, null) + }, onDismissRequest = onDismissRequest, confirmButton = { TextButton(onClick = onDismissRequest) { Text(stringResource(R.string.ok)) } }, - title = { Text(stringResource(R.string.unsupported_app)) }, + title = { Text(stringResource(R.string.unsupported_patches)) }, + text = { + Text( + stringResource( + R.string.unsupported_patches_dialog, + appVersion + ) + ) + } +) + +@Composable +private fun UnsupportedPatchDialog( + appVersion: String, + supportedVersions: List, + onDismissRequest: () -> Unit +) = AlertDialog( + icon = { + Icon(Icons.Outlined.WarningAmber, null) + }, + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton(onClick = onDismissRequest) { + Text(stringResource(R.string.ok)) + } + }, + title = { Text(stringResource(R.string.unsupported_patch)) }, text = { Text( stringResource( @@ -498,7 +543,7 @@ fun UnsupportedDialog( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun OptionsDialog( +private fun OptionsDialog( patch: PatchInfo, values: Map?, reset: () -> Unit, @@ -535,8 +580,14 @@ fun OptionsDialog( if (values == null || !values.contains(key)) option.default else values[key] @Suppress("UNCHECKED_CAST") - OptionItem(option = option as Option, value = value, setValue = { set(key, it) }) + OptionItem( + option = option as Option, + value = value, + setValue = { + set(key, it) + } + ) } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt index ac867335..da3e02ee 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/SelectedAppInfoScreen.kt @@ -69,11 +69,6 @@ fun SelectedAppInfoScreen( patches.values.sumOf { it.size } } } - val availablePatchCount by remember { - derivedStateOf { - bundles.sumOf { it.patchCount } - } - } val navController = rememberNavController(startDestination = SelectedAppInfoDestination.Main) @@ -111,7 +106,6 @@ fun SelectedAppInfoScreen( // navController.navigate(SelectedAppInfoDestination.VersionSelector) }, onBackClick = onBackClick, - availablePatchCount = availablePatchCount, selectedPatchCount = selectedPatchCount, packageName = packageName, version = version, @@ -145,7 +139,6 @@ private fun SelectedAppInfoScreen( onPatchSelectorClick: () -> Unit, onVersionSelectorClick: () -> Unit, onBackClick: () -> Unit, - availablePatchCount: Int, selectedPatchCount: Int, packageName: String, version: String?, @@ -161,7 +154,12 @@ private fun SelectedAppInfoScreen( floatingActionButton = { ExtendedFloatingActionButton( text = { Text(stringResource(R.string.patch)) }, - icon = { Icon(Icons.Default.AutoFixHigh, null) }, + icon = { + Icon( + Icons.Default.AutoFixHigh, + stringResource(R.string.patch) + ) + }, onClick = onPatchClick ) } @@ -173,13 +171,7 @@ private fun SelectedAppInfoScreen( ) { AppInfo(packageInfo, placeholderLabel = packageName) { Text( - version?.let { - stringResource( - R.string.selected_app_meta_version, - it, - availablePatchCount - ) - } ?: stringResource(R.string.selected_app_meta_no_version, availablePatchCount), + version ?: stringResource(R.string.selected_app_meta_any_version), color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium, ) diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt index 997d5284..04a104ff 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt @@ -5,26 +5,13 @@ import android.os.Build import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Api -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.material.icons.outlined.Restore +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -78,14 +65,18 @@ fun AdvancedSettingsScreen( var showApiUrlDialog by rememberSaveable { mutableStateOf(false) } if (showApiUrlDialog) { - APIUrlDialog(apiUrl) { - showApiUrlDialog = false - it?.let(vm::setApiUrl) - } + APIUrlDialog( + currentUrl = apiUrl, + defaultUrl = vm.prefs.api.default, + onSubmit = { + showApiUrlDialog = false + it?.let(vm::setApiUrl) + } + ) } SettingsListItem( headlineContent = stringResource(R.string.api_url), - supportingContent = apiUrl, + supportingContent = stringResource(R.string.api_url_description), modifier = Modifier.clickable { showApiUrlDialog = true } @@ -163,7 +154,7 @@ fun AdvancedSettingsScreen( } @Composable -private fun APIUrlDialog(currentUrl: String, onSubmit: (String?) -> Unit) { +private fun APIUrlDialog(currentUrl: String, defaultUrl: String, onSubmit: (String?) -> Unit) { var url by rememberSaveable(currentUrl) { mutableStateOf(currentUrl) } AlertDialog( @@ -207,9 +198,15 @@ private fun APIUrlDialog(currentUrl: String, onSubmit: (String?) -> Unit) { color = MaterialTheme.colorScheme.error ) OutlinedTextField( + modifier = Modifier.fillMaxWidth(), value = url, onValueChange = { url = it }, - label = { Text(stringResource(R.string.api_url)) } + label = { Text(stringResource(R.string.api_url)) }, + trailingIcon = { + IconButton(onClick = { url = defaultUrl }) { + Icon(Icons.Outlined.Restore, stringResource(R.string.api_url_dialog_reset)) + } + } ) } } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdatesSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdatesSettingsScreen.kt index 96aa2cd2..3772f3f4 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdatesSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/update/UpdatesSettingsScreen.kt @@ -64,6 +64,12 @@ fun UpdatesSettingsScreen( headline = R.string.update_checking_manager, description = R.string.update_checking_manager_description ) + + BooleanItem( + preference = vm.showManagerUpdateDialogOnLaunch, + headline = R.string.show_manager_update_dialog_on_launch, + description = R.string.update_checking_manager_description + ) } } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt index 3f810f63..7b91c4d3 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AppSelectorViewModel.kt @@ -29,7 +29,7 @@ class AppSelectorViewModel( private val pm: PM, private val patchBundleRepository: PatchBundleRepository ) : ViewModel() { - private val inputFile = File(app.cacheDir, "input.apk").also { + private val inputFile = File(app.filesDir, "input.apk").also { it.delete() } val appList = pm.appList diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt index 1d3e96a5..994897c0 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/DashboardViewModel.kt @@ -72,6 +72,12 @@ class DashboardViewModel( } } + fun setShowManagerUpdateDialogOnLaunch(value: Boolean) { + viewModelScope.launch { + prefs.showManagerUpdateDialogOnLaunch.update(value) + } + } + fun applyAutoUpdatePrefs(manager: Boolean, patches: Boolean) = viewModelScope.launch { prefs.firstLaunch.update(false) diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt index 83adb9b4..e01bd640 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt @@ -218,26 +218,14 @@ class PatcherViewModel( app.unregisterReceiver(installBroadcastReceiver) workManager.cancelWorkById(patcherWorkerId) - when (val selectedApp = input.selectedApp) { - is SelectedApp.Local -> { - if (selectedApp.temporary) selectedApp.file.delete() - } - - is SelectedApp.Installed -> { - GlobalScope.launch(Dispatchers.Main) { - uiSafe(app, R.string.failed_to_mount, "Failed to mount") { - installedApp?.let { - if (it.installType == InstallType.ROOT) { - withTimeout(Duration.ofMinutes(1L)) { - rootInstaller.mount(packageName) - } - } - } + if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.ROOT) { + GlobalScope.launch(Dispatchers.Main) { + uiSafe(app, R.string.failed_to_mount, "Failed to mount") { + withTimeout(Duration.ofMinutes(1L)) { + rootInstaller.mount(packageName) } } } - - else -> Unit } tempDir.deleteRecursively() diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt index 331548d6..b2986fbb 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt @@ -36,6 +36,7 @@ import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.get import kotlinx.collections.immutable.* +import kotlinx.coroutines.flow.map @Stable @OptIn(SavedStateHandleSaveableApi::class) @@ -77,7 +78,7 @@ class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent { } private var hasModifiedSelection = false - private var customPatchSelection: PersistentPatchSelection? by savedStateHandle.saveable( + var customPatchSelection: PersistentPatchSelection? by savedStateHandle.saveable( key = "selection", stateSaver = selectionSaver, ) { @@ -100,15 +101,16 @@ class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent { val compatibleVersions = mutableStateListOf() - var filter by mutableIntStateOf(SHOW_SUPPORTED or SHOW_UNIVERSAL or SHOW_UNSUPPORTED) + var filter by mutableIntStateOf(0) private set - private suspend fun generateDefaultSelection(): PersistentPatchSelection { - val bundles = bundlesFlow.first() - val generatedSelection = - bundles.toPatchSelection(allowIncompatiblePatches) { _, patch -> patch.include } + private val defaultPatchSelection = bundlesFlow.map { bundles -> + bundles.toPatchSelection(allowIncompatiblePatches) { _, patch -> patch.include } + .toPersistentPatchSelection() + } - return generatedSelection.toPersistentPatchSelection() + val defaultSelectionCount = defaultPatchSelection.map { selection -> + selection.values.sumOf { it.size } } fun selectionIsValid(bundles: List) = bundles.any { bundle -> @@ -124,7 +126,7 @@ class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent { fun togglePatch(bundle: Int, patch: PatchInfo) = viewModelScope.launch { hasModifiedSelection = true - val selection = customPatchSelection ?: generateDefaultSelection() + val selection = customPatchSelection ?: defaultPatchSelection.first() val newPatches = selection[bundle]?.let { patches -> if (patch.name in patches) patches.remove(patch.name) @@ -188,10 +190,8 @@ class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent { compatibleVersions.clear() } - fun openUnsupportedDialog(unsupportedPatches: List) { - compatibleVersions.addAll(unsupportedPatches.flatMap { patch -> - patch.compatiblePackages?.find { it.packageName == packageName }?.versions.orEmpty() - }) + fun openUnsupportedDialog(unsupportedPatch: PatchInfo) { + compatibleVersions.addAll(unsupportedPatch.compatiblePackages?.find { it.packageName == packageName }?.versions.orEmpty()) } fun toggleFlag(flag: Int) { diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt index 697e0ecb..ee6c5c87 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/SelectedAppInfoViewModel.kt @@ -1,6 +1,5 @@ package app.revanced.manager.ui.viewmodel -import android.app.Application import android.content.pm.PackageInfo import android.os.Parcelable import androidx.compose.runtime.MutableState @@ -38,7 +37,6 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent { private val optionsRepository: PatchOptionsRepository = get() private val pm: PM = get() private val savedStateHandle: SavedStateHandle = get() - private val app: Application = get() val prefs: PreferencesManager = get() private val persistConfiguration = input.patches == null @@ -82,20 +80,17 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent { private set private var selectionState by savedStateHandle.saveable { - if (input.patches != null) { + if (input.patches != null) return@saveable mutableStateOf(SelectionState.Customized(input.patches)) - } val selection: MutableState = mutableStateOf(SelectionState.Default) - // Get previous selection (if present). + // Try to get the previous selection if customization is enabled. viewModelScope.launch { + if (!prefs.disableSelectionWarning.get()) return@launch + val previous = selectionRepository.getSelection(selectedApp.packageName) - - if (previous.values.sumOf { it.size } == 0) { - return@launch - } - + if (previous.values.sumOf { it.size } == 0) return@launch selection.value = SelectionState.Customized(previous) } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdatesSettingsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdatesSettingsViewModel.kt index cd96e091..385aeddf 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdatesSettingsViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdatesSettingsViewModel.kt @@ -14,6 +14,7 @@ class UpdatesSettingsViewModel( private val reVancedAPI: ReVancedAPI, ) : ViewModel() { val managerAutoUpdates = prefs.managerAutoUpdates + val showManagerUpdateDialogOnLaunch = prefs.showManagerUpdateDialogOnLaunch suspend fun checkForUpdates(): Boolean { uiSafe(app, R.string.failed_to_check_updates, "Failed to check for updates") { diff --git a/app/src/main/jniLibs/armeabi-v7a/libaapt2.so b/app/src/main/jniLibs/armeabi-v7a/libaapt2.so new file mode 100644 index 00000000..46bdce5c Binary files /dev/null and b/app/src/main/jniLibs/armeabi-v7a/libaapt2.so differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e8af7a03..774ecd50 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,10 +12,10 @@ Dashboard Settings Select an app - Select patches + %1$d/%2$d selected - Patching on ARMv7 devices is not yet supported and will most likely fail. New downloader plugins available. Click here to configure them. + Patching on this device architecture is unsupported and will most likely fail. Import Import patch bundle @@ -34,8 +34,7 @@ Default Unnamed - %1$s • %2$d available patches - %d available patches + Any available version Start patching the application Patch selection and options @@ -166,10 +165,12 @@ Failed to export logs Exported logs API URL + The API used to download necessary files. Set custom API URL - You may have issues with features when using a custom API URL. - Only use API\'s you trust! + Set the API URL of ReVanced Manager. ReVanced Manager uses the API to download patches and updates. + ReVanced Manager connects to the API to download patches and updates. Make sure that you trust it. Set + Reset API URL Device Android version Model @@ -199,7 +200,6 @@ No patched apps found Tap on the patches to get more information about them %s selected - Unsupported app Unsupported patches Universal patches Patch selection and options has been reset to recommended defaults @@ -213,7 +213,7 @@ Universal Unsupported Patch name - Some of the patches do not support this app version (%1$s). The patches only support the following version(s): %2$s. + This patch is not compatible with the selected app version (%1$s).\n\nIt only supports the following version(s): %2$s. Continue with this version? Not all patches support this version (%s). Do you want to continue anyway? Download application? @@ -333,9 +333,9 @@ Failed to install update Check for updates Manually check for updates - Update checking + Auto check for updates Check for new versions of ReVanced Manager when the application starts - Changelog + View changelogs Loading changelog Failed to download changelog: %s Check out the latest changes in this update @@ -382,4 +382,9 @@ Add patch bundle Bundle URL Auto update + These patches are not compatible with the selected app version (%1$s).\n\nClick on the patches to see more details. + Unsupported patch + Never show again + Show update message on launch + Shows a popup notification whenever there is a new update available on launch. diff --git a/docs/0_prerequisites.md b/docs/0_prerequisites.md index a53b46fb..9d86cd54 100644 --- a/docs/0_prerequisites.md +++ b/docs/0_prerequisites.md @@ -5,9 +5,6 @@ In order to use ReVanced Manager, certain requirements must be met. ## 🤝 Requirements - An Android device running Android 8 or higher -- Any device architecture except ARMv7[^1] - -[^1]: This constraint only applies to patches, that require patching APK resources which is why some patches may or may not work on ARMv7 architecture. You can find out, which architectures your device supports here: [⚙️ Configuring ReVanced Manager](2_4_settings.md#%E2%84%B9%EF%B8%8F-about). ## ⏭️ What's next diff --git a/gradle.properties b/gradle.properties index f19c7b9b..36d069af 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,4 +21,6 @@ kotlin.code.style=official # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true -android.nonFinalResIds=false \ No newline at end of file +android.nonFinalResIds=false +org.gradle.configuration-cache=true +org.gradle.caching=true