From 747017a5f9ca13110271bbd65a9fd4e14257fc87 Mon Sep 17 00:00:00 2001 From: Pun Butrach Date: Thu, 15 Aug 2024 19:59:06 +0700 Subject: [PATCH 1/5] feat: Improve Settings order (#2060) Co-authored-by: oSumAtrIX Co-authored-by: Ax333l --- .../manager/ui/screen/SettingsScreen.kt | 2 +- .../screen/settings/AdvancedSettingsScreen.kt | 57 +++++++++++++------ .../ui/screen/settings/ContributorScreen.kt | 15 ++++- .../screen/settings/DeveloperOptionsScreen.kt | 2 +- .../screen/settings/GeneralSettingsScreen.kt | 16 +++++- .../settings/ImportExportSettingsScreen.kt | 9 ++- .../ui/screen/settings/LicensesScreen.kt | 7 ++- app/src/main/res/values/strings.xml | 24 ++++---- 8 files changed, 93 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt index f43ecb4b..0d0bd46e 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/SettingsScreen.kt @@ -66,7 +66,7 @@ fun SettingsScreen( ) to SettingsDestination.Advanced, Triple( R.string.about, - R.string.about_description, + R.string.app_name, Icons.Outlined.Info ) to SettingsDestination.About, ) 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 04a104ff..fa8cae28 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 @@ -1,10 +1,14 @@ package app.revanced.manager.ui.screen.settings import android.app.ActivityManager +import android.content.ClipData +import android.content.ClipboardManager import android.os.Build import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Api @@ -13,7 +17,9 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier +import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -28,9 +34,10 @@ import app.revanced.manager.ui.component.settings.BooleanItem import app.revanced.manager.ui.component.settings.IntegerItem import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel +import app.revanced.manager.util.toast import org.koin.androidx.compose.koinViewModel -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun AdvancedSettingsScreen( onBackClick: () -> Unit, @@ -45,6 +52,7 @@ fun AdvancedSettingsScreen( activityManager.largeMemoryClass ) } + val haptics = LocalHapticFeedback.current Scaffold( topBar = { @@ -82,15 +90,6 @@ fun AdvancedSettingsScreen( } ) - val exportDebugLogsLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { - it?.let(vm::exportDebugLogs) - } - SettingsListItem( - headlineContent = stringResource(R.string.debug_logs_export), - modifier = Modifier.clickable { exportDebugLogsLauncher.launch(vm.debugLogFileName) } - ) - GroupHeader(stringResource(R.string.patcher)) BooleanItem( preference = vm.prefs.useProcessRuntime, @@ -138,16 +137,38 @@ fun AdvancedSettingsScreen( ) GroupHeader(stringResource(R.string.debugging)) + val exportDebugLogsLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { + it?.let(vm::exportDebugLogs) + } SettingsListItem( - headlineContent = stringResource(R.string.about_device), - supportingContent = """ - **Version**: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) - **Build type**: ${BuildConfig.BUILD_TYPE} - **Model**: ${Build.MODEL} - **Android version**: ${Build.VERSION.RELEASE} (${Build.VERSION.SDK_INT}) - **Supported Archs**: ${Build.SUPPORTED_ABIS.joinToString(", ")} - **Memory limit**: $memoryLimit + headlineContent = stringResource(R.string.debug_logs_export), + modifier = Modifier.clickable { exportDebugLogsLauncher.launch(vm.debugLogFileName) } + ) + val clipboard = remember { context.getSystemService()!! } + val deviceContent = """ + Version: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) + Build type: ${BuildConfig.BUILD_TYPE} + Model: ${Build.MODEL} + Android version: ${Build.VERSION.RELEASE} (${Build.VERSION.SDK_INT}) + Supported Archs: ${Build.SUPPORTED_ABIS.joinToString(", ")} + Memory limit: $memoryLimit """.trimIndent() + SettingsListItem( + modifier = Modifier.combinedClickable( + onClick = { }, + onLongClickLabel = stringResource(R.string.copy_to_clipboard), + onLongClick = { + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + clipboard.setPrimaryClip( + ClipData.newPlainText("Device Information", deviceContent) + ) + + context.toast(context.getString(R.string.toast_copied_to_clipboard)) + } + ), + headlineContent = stringResource(R.string.about_device), + supportingContent = deviceContent ) } } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/ContributorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/ContributorScreen.kt index d4995aad..0b3a7888 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/ContributorScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/ContributorScreen.kt @@ -2,7 +2,20 @@ package app.revanced.manager.ui.screen.settings import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.border -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.items import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperOptionsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperOptionsScreen.kt index f8f1e0cf..1789329e 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperOptionsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperOptionsScreen.kt @@ -32,7 +32,7 @@ fun DeveloperOptionsScreen( Column(modifier = Modifier.padding(paddingValues)) { GroupHeader(stringResource(R.string.patch_bundles_section)) SettingsListItem( - headlineContent = stringResource(R.string.patch_bundles_redownload), + headlineContent = stringResource(R.string.patch_bundles_force_download), modifier = Modifier.clickable(onClick = vm::redownloadBundles) ) SettingsListItem( diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt index f41e6a66..03eae09f 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/GeneralSettingsScreen.kt @@ -2,8 +2,18 @@ package app.revanced.manager.ui.screen.settings import android.os.Build import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* +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.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.RadioButton +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 @@ -96,7 +106,7 @@ private fun ThemePicker( title = { Text(stringResource(R.string.theme)) }, text = { Column { - Theme.values().forEach { + Theme.entries.forEach { Row( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt index 1f15f469..84a96694 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt @@ -250,12 +250,17 @@ private fun PackageSelector(packages: Set, onFinish: (String?) -> Unit) } @Composable -private fun GroupItem(onClick: () -> Unit, @StringRes headline: Int, @StringRes description: Int) = +private fun GroupItem( + onClick: () -> Unit, + @StringRes headline: Int, + @StringRes description: Int? = null +) { SettingsListItem( modifier = Modifier.clickable { onClick() }, headlineContent = stringResource(headline), - supportingContent = stringResource(description) + supportingContent = description?.let { stringResource(it) } ) +} @Composable fun KeystoreCredentialsDialog( diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/LicensesScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/LicensesScreen.kt index 3a9bb824..76e2e964 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/LicensesScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/LicensesScreen.kt @@ -1,8 +1,11 @@ package app.revanced.manager.ui.screen.settings -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1270e091..828c726d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -6,6 +6,9 @@ CLI Manager + Copied! + Copy to clipboard + Dashboard Settings Select an app @@ -49,19 +52,18 @@ These settings can be changed later. General - General settings - Advanced - Advanced settings + Theme, dynamic color Updates - Updates for ReVanced Manager + Check for updates and view changelogs + Downloads + Downloader plugins and downloaded apps + Import & export + Keystore, patch options and selection + Advanced + API URL, memory limit, debugging + About Open source licenses View all the libraries used to make this application - Downloads - Manage downloaded content - Import & export - Import and export settings - About - About ReVanced Contributors View the contributors of ReVanced @@ -169,7 +171,7 @@ Memory limits %1$dMB (Normal) - %2$dMB (Large) Patch bundles - Redownload all patch bundles + Force download all patch bundles Reset patch bundles Patching Signing From edb4e421e26bdaadc9795066185a0edf45a5d224 Mon Sep 17 00:00:00 2001 From: Ushie Date: Sat, 17 Aug 2024 01:58:43 +0300 Subject: [PATCH 2/5] feat: Improve patch bundle screen (#2070) --- .../ui/component/bundle/BaseBundleDialog.kt | 197 +++++++++--------- .../bundle/BundleInformationDialog.kt | 16 +- app/src/main/res/values/plurals.xml | 4 - app/src/main/res/values/strings.xml | 7 +- 4 files changed, 103 insertions(+), 121 deletions(-) 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 2b678603..4450ef5c 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 @@ -2,37 +2,33 @@ package app.revanced.manager.ui.component.bundle import android.webkit.URLUtil import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.ArrowRight -import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Switch -import androidx.compose.material3.Text +import androidx.compose.material.icons.outlined.Extension +import androidx.compose.material.icons.outlined.Inventory2 +import androidx.compose.material.icons.outlined.Sell +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import app.revanced.manager.R import app.revanced.manager.ui.component.ColumnWithScrollbar import app.revanced.manager.ui.component.TextInputDialog -import app.revanced.manager.util.isDebuggable @Composable fun BaseBundleDialog( modifier: Modifier = Modifier, isDefault: Boolean, name: String?, - onNameChange: ((String) -> Unit)? = null, remoteUrl: String?, onRemoteUrlChange: ((String) -> Unit)? = null, patchCount: Int, @@ -40,39 +36,66 @@ fun BaseBundleDialog( autoUpdate: Boolean, onAutoUpdateChange: (Boolean) -> Unit, onPatchesClick: () -> Unit, - onBundleTypeClick: () -> Unit = {}, extraFields: @Composable ColumnScope.() -> Unit = {} ) { ColumnWithScrollbar( modifier = Modifier .fillMaxWidth() - .then(modifier) + .then(modifier), ) { - if (name != null) { - var showNameInputDialog by rememberSaveable { - mutableStateOf(false) - } - if (showNameInputDialog) { - TextInputDialog( - initial = name, - title = stringResource(R.string.bundle_input_name), - onDismissRequest = { - showNameInputDialog = false - }, - onConfirm = { - showNameInputDialog = false - onNameChange?.invoke(it) - }, - validator = { - it.length in 1..19 - } + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Outlined.Inventory2, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(32.dp) ) + name?.let { + Text( + text = it, + style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight(800)), + color = MaterialTheme.colorScheme.primary, + ) + } } + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(start = 2.dp) + ) { + version?.let { + Tag(Icons.Outlined.Sell, it) + } + Tag(Icons.Outlined.Extension, patchCount.toString()) + } + } + + HorizontalDivider( + modifier = Modifier.padding(horizontal = 16.dp), + color = MaterialTheme.colorScheme.outlineVariant + ) + + if (remoteUrl != null) { BundleListItem( - headlineText = stringResource(R.string.bundle_input_name), - supportingText = name.ifEmpty { stringResource(R.string.field_not_set) }, - modifier = Modifier.clickable(enabled = onNameChange != null) { - showNameInputDialog = true + headlineText = stringResource(R.string.bundle_auto_update), + supportingText = stringResource(R.string.bundle_auto_update_description), + trailingContent = { + Switch( + checked = autoUpdate, + onCheckedChange = onAutoUpdateChange + ) + }, + modifier = Modifier.clickable { + onAutoUpdateChange(!autoUpdate) } ) } @@ -99,81 +122,59 @@ fun BaseBundleDialog( } BundleListItem( - modifier = Modifier.clickable(enabled = onRemoteUrlChange != null) { - showUrlInputDialog = true - }, - headlineText = stringResource(R.string.bundle_input_source_url), - supportingText = url.ifEmpty { stringResource(R.string.field_not_set) } - ) - } - - extraFields() - - if (remoteUrl != null) { - BundleListItem( - headlineText = stringResource(R.string.bundle_auto_update), - supportingText = stringResource(R.string.bundle_auto_update_description), - trailingContent = { - Switch( - checked = autoUpdate, - onCheckedChange = onAutoUpdateChange - ) - }, - modifier = Modifier.clickable { - onAutoUpdateChange(!autoUpdate) - } - ) - } - - BundleListItem( - headlineText = stringResource(R.string.bundle_type), - supportingText = stringResource(R.string.bundle_type_description), - modifier = Modifier.clickable { - onBundleTypeClick() - } - ) { - FilledTonalButton( - onClick = onBundleTypeClick, - content = { - if (remoteUrl == null) { - Text(stringResource(R.string.local)) - } else { - Text(stringResource(R.string.remote)) + modifier = Modifier.clickable( + enabled = onRemoteUrlChange != null, + onClick = { + showUrlInputDialog = true } + ), + headlineText = stringResource(R.string.bundle_input_source_url), + supportingText = url.ifEmpty { + stringResource(R.string.field_not_set) } ) } - if (version != null || patchCount > 0) { - Text( - text = stringResource(R.string.information), - modifier = Modifier.padding( - horizontal = 16.dp, - vertical = 12.dp - ), - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.primary, - ) - } - - val patchesClickable = LocalContext.current.isDebuggable && patchCount > 0 + val patchesClickable = patchCount > 0 BundleListItem( headlineText = stringResource(R.string.patches), - supportingText = pluralStringResource(R.plurals.bundle_patches_available, patchCount, patchCount), - modifier = Modifier.clickable(enabled = patchesClickable, onClick = onPatchesClick) + supportingText = stringResource(R.string.bundle_view_patches), + modifier = Modifier.clickable( + enabled = patchesClickable, + onClick = onPatchesClick + ) ) { - if (patchesClickable) + if (patchesClickable) { Icon( Icons.AutoMirrored.Outlined.ArrowRight, stringResource(R.string.patches) ) + } } - version?.let { - BundleListItem( - headlineText = stringResource(R.string.version), - supportingText = it, - ) - } + extraFields() + } +} + +@Composable +private fun Tag( + icon: ImageVector, + text: String +) { + Row( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.outline, + ) + Text( + text, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.outline, + ) } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt index 9a9573a5..f5919ced 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt @@ -11,18 +11,9 @@ import androidx.compose.material.icons.automirrored.outlined.ArrowRight import androidx.compose.material.icons.outlined.DeleteOutline import androidx.compose.material.icons.outlined.Share import androidx.compose.material.icons.outlined.Update -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope +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,7 +69,7 @@ fun BundleInformationDialog( Scaffold( topBar = { BundleTopBar( - title = bundleName, + title = stringResource(R.string.patch_bundle_field), onBackClick = onDismissRequest, backIcon = { Icon( @@ -111,7 +102,6 @@ fun BundleInformationDialog( modifier = Modifier.padding(paddingValues), isDefault = bundle.isDefault, name = bundleName, - onNameChange = { composableScope.launch { bundle.setName(it) } }, remoteUrl = bundle.asRemoteOrNull?.endpoint, patchCount = patchCount, version = props?.versionInfo?.patches, diff --git a/app/src/main/res/values/plurals.xml b/app/src/main/res/values/plurals.xml index 9bcfcc08..d0178073 100644 --- a/app/src/main/res/values/plurals.xml +++ b/app/src/main/res/values/plurals.xml @@ -11,8 +11,4 @@ %d selected - - %d patch available - %d patches available - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 828c726d..36e2e43e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -145,7 +145,6 @@ Close System Light - Information Dark Appearance Downloaded apps @@ -181,8 +180,6 @@ Patch bundles Delete Refresh - Remote - Local Continue anyways Download another version Download app @@ -302,14 +299,12 @@ Help us improve this application Developer options Options for debugging issues - Name Source URL Successfully updated %s No update available for %s Auto update Automatically update this bundle when ReVanced starts - Bundle type - Choose the type of bundle you want + View patches About ReVanced Manager ReVanced Manager is an application designed to work with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps. The patching system is designed to automatically work with new versions of apps with minimal maintenance. An update is available From 10e7e4b39f0e05f2977f3f6ca446242670ff1c6a Mon Sep 17 00:00:00 2001 From: kitadai31 <90122968+kitadai31@users.noreply.github.com> Date: Sun, 18 Aug 2024 19:54:07 +0900 Subject: [PATCH 3/5] feat: Open the app-specific manage all files permission dialog (#2148) --- .../app/revanced/manager/util/RequestManageStorageContract.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/revanced/manager/util/RequestManageStorageContract.kt b/app/src/main/java/app/revanced/manager/util/RequestManageStorageContract.kt index 8d7b7ec3..67dce3d4 100644 --- a/app/src/main/java/app/revanced/manager/util/RequestManageStorageContract.kt +++ b/app/src/main/java/app/revanced/manager/util/RequestManageStorageContract.kt @@ -2,6 +2,7 @@ package app.revanced.manager.util import android.content.Context import android.content.Intent +import android.net.Uri import android.os.Build import android.os.Environment import android.provider.Settings @@ -10,7 +11,7 @@ import androidx.annotation.RequiresApi @RequiresApi(Build.VERSION_CODES.R) class RequestManageStorageContract(private val forceLaunch: Boolean = false) : ActivityResultContract() { - override fun createIntent(context: Context, input: String) = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION) + override fun createIntent(context: Context, input: String) = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Uri.fromParts("package", context.packageName, null)) override fun getSynchronousResult(context: Context, input: String): SynchronousResult? = if (!forceLaunch && Environment.isExternalStorageManager()) SynchronousResult(true) else null From 20554005652f050835940a6e7ff5b74848f57245 Mon Sep 17 00:00:00 2001 From: Ushie Date: Sun, 25 Aug 2024 01:23:13 +0300 Subject: [PATCH 4/5] feat: View bundle patches (#2065) --- .../component/bundle/BundlePatchesDialog.kt | 261 +++++++++++++++--- app/src/main/res/values/strings.xml | 4 + 2 files changed, 220 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt index a4fbce81..99201949 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundlePatchesDialog.kt @@ -1,33 +1,35 @@ package app.revanced.manager.ui.component.bundle import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.outlined.Lightbulb -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.ListItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R import app.revanced.manager.domain.bundles.PatchBundleSource +import app.revanced.manager.patcher.patch.PatchInfo +import app.revanced.manager.ui.component.ArrowButton import app.revanced.manager.ui.component.LazyColumnWithScrollbar -import app.revanced.manager.ui.component.NotificationCard @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -35,7 +37,8 @@ fun BundlePatchesDialog( onDismissRequest: () -> Unit, bundle: PatchBundleSource, ) { - var informationCardVisible by remember { mutableStateOf(true) } + var showAllVersions by rememberSaveable { mutableStateOf(false) } + var showOptions by rememberSaveable { mutableStateOf(false) } val state by bundle.state.collectAsStateWithLifecycle() Dialog( @@ -62,44 +65,212 @@ fun BundlePatchesDialog( LazyColumnWithScrollbar( modifier = Modifier .fillMaxWidth() - .padding(paddingValues) - .padding(16.dp) + .padding(paddingValues), + verticalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(16.dp) ) { - item { - AnimatedVisibility(visible = informationCardVisible) { - NotificationCard( - icon = Icons.Outlined.Lightbulb, - text = stringResource(R.string.tap_on_patches), - onDismiss = { informationCardVisible = false } - ) - } - } - state.patchBundleOrNull()?.let { bundle -> - items(bundle.patches.size) { bundleIndex -> - val patch = bundle.patches[bundleIndex] - ListItem( - headlineContent = { - Text( - text = patch.name, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface - ) - }, - supportingContent = { - patch.description?.let { - Text( - text = it, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } + items(bundle.patches) { patch -> + PatchItem( + patch, + showAllVersions, + onExpandVersions = { showAllVersions = !showAllVersions }, + showOptions, + onExpandOptions = { showOptions = !showOptions } ) - HorizontalDivider() } } } } } } + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun PatchItem( + patch: PatchInfo, + expandVersions: Boolean, + onExpandVersions: () -> Unit, + expandOptions: Boolean, + onExpandOptions: () -> Unit +) { + ElevatedCard( + modifier = Modifier + .fillMaxWidth() + .then( + if (patch.options.isNullOrEmpty()) Modifier else Modifier + .clip(RoundedCornerShape(8.dp)) + .clickable(onClick = onExpandOptions), + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Absolute.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = patch.name, + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + + if (!patch.options.isNullOrEmpty()) { + ArrowButton(expanded = expandOptions, onClick = null) + } + } + patch.description?.let { + Text( + text = it, + style = MaterialTheme.typography.bodyMedium + ) + } + Column( + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + if (patch.compatiblePackages.isNullOrEmpty()) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + PatchInfoChip( + text = "$PACKAGE_ICON ${stringResource(R.string.bundle_view_patches_any_package)}" + ) + PatchInfoChip( + text = "$VERSION_ICON ${stringResource(R.string.bundle_view_patches_any_version)}" + ) + } + } else { + patch.compatiblePackages.forEach { compatiblePackage -> + val packageName = compatiblePackage.packageName + val versions = compatiblePackage.versions.orEmpty().reversed() + + FlowRow( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + PatchInfoChip( + modifier = Modifier.align(Alignment.CenterVertically), + text = "$PACKAGE_ICON $packageName" + ) + + if (versions.isNotEmpty()) { + if (expandVersions) { + versions.forEach { version -> + PatchInfoChip( + modifier = Modifier.align(Alignment.CenterVertically), + text = "$VERSION_ICON $version" + ) + } + } else { + PatchInfoChip( + modifier = Modifier.align(Alignment.CenterVertically), + text = "$VERSION_ICON ${versions.first()}" + ) + } + if (versions.size > 1) { + PatchInfoChip( + onClick = onExpandVersions, + text = if (expandVersions) stringResource(R.string.less) else "+${versions.size - 1}" + ) + } + } + } + } + } + } + if (!patch.options.isNullOrEmpty()) { + AnimatedVisibility(visible = expandOptions) { + val options = patch.options + + Column { + options.forEachIndexed { i, option -> + OutlinedCard( + modifier = Modifier.fillMaxWidth(), + colors = CardColors( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onSurface, + disabledContainerColor = Color.Transparent, + disabledContentColor = MaterialTheme.colorScheme.onSurface + ), shape = when { + options.size == 1 -> RoundedCornerShape(8.dp) + i == 0 -> RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp) + i == options.lastIndex -> RoundedCornerShape( + bottomStart = 8.dp, + bottomEnd = 8.dp + ) + + else -> RoundedCornerShape(0.dp) + } + ) { + Column( + modifier = Modifier.padding(12.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = option.title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary + ) + Text( + text = option.description, + style = MaterialTheme.typography.bodyMedium + ) + } + } + } + } + } + } + } + } +} + +@Composable +fun PatchInfoChip( + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, + text: String +) { + val shape = RoundedCornerShape(8.0.dp) + val cardModifier = if (onClick != null) { + Modifier + .clip(shape) + .clickable(onClick = onClick) + } else { + Modifier + } + + OutlinedCard( + modifier = modifier.then(cardModifier), + colors = CardColors( + containerColor = Color.Transparent, + contentColor = MaterialTheme.colorScheme.onSurface, + disabledContainerColor = Color.Transparent, + disabledContentColor = MaterialTheme.colorScheme.onSurface + ), + shape = shape, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.20f)) + ) { + Row( + modifier = Modifier.padding(8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text( + text, + overflow = TextOverflow.Ellipsis, + softWrap = false, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + +const val PACKAGE_ICON = "\uD83D\uDCE6" +const val VERSION_ICON = "\uD83C\uDFAF" \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 36e2e43e..81522032 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -287,6 +287,7 @@ reorder More + Less Continue Dismiss Do not show this again @@ -305,6 +306,9 @@ Auto update Automatically update this bundle when ReVanced starts View patches + Any version + Any package + About ReVanced Manager ReVanced Manager is an application designed to work with ReVanced Patcher, which allows for long-lasting patches to be created for Android apps. The patching system is designed to automatically work with new versions of apps with minimal maintenance. An update is available From d201bdc422fcceb09040398c87edbaf2e97c7296 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 29 Aug 2024 20:28:13 +0400 Subject: [PATCH 5/5] feat: Add installer status dialog (#1473) Co-authored-by: Benjamin Halko Co-authored-by: Benjamin <73490201+BenjaminHalko@users.noreply.github.com> Co-authored-by: Ushie Co-authored-by: Ax333l --- app/build.gradle.kts | 4 + .../ui/component/InstallerStatusDialog.kt | 159 ++++++++++++++++++ .../manager/ui/screen/PatcherScreen.kt | 8 +- .../manager/ui/viewmodel/PatcherViewModel.kt | 120 +++++++++++-- .../main/java/app/revanced/manager/util/PM.kt | 5 +- app/src/main/res/values/strings.xml | 19 +++ gradle/libs.versions.toml | 5 + 7 files changed, 303 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ee855884..70281945 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -191,6 +191,10 @@ dependencies { // Scrollbars implementation(libs.scrollbars) + // EnumUtil + implementation(libs.enumutil) + ksp(libs.enumutil.ksp) + // Reorderable lists implementation(libs.reorderable) diff --git a/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt new file mode 100644 index 00000000..a31a813e --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt @@ -0,0 +1,159 @@ +package app.revanced.manager.ui.component + +import android.content.pm.PackageInstaller +import androidx.annotation.RequiresApi +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Check +import androidx.compose.material.icons.outlined.ErrorOutline +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import app.revanced.manager.R +import com.github.materiiapps.enumutil.FromValue + +private typealias InstallerStatusDialogButtonHandler = ((model: InstallerModel) -> Unit) +private typealias InstallerStatusDialogButton = @Composable (model: InstallerStatusDialogModel) -> Unit + +interface InstallerModel { + fun reinstall() + fun install() +} + +interface InstallerStatusDialogModel : InstallerModel { + var packageInstallerStatus: Int? +} + +@Composable +fun InstallerStatusDialog(model: InstallerStatusDialogModel) { + val dialogKind = remember { + DialogKind.fromValue(model.packageInstallerStatus!!) ?: DialogKind.FAILURE + } + + AlertDialog( + onDismissRequest = { + model.packageInstallerStatus = null + }, + confirmButton = { + dialogKind.confirmButton(model) + }, + dismissButton = { + dialogKind.dismissButton?.invoke(model) + }, + icon = { + Icon(dialogKind.icon, null) + }, + title = { + Text( + text = stringResource(dialogKind.title), + style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center), + color = MaterialTheme.colorScheme.onSurface, + ) + }, + text = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text(stringResource(dialogKind.contentStringResId)) + } + } + ) +} + +private fun installerStatusDialogButton( + @StringRes buttonStringResId: Int, + buttonHandler: InstallerStatusDialogButtonHandler = { }, +): InstallerStatusDialogButton = { model -> + TextButton( + onClick = { + model.packageInstallerStatus = null + buttonHandler(model) + } + ) { + Text(stringResource(buttonStringResId)) + } +} + +@FromValue("flag") +enum class DialogKind( + val flag: Int, + val title: Int, + @StringRes val contentStringResId: Int, + val icon: ImageVector = Icons.Outlined.ErrorOutline, + val confirmButton: InstallerStatusDialogButton = installerStatusDialogButton(R.string.ok), + val dismissButton: InstallerStatusDialogButton? = null, +) { + FAILURE( + flag = PackageInstaller.STATUS_FAILURE, + title = R.string.installation_failed_dialog_title, + contentStringResId = R.string.installation_failed_description, + confirmButton = installerStatusDialogButton(R.string.install_app) { model -> + model.install() + } + ), + FAILURE_ABORTED( + flag = PackageInstaller.STATUS_FAILURE_ABORTED, + title = R.string.installation_cancelled_dialog_title, + contentStringResId = R.string.installation_aborted_description, + confirmButton = installerStatusDialogButton(R.string.install_app) { model -> + model.install() + } + ), + FAILURE_BLOCKED( + flag = PackageInstaller.STATUS_FAILURE_BLOCKED, + title = R.string.installation_blocked_dialog_title, + contentStringResId = R.string.installation_blocked_description, + ), + FAILURE_CONFLICT( + flag = PackageInstaller.STATUS_FAILURE_CONFLICT, + title = R.string.installation_conflict_dialog_title, + contentStringResId = R.string.installation_conflict_description, + confirmButton = installerStatusDialogButton(R.string.reinstall) { model -> + model.reinstall() + }, + dismissButton = installerStatusDialogButton(R.string.cancel), + ), + FAILURE_INCOMPATIBLE( + flag = PackageInstaller.STATUS_FAILURE_INCOMPATIBLE, + title = R.string.installation_incompatible_dialog_title, + contentStringResId = R.string.installation_incompatible_description, + ), + FAILURE_INVALID( + flag = PackageInstaller.STATUS_FAILURE_INVALID, + title = R.string.installation_invalid_dialog_title, + contentStringResId = R.string.installation_invalid_description, + confirmButton = installerStatusDialogButton(R.string.reinstall) { model -> + model.reinstall() + }, + dismissButton = installerStatusDialogButton(R.string.cancel), + ), + FAILURE_STORAGE( + flag = PackageInstaller.STATUS_FAILURE_STORAGE, + title = R.string.installation_storage_issue_dialog_title, + contentStringResId = R.string.installation_storage_issue_description, + ), + + @RequiresApi(34) + FAILURE_TIMEOUT( + flag = PackageInstaller.STATUS_FAILURE_TIMEOUT, + title = R.string.installation_timeout_dialog_title, + contentStringResId = R.string.installation_timeout_description, + confirmButton = installerStatusDialogButton(R.string.install_app) { model -> + model.install() + }, + ); + // Needed due to the @FromValue annotation. + companion object +} diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt index 163dfbc6..096bbf03 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt @@ -40,6 +40,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import app.revanced.manager.R import app.revanced.manager.ui.component.AppScaffold import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.InstallerStatusDialog import app.revanced.manager.ui.component.patcher.InstallPickerDialog import app.revanced.manager.ui.component.patcher.Steps import app.revanced.manager.ui.model.State @@ -91,6 +92,9 @@ fun PatcherScreen( onConfirm = vm::install ) + if (vm.installerStatusDialogModel.packageInstallerStatus != null) + InstallerStatusDialog(vm.installerStatusDialogModel) + AppScaffold( topBar = { AppTopBar( @@ -103,7 +107,7 @@ fun PatcherScreen( actions = { IconButton( onClick = { exportApkLauncher.launch("${vm.packageName}.apk") }, - enabled = canInstall + enabled = patcherSucceeded == true ) { Icon(Icons.Outlined.Save, stringResource(id = R.string.save_apk)) } @@ -172,4 +176,4 @@ fun PatcherScreen( } } } -} \ No newline at end of file +} 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 e0995911..ae7f95b9 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 @@ -30,6 +30,8 @@ import app.revanced.manager.patcher.logger.LogLevel import app.revanced.manager.patcher.logger.Logger import app.revanced.manager.patcher.worker.PatcherWorker import app.revanced.manager.service.InstallService +import app.revanced.manager.service.UninstallService +import app.revanced.manager.ui.component.InstallerStatusDialogModel import app.revanced.manager.ui.destination.Destination import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.State @@ -66,6 +68,20 @@ class PatcherViewModel( private val installedAppRepository: InstalledAppRepository by inject() private val rootInstaller: RootInstaller by inject() + val installerStatusDialogModel : InstallerStatusDialogModel = object : InstallerStatusDialogModel { + override var packageInstallerStatus: Int? by mutableStateOf(null) + + override fun reinstall() { + this@PatcherViewModel.reinstall() + } + + override fun install() { + // Since this is a package installer status dialog, + // InstallType.ROOT is never used here. + install(InstallType.DEFAULT) + } + } + private var installedApp: InstalledApp? = null val packageName: String = input.selectedApp.packageName var installedPackageName by mutableStateOf(null) @@ -144,15 +160,19 @@ class PatcherViewModel( } } - private val installBroadcastReceiver = object : BroadcastReceiver() { + private val installerBroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { when (intent?.action) { InstallService.APP_INSTALL_ACTION -> { - val pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999) - val extra = intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!! + val pmStatus = intent.getIntExtra( + InstallService.EXTRA_INSTALL_STATUS, + PackageInstaller.STATUS_FAILURE + ) + + intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE) + ?.let(logger::trace) if (pmStatus == PackageInstaller.STATUS_SUCCESS) { - app.toast(app.getString(R.string.install_app_success)) installedPackageName = intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME) viewModelScope.launch { @@ -164,8 +184,24 @@ class PatcherViewModel( input.selectedPatches ) } - } else { - app.toast(app.getString(R.string.install_app_fail, extra)) + } + + installerStatusDialogModel.packageInstallerStatus = pmStatus + + isInstalling = false + } + + UninstallService.APP_UNINSTALL_ACTION -> { + val pmStatus = intent.getIntExtra( + UninstallService.EXTRA_UNINSTALL_STATUS, + PackageInstaller.STATUS_FAILURE + ) + + intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE) + ?.let(logger::trace) + + if (pmStatus != PackageInstaller.STATUS_SUCCESS) { + installerStatusDialogModel.packageInstallerStatus = pmStatus } } } @@ -173,9 +209,15 @@ class PatcherViewModel( } init { // TODO: navigate away when system-initiated process death is detected because it is not possible to recover from it. - ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply { - addAction(InstallService.APP_INSTALL_ACTION) - }, ContextCompat.RECEIVER_NOT_EXPORTED) + ContextCompat.registerReceiver( + app, + installerBroadcastReceiver, + IntentFilter().apply { + addAction(InstallService.APP_INSTALL_ACTION) + addAction(UninstallService.APP_UNINSTALL_ACTION) + }, + ContextCompat.RECEIVER_NOT_EXPORTED + ) viewModelScope.launch { installedApp = installedAppRepository.get(packageName) @@ -185,7 +227,7 @@ class PatcherViewModel( @OptIn(DelicateCoroutinesApi::class) override fun onCleared() { super.onCleared() - app.unregisterReceiver(installBroadcastReceiver) + app.unregisterReceiver(installerBroadcastReceiver) workManager.cancelWorkById(patcherWorkerId) if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.ROOT) { @@ -228,20 +270,56 @@ class PatcherViewModel( fun open() = installedPackageName?.let(pm::launch) fun install(installType: InstallType) = viewModelScope.launch { + var pmInstallStarted = false try { isInstalling = true + + val currentPackageInfo = pm.getPackageInfo(outputFile) + ?: throw Exception("Failed to load application info") + + // If the app is currently installed + val existingPackageInfo = pm.getPackageInfo(currentPackageInfo.packageName) + if (existingPackageInfo != null) { + // Check if the app version is less than the installed version + if (pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(existingPackageInfo)) { + // Exit if the selected app version is less than the installed version + installerStatusDialogModel.packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT + return@launch + } + } + when (installType) { InstallType.DEFAULT -> { + // Check if the app is mounted as root + // If it is, unmount it first, silently + if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) { + rootInstaller.unmount(packageName) + } + + // Install regularly pm.installApp(listOf(outputFile)) + pmInstallStarted = true } InstallType.ROOT -> { try { - val label = with(pm) { - getPackageInfo(outputFile)?.label() - ?: throw Exception("Failed to load application info") + // Check for base APK, first check if the app is already installed + if (existingPackageInfo == null) { + // If the app is not installed, check if the output file is a base apk + if (currentPackageInfo.splitNames != null) { + // Exit if there is no base APK package + installerStatusDialogModel.packageInstallerStatus = + PackageInstaller.STATUS_FAILURE_INVALID + return@launch + } } + // Get label + val label = with(pm) { + currentPackageInfo.label() + } + + // Install as root rootInstaller.install( outputFile, inputFile, @@ -273,8 +351,22 @@ class PatcherViewModel( } } } + } catch(e: Exception) { + Log.e(tag, "Failed to install", e) + app.toast(app.getString(R.string.install_app_fail, e.simpleMessage())) } finally { - isInstalling = false + if (!pmInstallStarted) + isInstalling = false + } + } + + fun reinstall() = viewModelScope.launch { + uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") { + pm.getPackageInfo(outputFile)?.packageName?.let { pm.uninstallPackage(it) } + ?: throw Exception("Failed to load application info") + + pm.installApp(listOf(outputFile)) + isInstalling = true } } diff --git a/app/src/main/java/app/revanced/manager/util/PM.kt b/app/src/main/java/app/revanced/manager/util/PM.kt index 21a60b97..0d7a822b 100644 --- a/app/src/main/java/app/revanced/manager/util/PM.kt +++ b/app/src/main/java/app/revanced/manager/util/PM.kt @@ -10,6 +10,7 @@ import android.content.pm.PackageInstaller import android.content.pm.PackageManager import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES import android.content.pm.PackageManager.NameNotFoundException +import androidx.core.content.pm.PackageInfoCompat import android.os.Build import android.os.Parcelable import androidx.compose.runtime.Immutable @@ -115,6 +116,8 @@ class PM( fun PackageInfo.label() = this.applicationInfo.loadLabel(app.packageManager).toString() + fun getVersionCode(packageInfo: PackageInfo) = PackageInfoCompat.getLongVersionCode(packageInfo) + suspend fun installApp(apks: List) = withContext(Dispatchers.IO) { val packageInstaller = app.packageManager.packageInstaller packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session -> @@ -170,4 +173,4 @@ class PM( Intent(this, UninstallService::class.java), intentFlags ).intentSender -} \ No newline at end of file +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 81522032..5b6c6d93 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -257,6 +257,7 @@ Install App installed Failed to install app: %s + Failed to reinstall app: %s Failed to uninstall app: %s Open Save APK @@ -360,6 +361,24 @@ Import local files from your storage, does not automatically update Import remote files from a URL, can automatically update Recommended + + Installation failed + Installation cancelled + Installation blocked + Installation conflict + Installation incompatible + Installation invalid + Not enough storage + Installation timed out + The installation failed due to an unknown reason. Try again? + The installation was cancelled manually. Try again? + The installation was blocked. Review your device security settings and try again. + The installation was prevented by an existing installation of the app. Uninstall the installed app and try again? + The app is incompatible with this device. Use the correct APK for your device and try again. + The app is invalid. Uninstall the app and try again? + The app could not be installed due to insufficient storage. Free up some space and try again. + The installation took too long. Try again? + Reinstall Show Debugging About device diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 05e854ed..1a3f425a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,6 +32,7 @@ app-icon-loader-coil = "1.5.0" skrapeit = "1.2.2" libsu = "5.2.2" scrollbars = "1.0.4" +enumutil = "1.1.0" compose-icons = "1.2.4" kotlin-process = "1.4.1" hidden-api-stub = "4.3.3" @@ -121,6 +122,10 @@ libsu-nio = { group = "com.github.topjohnwu.libsu", name = "nio", version.ref = # Scrollbars scrollbars = { group = "com.github.GIGAMOLE", name = "ComposeScrollbars", version.ref = "scrollbars" } +# EnumUtil +enumutil = { group = "io.github.materiiapps", name = "enumutil", version.ref = "enumutil" } +enumutil-ksp = { group = "io.github.materiiapps", name = "enumutil-ksp", version.ref = "enumutil" } + # Reorderable lists reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }