diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index e4a04351..f912fedb 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -23,8 +23,8 @@ jobs: java-version: '17' distribution: 'temurin' - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v3 - name: Build with Gradle env: @@ -38,7 +38,7 @@ jobs: run: mv app/build/outputs/apk/release/app-release.apk revanced-manager-${{ env.COMMIT_HASH }}.apk - name: Upload build - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: revanced-manager path: revanced-manager-${{ env.COMMIT_HASH }}.apk diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 8e273987..9574e59b 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -20,10 +20,8 @@ jobs: java-version: '17' distribution: 'temurin' - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - with: - cache-disabled: true + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v3 - name: Build with Gradle env: diff --git a/.github/workflows/update-documentation.yml b/.github/workflows/update-documentation.yml index 77097e2f..541a7aa5 100644 --- a/.github/workflows/update-documentation.yml +++ b/.github/workflows/update-documentation.yml @@ -11,7 +11,7 @@ jobs: name: Dispatch event to documentation repository if: github.ref == 'refs/heads/main' steps: - - uses: peter-evans/repository-dispatch@v2 + - uses: peter-evans/repository-dispatch@v3 with: token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }} repository: revanced/revanced-documentation diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c44dd2da..ee855884 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,9 +20,6 @@ android { targetSdk = 34 versionCode = 1 versionName = "0.0.1" - resourceConfigurations.addAll(listOf( - "en", - )) vectorDrawables.useSupportLibrary = true } @@ -88,6 +85,12 @@ android { buildFeatures.aidl = true buildFeatures.buildConfig=true + android { + androidResources { + generateLocaleConfig = true + } + } + composeOptions.kotlinCompilerExtensionVersion = "1.5.10" externalNativeBuild { cmake { diff --git a/app/src/main/java/app/revanced/manager/ManagerApplication.kt b/app/src/main/java/app/revanced/manager/ManagerApplication.kt index 8a2811bd..66ab2483 100644 --- a/app/src/main/java/app/revanced/manager/ManagerApplication.kt +++ b/app/src/main/java/app/revanced/manager/ManagerApplication.kt @@ -1,23 +1,18 @@ package app.revanced.manager import android.app.Application -import android.content.Intent import app.revanced.manager.di.* import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.repository.PatchBundleRepository -import app.revanced.manager.service.ManagerRootService -import app.revanced.manager.service.RootConnection import kotlinx.coroutines.Dispatchers import coil.Coil import coil.ImageLoader import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.internal.BuilderImpl -import com.topjohnwu.superuser.ipc.RootService import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import me.zhanghai.android.appiconloader.coil.AppIconFetcher import me.zhanghai.android.appiconloader.coil.AppIconKeyer -import org.koin.android.ext.android.get import org.koin.android.ext.android.inject import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger @@ -61,9 +56,6 @@ class ManagerApplication : Application() { val shellBuilder = BuilderImpl.create().setFlags(Shell.FLAG_MOUNT_MASTER) Shell.setDefaultBuilder(shellBuilder) - val intent = Intent(this, ManagerRootService::class.java) - RootService.bind(intent, get()) - scope.launch { prefs.preload() } diff --git a/app/src/main/java/app/revanced/manager/di/RootModule.kt b/app/src/main/java/app/revanced/manager/di/RootModule.kt index acfad58d..1e27555b 100644 --- a/app/src/main/java/app/revanced/manager/di/RootModule.kt +++ b/app/src/main/java/app/revanced/manager/di/RootModule.kt @@ -1,11 +1,9 @@ package app.revanced.manager.di import app.revanced.manager.domain.installer.RootInstaller -import app.revanced.manager.service.RootConnection import org.koin.core.module.dsl.singleOf import org.koin.dsl.module val rootModule = module { - singleOf(::RootConnection) singleOf(::RootInstaller) } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt index 9bce104c..0c69767c 100644 --- a/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt +++ b/app/src/main/java/app/revanced/manager/di/ViewModelModule.kt @@ -18,6 +18,7 @@ val viewModelModule = module { viewModelOf(::ChangelogsViewModel) viewModelOf(::ImportExportViewModel) viewModelOf(::AboutViewModel) + viewModelOf(::DeveloperOptionsViewModel) viewModelOf(::ContributorViewModel) viewModelOf(::DownloadsViewModel) viewModelOf(::InstalledAppsViewModel) diff --git a/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt b/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt index 712d6534..1ded6d43 100644 --- a/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt +++ b/app/src/main/java/app/revanced/manager/domain/bundles/PatchBundleSource.kt @@ -9,9 +9,11 @@ import app.revanced.manager.R import app.revanced.manager.domain.repository.PatchBundlePersistenceRepository import app.revanced.manager.patcher.patch.PatchBundle import app.revanced.manager.util.tag +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import org.koin.core.component.KoinComponent import org.koin.core.component.inject @@ -80,8 +82,8 @@ sealed class PatchBundleSource(initialName: String, val uid: Int, directory: Fil * Create a flow that emits the [app.revanced.manager.data.room.bundles.BundleProperties] of this [PatchBundleSource]. * The flow will emit null if the associated [PatchBundleSource] is deleted. */ - fun propsFlow() = configRepository.getProps(uid) - suspend fun getProps() = configRepository.getProps(uid).first()!! + fun propsFlow() = configRepository.getProps(uid).flowOn(Dispatchers.Default) + suspend fun getProps() = propsFlow().first()!! suspend fun currentVersion() = getProps().versionInfo protected suspend fun saveVersion(patches: String?, integrations: String?) = diff --git a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt index 0b2ee413..9ca6cd9b 100644 --- a/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt +++ b/app/src/main/java/app/revanced/manager/domain/installer/RootInstaller.kt @@ -1,49 +1,93 @@ package app.revanced.manager.domain.installer import android.app.Application -import app.revanced.manager.service.RootConnection +import android.content.ComponentName +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder +import app.revanced.manager.IRootSystemService +import app.revanced.manager.service.ManagerRootService import app.revanced.manager.util.PM import com.topjohnwu.superuser.Shell +import com.topjohnwu.superuser.ipc.RootService +import com.topjohnwu.superuser.nio.FileSystemManager +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.time.withTimeoutOrNull import kotlinx.coroutines.withContext import java.io.File +import java.time.Duration class RootInstaller( private val app: Application, - private val rootConnection: RootConnection, private val pm: PM -) { +) : ServiceConnection { + private var remoteFS = CompletableDeferred() + + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + val ipc = IRootSystemService.Stub.asInterface(service) + val binder = ipc.fileSystemService + + remoteFS.complete(FileSystemManager.getRemote(binder)) + } + + override fun onServiceDisconnected(name: ComponentName?) { + remoteFS = CompletableDeferred() + } + + private suspend fun awaitRemoteFS(): FileSystemManager { + if (remoteFS.isActive) { + withContext(Dispatchers.Main) { + val intent = Intent(app, ManagerRootService::class.java) + RootService.bind(intent, this@RootInstaller) + } + } + + return withTimeoutOrNull(Duration.ofSeconds(120L)) { + remoteFS.await() + } ?: throw RootServiceException() + } + + private suspend fun getShell() = with(CompletableDeferred()) { + Shell.getShell(::complete) + + await() + } + + suspend fun execute(vararg commands: String) = getShell().newJob().add(*commands).exec() + fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false - fun isAppInstalled(packageName: String) = - rootConnection.remoteFS?.getFile("$modulesPath/$packageName-revanced") - ?.exists() ?: throw RootServiceException() + suspend fun isAppInstalled(packageName: String) = + awaitRemoteFS().getFile("$modulesPath/$packageName-revanced").exists() - fun isAppMounted(packageName: String): Boolean { - return pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir?.let { - Shell.cmd("mount | grep \"$it\"").exec().isSuccess + suspend fun isAppMounted(packageName: String) = withContext(Dispatchers.IO) { + pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir?.let { + execute("mount | grep \"$it\"").isSuccess } ?: false } - fun mount(packageName: String) { + suspend fun mount(packageName: String) { if (isAppMounted(packageName)) return - val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir - ?: throw Exception("Failed to load application info") - val patchedAPK = "$modulesPath/$packageName-revanced/$packageName.apk" + withContext(Dispatchers.IO) { + val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir + ?: throw Exception("Failed to load application info") + val patchedAPK = "$modulesPath/$packageName-revanced/$packageName.apk" - Shell.cmd("mount -o bind \"$patchedAPK\" \"$stockAPK\"").exec() - .also { if (!it.isSuccess) throw Exception("Failed to mount APK") } + execute("mount -o bind \"$patchedAPK\" \"$stockAPK\"").assertSuccess("Failed to mount APK") + } } - fun unmount(packageName: String) { + suspend fun unmount(packageName: String) { if (!isAppMounted(packageName)) return - val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir - ?: throw Exception("Failed to load application info") + withContext(Dispatchers.IO) { + val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir + ?: throw Exception("Failed to load application info") - Shell.cmd("umount -l \"$stockAPK\"").exec() - .also { if (!it.isSuccess) throw Exception("Failed to unmount APK") } + execute("umount -l \"$stockAPK\"").assertSuccess("Failed to unmount APK") + } } suspend fun install( @@ -52,80 +96,77 @@ class RootInstaller( packageName: String, version: String, label: String - ) { - withContext(Dispatchers.IO) { - rootConnection.remoteFS?.let { remoteFS -> - val assets = app.assets - val modulePath = "$modulesPath/$packageName-revanced" + ) = withContext(Dispatchers.IO) { + val remoteFS = awaitRemoteFS() + val assets = app.assets + val modulePath = "$modulesPath/$packageName-revanced" - unmount(packageName) + unmount(packageName) - stockAPK?.let { stockApp -> - pm.getPackageInfo(packageName)?.let { packageInfo -> - if (packageInfo.versionName <= version) - Shell.cmd("pm uninstall -k --user 0 $packageName").exec() - .also { if (!it.isSuccess) throw Exception("Failed to uninstall stock app") } + stockAPK?.let { stockApp -> + pm.getPackageInfo(packageName)?.let { packageInfo -> + if (packageInfo.versionName <= version) + execute("pm uninstall -k --user 0 $packageName").assertSuccess("Failed to uninstall stock app") + } + + execute("pm install \"${stockApp.absolutePath}\"").assertSuccess("Failed to install stock app") + } + + remoteFS.getFile(modulePath).mkdir() + + listOf( + "service.sh", + "module.prop", + ).forEach { file -> + assets.open("root/$file").use { inputStream -> + remoteFS.getFile("$modulePath/$file").newOutputStream() + .use { outputStream -> + val content = String(inputStream.readBytes()) + .replace("__PKG_NAME__", packageName) + .replace("__VERSION__", version) + .replace("__LABEL__", label) + .toByteArray() + + outputStream.write(content) } + } + } - Shell.cmd("pm install \"${stockApp.absolutePath}\"").exec() - .also { if (!it.isSuccess) throw Exception("Failed to install stock app") } - } + "$modulePath/$packageName.apk".let { apkPath -> - remoteFS.getFile(modulePath).mkdir() - - listOf( - "service.sh", - "module.prop", - ).forEach { file -> - assets.open("root/$file").use { inputStream -> - remoteFS.getFile("$modulePath/$file").newOutputStream() - .use { outputStream -> - val content = String(inputStream.readBytes()) - .replace("__PKG_NAME__", packageName) - .replace("__VERSION__", version) - .replace("__LABEL__", label) - .toByteArray() - - outputStream.write(content) - } + remoteFS.getFile(patchedAPK.absolutePath) + .also { if (!it.exists()) throw Exception("File doesn't exist") } + .newInputStream().use { inputStream -> + remoteFS.getFile(apkPath).newOutputStream().use { outputStream -> + inputStream.copyTo(outputStream) } } - "$modulePath/$packageName.apk".let { apkPath -> - - remoteFS.getFile(patchedAPK.absolutePath) - .also { if (!it.exists()) throw Exception("File doesn't exist") } - .newInputStream().use { inputStream -> - remoteFS.getFile(apkPath).newOutputStream().use { outputStream -> - inputStream.copyTo(outputStream) - } - } - - Shell.cmd( - "chmod 644 $apkPath", - "chown system:system $apkPath", - "chcon u:object_r:apk_data_file:s0 $apkPath", - "chmod +x $modulePath/service.sh" - ).exec() - .let { if (!it.isSuccess) throw Exception("Failed to set file permissions") } - } - } ?: throw RootServiceException() + execute( + "chmod 644 $apkPath", + "chown system:system $apkPath", + "chcon u:object_r:apk_data_file:s0 $apkPath", + "chmod +x $modulePath/service.sh" + ).assertSuccess("Failed to set file permissions") } } - fun uninstall(packageName: String) { - rootConnection.remoteFS?.let { remoteFS -> - if (isAppMounted(packageName)) - unmount(packageName) + suspend fun uninstall(packageName: String) { + val remoteFS = awaitRemoteFS() + if (isAppMounted(packageName)) + unmount(packageName) - remoteFS.getFile("$modulesPath/$packageName-revanced").deleteRecursively() - .also { if (!it) throw Exception("Failed to delete files") } - } ?: throw RootServiceException() + remoteFS.getFile("$modulesPath/$packageName-revanced").deleteRecursively() + .also { if (!it) throw Exception("Failed to delete files") } } companion object { const val modulesPath = "/data/adb/modules" + + private fun Shell.Result.assertSuccess(errorMessage: String) { + if (!isSuccess) throw Exception(errorMessage) + } } } -class RootServiceException: Exception("Root not available") \ No newline at end of file +class RootServiceException : Exception("Root not available") \ No newline at end of file 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 dc6d713c..08b6a94d 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 @@ -15,7 +15,6 @@ class PreferencesManager( val multithreadingDexFileWriter = booleanPreference("multithreading_dex_file_writer", true) val useProcessRuntime = booleanPreference("use_process_runtime", false) val patcherProcessMemoryLimit = intPreference("process_runtime_memory_limit", 700) - val disablePatchVersionCompatCheck = booleanPreference("disable_patch_version_compatibility_check", false) val keystoreCommonName = stringPreference("keystore_cn", KeystoreManager.DEFAULT) val keystorePass = stringPreference("keystore_pass", KeystoreManager.DEFAULT) @@ -25,8 +24,8 @@ class PreferencesManager( val firstLaunch = booleanPreference("first_launch", true) val managerAutoUpdates = booleanPreference("manager_auto_updates", false) + val disablePatchVersionCompatCheck = booleanPreference("disable_patch_version_compatibility_check", false) val disableSelectionWarning = booleanPreference("disable_selection_warning", false) - val enableSelectionWarningCountdown = booleanPreference("enable_selection_warning_countdown", true) - + val disableUniversalPatchWarning = booleanPreference("disable_universal_patch_warning", false) val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true) } diff --git a/app/src/main/java/app/revanced/manager/service/RootService.kt b/app/src/main/java/app/revanced/manager/service/RootService.kt index 6fa68b6c..ed475e50 100644 --- a/app/src/main/java/app/revanced/manager/service/RootService.kt +++ b/app/src/main/java/app/revanced/manager/service/RootService.kt @@ -1,8 +1,6 @@ package app.revanced.manager.service -import android.content.ComponentName import android.content.Intent -import android.content.ServiceConnection import android.os.IBinder import app.revanced.manager.IRootSystemService import com.topjohnwu.superuser.ipc.RootService @@ -14,23 +12,5 @@ class ManagerRootService : RootService() { FileSystemManager.getService() } - override fun onBind(intent: Intent): IBinder { - return RootSystemService() - } -} - -class RootConnection : ServiceConnection { - var remoteFS: FileSystemManager? = null - private set - - override fun onServiceConnected(name: ComponentName?, service: IBinder?) { - val ipc = IRootSystemService.Stub.asInterface(service) - val binder = ipc.fileSystemService - - remoteFS = FileSystemManager.getRemote(binder) - } - - override fun onServiceDisconnected(name: ComponentName?) { - remoteFS = null - } + override fun onBind(intent: Intent): IBinder = RootSystemService() } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/Countdown.kt b/app/src/main/java/app/revanced/manager/ui/component/Countdown.kt deleted file mode 100644 index 0fb23c27..00000000 --- a/app/src/main/java/app/revanced/manager/ui/component/Countdown.kt +++ /dev/null @@ -1,26 +0,0 @@ -package app.revanced.manager.ui.component - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import kotlinx.coroutines.delay - -@Composable -fun Countdown(start: Int, content: @Composable (Int) -> Unit) { - var timer by rememberSaveable(start) { - mutableStateOf(start) - } - LaunchedEffect(timer) { - if (timer == 0) { - return@LaunchedEffect - } - - delay(1000L) - timer -= 1 - } - - content(timer) -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/DangerousActionDialogBase.kt b/app/src/main/java/app/revanced/manager/ui/component/DangerousActionDialogBase.kt deleted file mode 100644 index ca11b31d..00000000 --- a/app/src/main/java/app/revanced/manager/ui/component/DangerousActionDialogBase.kt +++ /dev/null @@ -1,91 +0,0 @@ -package app.revanced.manager.ui.component - -import androidx.annotation.StringRes -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.WarningAmber -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Checkbox -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.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.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import app.revanced.manager.R - -@Composable -fun DangerousActionDialogBase( - onCancel: () -> Unit, - confirmButton: @Composable (Boolean) -> Unit, - @StringRes title: Int, - body: String, -) { - var dismissPermanently by rememberSaveable { - mutableStateOf(false) - } - - AlertDialog( - onDismissRequest = onCancel, - confirmButton = { - confirmButton(dismissPermanently) - }, - dismissButton = { - TextButton(onClick = onCancel) { - Text(stringResource(R.string.cancel)) - } - }, - icon = { - Icon(Icons.Outlined.WarningAmber, null) - }, - title = { - Text( - text = stringResource(title), - style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center), - color = MaterialTheme.colorScheme.onSurface, - ) - }, - text = { - Column( - verticalArrangement = Arrangement.spacedBy(16.dp), - horizontalAlignment = Alignment.Start - ) { - Text( - text = body, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(0.dp), - modifier = Modifier - .fillMaxWidth() - .clickable { - dismissPermanently = !dismissPermanently - } - ) { - Checkbox( - checked = dismissPermanently, - onCheckedChange = { - dismissPermanently = it - } - ) - Text(stringResource(R.string.permanent_dismiss)) - } - } - } - ) -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/NonSuggestedVersionDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/NonSuggestedVersionDialog.kt deleted file mode 100644 index b55dd5f5..00000000 --- a/app/src/main/java/app/revanced/manager/ui/component/NonSuggestedVersionDialog.kt +++ /dev/null @@ -1,23 +0,0 @@ -package app.revanced.manager.ui.component - -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import app.revanced.manager.R - -@Composable -fun NonSuggestedVersionDialog(suggestedVersion: String, onCancel: () -> Unit, onContinue: (Boolean) -> Unit) { - DangerousActionDialogBase( - onCancel = onCancel, - confirmButton = { dismissPermanently -> - TextButton( - onClick = { onContinue(dismissPermanently) } - ) { - Text(stringResource(R.string.continue_)) - } - }, - title = R.string.non_suggested_version_warning_title, - body = stringResource(R.string.non_suggested_version_warning_description, suggestedVersion), - ) -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/SafeguardDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/SafeguardDialog.kt new file mode 100644 index 00000000..6aabe12a --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/SafeguardDialog.kt @@ -0,0 +1,51 @@ +package app.revanced.manager.ui.component + +import androidx.annotation.StringRes +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.WarningAmber +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.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import app.revanced.manager.R + +@Composable +fun SafeguardDialog( + onDismiss: () -> Unit, + @StringRes title: Int, + body: String, +) { + AlertDialog( + onDismissRequest = onDismiss, + confirmButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.ok)) + } + }, + icon = { + Icon(Icons.Outlined.WarningAmber, null) + }, + title = { + Text( + text = stringResource(title), + style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center) + ) + }, + text = { + Text(body) + } + ) +} + +@Composable +fun NonSuggestedVersionDialog(suggestedVersion: String, onDismiss: () -> Unit) { + SafeguardDialog( + onDismiss = onDismiss, + title = R.string.non_suggested_version_warning_title, + body = stringResource(R.string.non_suggested_version_warning_description, suggestedVersion), + ) +} \ No newline at end of file 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 c2e1feca..f2bb3de5 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 @@ -19,6 +19,7 @@ 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.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.revanced.manager.R @@ -115,8 +116,8 @@ fun BaseBundleDialog( if (remoteUrl != null) { BundleListItem( - headlineText = stringResource(R.string.automatically_update), - supportingText = stringResource(R.string.automatically_update_description), + headlineText = stringResource(R.string.bundle_auto_update), + supportingText = stringResource(R.string.bundle_auto_update_description), trailingContent = { Switch( checked = autoUpdate, @@ -163,8 +164,7 @@ fun BaseBundleDialog( val patchesClickable = LocalContext.current.isDebuggable && patchCount > 0 BundleListItem( headlineText = stringResource(R.string.patches), - supportingText = if (patchCount == 0) stringResource(R.string.no_patches) - else stringResource(R.string.patches_available, patchCount), + supportingText = pluralStringResource(R.plurals.bundle_patches_available, patchCount, patchCount), modifier = Modifier.clickable(enabled = patchesClickable, onClick = onPatchesClick) ) { if (patchesClickable) diff --git a/app/src/main/java/app/revanced/manager/ui/destination/SettingsDestination.kt b/app/src/main/java/app/revanced/manager/ui/destination/SettingsDestination.kt index 6f880de6..e62ab4f4 100644 --- a/app/src/main/java/app/revanced/manager/ui/destination/SettingsDestination.kt +++ b/app/src/main/java/app/revanced/manager/ui/destination/SettingsDestination.kt @@ -6,35 +6,38 @@ import kotlinx.parcelize.Parcelize sealed interface SettingsDestination : Parcelable { @Parcelize - object Settings : SettingsDestination + data object Settings : SettingsDestination @Parcelize - object General : SettingsDestination + data object General : SettingsDestination @Parcelize - object Advanced : SettingsDestination + data object Advanced : SettingsDestination @Parcelize - object Updates : SettingsDestination + data object Updates : SettingsDestination @Parcelize - object Downloads : SettingsDestination + data object Downloads : SettingsDestination @Parcelize - object ImportExport : SettingsDestination + data object ImportExport : SettingsDestination @Parcelize - object About : SettingsDestination + data object About : SettingsDestination @Parcelize data class Update(val downloadOnScreenEntry: Boolean = false) : SettingsDestination @Parcelize - object Changelogs : SettingsDestination + data object Changelogs : SettingsDestination @Parcelize - object Contributors: SettingsDestination + data object Contributors: SettingsDestination @Parcelize - object Licenses: SettingsDestination + data object Licenses: SettingsDestination + + @Parcelize + data object DeveloperOptions: SettingsDestination } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt index c8011744..6d2e1d5f 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/AppSelectorScreen.kt @@ -70,8 +70,7 @@ fun AppSelectorScreen( vm.nonSuggestedVersionDialogSubject?.let { NonSuggestedVersionDialog( suggestedVersion = suggestedVersions[it.packageName].orEmpty(), - onCancel = vm::dismissNonSuggestedVersionDialog, - onContinue = vm::continueWithNonSuggestedVersion, + onDismiss = vm::dismissNonSuggestedVersionDialog ) } 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 710dddc6..11681824 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 @@ -1,5 +1,9 @@ package app.revanced.manager.ui.screen +import android.annotation.SuppressLint +import android.content.Intent +import android.net.Uri +import android.provider.Settings import androidx.activity.compose.BackHandler import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement @@ -10,6 +14,7 @@ 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 @@ -67,6 +72,7 @@ enum class DashboardPage( BUNDLES(R.string.tab_bundles, Icons.Outlined.Source), } +@SuppressLint("BatteryLife") @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @Composable fun DashboardScreen( @@ -211,6 +217,20 @@ fun DashboardScreen( ) } } else null, + if (vm.showBatteryOptimizationsWarning) { + { + NotificationCard( + isWarning = true, + icon = Icons.Default.BatteryAlert, + text = stringResource(R.string.battery_optimization_notification), + onClick = { + androidContext.startActivity(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { + data = Uri.parse("package:${androidContext.packageName}") + }) + } + ) + } + } else null, vm.updatedManagerVersion?.let { { NotificationCard( diff --git a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt index cd2752ca..2727e290 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/InstalledAppInfoScreen.kt @@ -83,7 +83,7 @@ fun InstalledAppInfoScreen( if (viewModel.installedApp.installType == InstallType.ROOT) { Text( - text = if (viewModel.rootInstaller.isAppMounted(viewModel.installedApp.currentPackageName)) { + text = if (viewModel.isMounted) { stringResource(R.string.mounted) } else { stringResource(R.string.not_mounted) 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 12d02425..e6a32f25 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 @@ -20,6 +20,7 @@ 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 @@ -37,7 +38,6 @@ 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 @@ -48,17 +48,16 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign 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.manager.PreferencesManager 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.Countdown -import app.revanced.manager.ui.component.DangerousActionDialogBase +import app.revanced.manager.ui.component.SafeguardDialog import app.revanced.manager.ui.component.LazyColumnWithScrollbar import app.revanced.manager.ui.component.SearchView import app.revanced.manager.ui.component.patches.OptionItem @@ -70,7 +69,6 @@ import app.revanced.manager.util.Options import app.revanced.manager.util.PatchSelection import app.revanced.manager.util.isScrollingUp import kotlinx.coroutines.launch -import org.koin.compose.koinInject @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable @@ -160,10 +158,16 @@ fun PatchesSelectorScreen( ) } - vm.pendingSelectionAction?.let { - SelectionWarningDialog( - onCancel = vm::dismissSelectionWarning, - onConfirm = vm::confirmSelectionWarning + var showSelectionWarning by rememberSaveable { + mutableStateOf(false) + } + if (showSelectionWarning) { + SelectionWarningDialog(onDismiss = { showSelectionWarning = false }) + } + vm.pendingUniversalPatchAction?.let { + UniversalPatchWarningDialog( + onCancel = vm::dismissUniversalPatchWarning, + onConfirm = vm::confirmUniversalPatchWarning ) } @@ -196,9 +200,9 @@ fun PatchesSelectorScreen( ), onToggle = { if (vm.selectionWarningEnabled) { - vm.pendingSelectionAction = { - vm.togglePatch(uid, patch) - } + showSelectionWarning = true + } else if (vm.universalPatchWarningEnabled && patch.compatiblePackages == null) { + vm.pendingUniversalPatchAction = { vm.togglePatch(uid, patch) } } else { vm.togglePatch(uid, patch) } @@ -369,36 +373,43 @@ fun PatchesSelectorScreen( } @Composable -fun SelectionWarningDialog( +fun SelectionWarningDialog(onDismiss: () -> Unit) { + SafeguardDialog( + onDismiss = onDismiss, + title = R.string.warning, + body = stringResource(R.string.selection_warning_description), + ) +} + +@Composable +fun UniversalPatchWarningDialog( onCancel: () -> Unit, - onConfirm: (Boolean) -> Unit + onConfirm: () -> Unit ) { - val prefs: PreferencesManager = koinInject() - - DangerousActionDialogBase( - onCancel = onCancel, - confirmButton = { dismissPermanently -> - val enableCountdown by prefs.enableSelectionWarningCountdown.getAsState() - - Countdown(start = if (enableCountdown) 3 else 0) { timer -> - LaunchedEffect(timer) { - if (timer == 0) prefs.enableSelectionWarningCountdown.update(false) - } - - TextButton( - onClick = { onConfirm(dismissPermanently) }, - enabled = timer == 0 - ) { - val text = - if (timer == 0) stringResource(R.string.continue_) else stringResource( - R.string.selection_warning_continue_countdown, timer - ) - Text(text, color = MaterialTheme.colorScheme.error) - } + AlertDialog( + onDismissRequest = onCancel, + confirmButton = { + TextButton(onClick = onConfirm) { + Text(stringResource(R.string.continue_)) } }, - title = R.string.selection_warning_title, - body = stringResource(R.string.selection_warning_description), + dismissButton = { + TextButton(onClick = onCancel) { + Text(stringResource(R.string.cancel)) + } + }, + icon = { + Icon(Icons.Outlined.WarningAmber, null) + }, + title = { + Text( + text = stringResource(R.string.warning), + style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center) + ) + }, + text = { + Text(stringResource(R.string.universal_patch_warning_description)) + } ) } 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 08468808..f43ecb4b 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 @@ -1,32 +1,17 @@ package app.revanced.manager.ui.screen -import android.annotation.SuppressLint -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.PowerManager -import android.provider.Settings -import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.BatteryAlert import androidx.compose.material.icons.outlined.* 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.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import app.revanced.manager.R import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.ColumnWithScrollbar -import app.revanced.manager.ui.component.NotificationCard import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.destination.SettingsDestination import app.revanced.manager.ui.screen.settings.* @@ -38,7 +23,6 @@ import dev.olshevski.navigation.reimagined.* import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf -@SuppressLint("BatteryLife") @OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsScreen( @@ -54,10 +38,6 @@ fun SettingsScreen( else navController.pop() } - val context = LocalContext.current - val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager - var showBatteryButton by remember { mutableStateOf(!pm.isIgnoringBatteryOptimizations(context.packageName)) } - val settingsSections = listOf( Triple( R.string.general, @@ -122,7 +102,8 @@ fun SettingsScreen( is SettingsDestination.About -> AboutSettingsScreen( onBackClick = backClick, onContributorsClick = { navController.navigate(SettingsDestination.Contributors) }, - onLicensesClick = { navController.navigate(SettingsDestination.Licenses) } + onDeveloperOptionsClick = { navController.navigate(SettingsDestination.DeveloperOptions) }, + onLicensesClick = { navController.navigate(SettingsDestination.Licenses) }, ) is SettingsDestination.Update -> UpdateScreen( @@ -146,6 +127,8 @@ fun SettingsScreen( onBackClick = backClick, ) + is SettingsDestination.DeveloperOptions -> DeveloperOptionsScreen(onBackClick = backClick) + is SettingsDestination.Settings -> { Scaffold( topBar = { @@ -160,21 +143,6 @@ fun SettingsScreen( .padding(paddingValues) .fillMaxSize() ) { - AnimatedVisibility(visible = showBatteryButton) { - NotificationCard( - modifier = Modifier.padding(16.dp), - isWarning = true, - icon = Icons.Default.BatteryAlert, - text = stringResource(R.string.battery_optimization_notification), - onClick = { - context.startActivity(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { - data = Uri.parse("package:${context.packageName}") - }) - showBatteryButton = - !pm.isIgnoringBatteryOptimizations(context.packageName) - } - ) - } settingsSections.forEach { (titleDescIcon, destination) -> SettingsListItem( modifier = Modifier.clickable { navController.navigate(destination) }, diff --git a/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt index 388b5dee..69548d3c 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/VersionSelectorScreen.kt @@ -69,8 +69,7 @@ fun VersionSelectorScreen( if (viewModel.showNonSuggestedVersionDialog) NonSuggestedVersionDialog( suggestedVersion = viewModel.requiredVersion.orEmpty(), - onCancel = viewModel::dismissNonSuggestedVersionDialog, - onContinue = viewModel::continueWithNonSuggestedVersion, + onDismiss = viewModel::dismissNonSuggestedVersionDialog ) val lazyListState = rememberLazyListState() diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/AboutSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/AboutSettingsScreen.kt index c4425913..7ee21609 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/AboutSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/AboutSettingsScreen.kt @@ -39,7 +39,6 @@ import app.revanced.manager.ui.component.ColumnWithScrollbar import app.revanced.manager.ui.component.settings.SettingsListItem import app.revanced.manager.ui.viewmodel.AboutViewModel import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.getSocialIcon -import app.revanced.manager.util.isDebuggable import app.revanced.manager.util.openUrl import com.google.accompanist.drawablepainter.rememberDrawablePainter import org.koin.androidx.compose.koinViewModel @@ -50,6 +49,7 @@ fun AboutSettingsScreen( onBackClick: () -> Unit, onContributorsClick: () -> Unit, onLicensesClick: () -> Unit, + onDeveloperOptionsClick: () -> Unit, viewModel: AboutViewModel = koinViewModel() ) { val context = LocalContext.current @@ -116,9 +116,11 @@ fun AboutSettingsScreen( stringResource(R.string.contributors_description), third = onContributorsClick ), - Triple(stringResource(R.string.developer_options), + Triple( + stringResource(R.string.developer_options), stringResource(R.string.developer_options_description), - third = { /*TODO*/ }).takeIf { context.isDebuggable }, + third = onDeveloperOptionsClick + ), Triple( stringResource(R.string.opensource_licenses), stringResource(R.string.opensource_licenses_description), 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 efd26ccc..997d5284 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 @@ -72,6 +72,8 @@ fun AdvancedSettingsScreen( .fillMaxSize() .padding(paddingValues) ) { + GroupHeader(stringResource(R.string.manager)) + val apiUrl by vm.prefs.api.getAsState() var showApiUrlDialog by rememberSaveable { mutableStateOf(false) } @@ -111,12 +113,26 @@ fun AdvancedSettingsScreen( headline = R.string.process_runtime_memory_limit, description = R.string.process_runtime_memory_limit_description, ) + BooleanItem( + preference = vm.prefs.multithreadingDexFileWriter, + coroutineScope = vm.viewModelScope, + headline = R.string.multithreaded_dex_file_writer, + description = R.string.multithreaded_dex_file_writer_description, + ) + + GroupHeader(stringResource(R.string.safeguards)) BooleanItem( preference = vm.prefs.disablePatchVersionCompatCheck, coroutineScope = vm.viewModelScope, headline = R.string.patch_compat_check, description = R.string.patch_compat_check_description ) + BooleanItem( + preference = vm.prefs.disableUniversalPatchWarning, + coroutineScope = vm.viewModelScope, + headline = R.string.universal_patches_safeguard, + description = R.string.universal_patches_safeguard_description + ) BooleanItem( preference = vm.prefs.suggestedVersionSafeguard, coroutineScope = vm.viewModelScope, @@ -124,24 +140,10 @@ fun AdvancedSettingsScreen( description = R.string.suggested_version_safeguard_description ) BooleanItem( - preference = vm.prefs.multithreadingDexFileWriter, + preference = vm.prefs.disableSelectionWarning, coroutineScope = vm.viewModelScope, - headline = R.string.multithreaded_dex_file_writer, - description = R.string.multithreaded_dex_file_writer_description, - ) - - GroupHeader(stringResource(R.string.patch_bundles_section)) - SettingsListItem( - headlineContent = stringResource(R.string.patch_bundles_redownload), - modifier = Modifier.clickable { - vm.redownloadBundles() - } - ) - SettingsListItem( - headlineContent = stringResource(R.string.patch_bundles_reset), - modifier = Modifier.clickable { - vm.resetBundles() - } + headline = R.string.patch_selection_safeguard, + description = R.string.patch_selection_safeguard_description ) GroupHeader(stringResource(R.string.debugging)) 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 new file mode 100644 index 00000000..f8f1e0cf --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/DeveloperOptionsScreen.kt @@ -0,0 +1,44 @@ +package app.revanced.manager.ui.screen.settings + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import app.revanced.manager.R +import app.revanced.manager.ui.component.AppTopBar +import app.revanced.manager.ui.component.GroupHeader +import app.revanced.manager.ui.component.settings.SettingsListItem +import app.revanced.manager.ui.viewmodel.DeveloperOptionsViewModel +import org.koin.androidx.compose.koinViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DeveloperOptionsScreen( + onBackClick: () -> Unit, + vm: DeveloperOptionsViewModel = koinViewModel() +) { + Scaffold( + topBar = { + AppTopBar( + title = stringResource(R.string.developer_options), + onBackClick = onBackClick + ) + } + ) { paddingValues -> + Column(modifier = Modifier.padding(paddingValues)) { + GroupHeader(stringResource(R.string.patch_bundles_section)) + SettingsListItem( + headlineContent = stringResource(R.string.patch_bundles_redownload), + modifier = Modifier.clickable(onClick = vm::redownloadBundles) + ) + SettingsListItem( + headlineContent = stringResource(R.string.patch_bundles_reset), + modifier = Modifier.clickable(onClick = vm::redownloadBundles) + ) + } + } +} \ No newline at end of file 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 4fb32ea7..f41e6a66 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 @@ -112,7 +112,7 @@ private fun ThemePicker( } }, confirmButton = { - Button( + TextButton( onClick = { onConfirm(selectedTheme) onDismiss() diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/AdvancedSettingsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/AdvancedSettingsViewModel.kt index 5db843c0..cbad0045 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/AdvancedSettingsViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/AdvancedSettingsViewModel.kt @@ -6,12 +6,10 @@ import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.revanced.manager.R -import app.revanced.manager.domain.bundles.RemotePatchBundle import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.util.tag import app.revanced.manager.util.toast -import app.revanced.manager.util.uiSafe import com.github.pgreze.process.Redirect import com.github.pgreze.process.process import kotlinx.coroutines.CancellationException @@ -43,16 +41,6 @@ class AdvancedSettingsViewModel( patchBundleRepository.reloadApiBundles() } - fun redownloadBundles() = viewModelScope.launch { - uiSafe(app, R.string.source_download_fail, RemotePatchBundle.updateFailMsg) { - patchBundleRepository.redownloadRemoteBundles() - } - } - - fun resetBundles() = viewModelScope.launch { - patchBundleRepository.reset() - } - fun exportDebugLogs(target: Uri) = viewModelScope.launch { val exitCode = try { withContext(Dispatchers.IO) { 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 24a63960..a2174852 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 @@ -3,14 +3,12 @@ package app.revanced.manager.ui.viewmodel import android.app.Application import android.content.pm.PackageInfo import android.net.Uri -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.revanced.manager.R -import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.util.PM @@ -25,8 +23,7 @@ import java.nio.file.Files class AppSelectorViewModel( private val app: Application, private val pm: PM, - private val patchBundleRepository: PatchBundleRepository, - private val prefs: PreferencesManager, + private val patchBundleRepository: PatchBundleRepository ) : ViewModel() { private val inputFile = File(app.cacheDir, "input.apk").also { it.delete() @@ -46,13 +43,6 @@ class AppSelectorViewModel( nonSuggestedVersionDialogSubject = null } - fun continueWithNonSuggestedVersion(dismissPermanently: Boolean) = viewModelScope.launch { - if (dismissPermanently) prefs.suggestedVersionSafeguard.update(false) - - nonSuggestedVersionDialogSubject?.let(onStorageClick) - dismissNonSuggestedVersionDialog() - } - fun handleStorageResult(uri: Uri) = viewModelScope.launch { val selectedApp = withContext(Dispatchers.IO) { loadSelectedFile(uri) 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 c2dd6d64..9d2e1224 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 @@ -3,10 +3,12 @@ package app.revanced.manager.ui.viewmodel import android.app.Application import android.content.ContentResolver import android.net.Uri +import android.os.PowerManager import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.core.content.getSystemService import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.revanced.manager.R @@ -33,15 +35,21 @@ class DashboardViewModel( val availablePatches = patchBundleRepository.bundles.map { it.values.sumOf { bundle -> bundle.patches.size } } private val contentResolver: ContentResolver = app.contentResolver + private val powerManager = app.getSystemService()!! val sources = patchBundleRepository.sources val selectedSources = mutableStateListOf() - var updatedManagerVersion: String? by mutableStateOf(null) private set + var showBatteryOptimizationsWarning by mutableStateOf(false) + private set init { - viewModelScope.launch { checkForManagerUpdates() } + viewModelScope.launch { + checkForManagerUpdates() + showBatteryOptimizationsWarning = + !powerManager.isIgnoringBatteryOptimizations(app.packageName) + } } fun dismissUpdateDialog() { @@ -80,12 +88,14 @@ class DashboardViewModel( fun cancelSourceSelection() { selectedSources.clear() } + fun createLocalSource(patchBundle: Uri, integrations: Uri?) = viewModelScope.launch { contentResolver.openInputStream(patchBundle)!!.use { patchesStream -> - integrations?.let { contentResolver.openInputStream(it) }.use { integrationsStream -> - patchBundleRepository.createLocal(patchesStream, integrationsStream) - } + integrations?.let { contentResolver.openInputStream(it) } + .use { integrationsStream -> + patchBundleRepository.createLocal(patchesStream, integrationsStream) + } } } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/DeveloperOptionsViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/DeveloperOptionsViewModel.kt new file mode 100644 index 00000000..bc8d0527 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/DeveloperOptionsViewModel.kt @@ -0,0 +1,27 @@ +package app.revanced.manager.ui.viewmodel + +import android.app.Application +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.revanced.manager.R +import app.revanced.manager.domain.bundles.RemotePatchBundle +import app.revanced.manager.domain.manager.PreferencesManager +import app.revanced.manager.domain.repository.PatchBundleRepository +import app.revanced.manager.util.uiSafe +import kotlinx.coroutines.launch + +class DeveloperOptionsViewModel( + val prefs: PreferencesManager, + private val app: Application, + private val patchBundleRepository: PatchBundleRepository +) : ViewModel() { + fun redownloadBundles() = viewModelScope.launch { + uiSafe(app, R.string.source_download_fail, RemotePatchBundle.updateFailMsg) { + patchBundleRepository.redownloadRemoteBundles() + } + } + + fun resetBundles() = viewModelScope.launch { + patchBundleRepository.reset() + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt index aa0a8203..7e610f55 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt @@ -44,12 +44,18 @@ class InstalledAppInfoViewModel( var appInfo: PackageInfo? by mutableStateOf(null) private set var appliedPatches: PatchSelection? by mutableStateOf(null) - var isMounted by mutableStateOf(rootInstaller.isAppMounted(installedApp.currentPackageName)) + var isMounted by mutableStateOf(false) private set + init { + viewModelScope.launch { + isMounted = rootInstaller.isAppMounted(installedApp.currentPackageName) + } + } + fun launch() = pm.launch(installedApp.currentPackageName) - fun mountOrUnmount() { + fun mountOrUnmount() = viewModelScope.launch { try { if (isMounted) rootInstaller.unmount(installedApp.currentPackageName) diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt index 4f0fbcd9..1b9aab30 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/MainViewModel.kt @@ -1,5 +1,6 @@ package app.revanced.manager.ui.viewmodel +import android.app.Activity import android.app.Application import android.content.ActivityNotFoundException import android.content.Intent @@ -39,7 +40,7 @@ class MainViewModel( val launcher = componentActivity.registerForActivityResult( ActivityResultContracts.StartActivityForResult() ) { result: ActivityResult -> - if (result.resultCode == ComponentActivity.RESULT_OK) { + if (result.resultCode == Activity.RESULT_OK) { result.data?.getStringExtra("data")?.let { applyLegacySettings(it) } ?: app.toast(app.getString(R.string.legacy_import_failed)) 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 15a51a20..27049127 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 @@ -39,15 +39,21 @@ import app.revanced.manager.util.PM import app.revanced.manager.util.simpleMessage import app.revanced.manager.util.tag import app.revanced.manager.util.toast +import app.revanced.manager.util.uiSafe +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.time.withTimeout import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout import org.koin.core.component.KoinComponent import org.koin.core.component.inject import java.io.File import java.nio.file.Files +import java.time.Duration import java.util.UUID @Stable @@ -177,6 +183,7 @@ class PatcherViewModel( } } + @OptIn(DelicateCoroutinesApi::class) override fun onCleared() { super.onCleared() app.unregisterReceiver(installBroadcastReceiver) @@ -188,15 +195,16 @@ class PatcherViewModel( } is SelectedApp.Installed -> { - try { - installedApp?.let { - if (it.installType == InstallType.ROOT) { - rootInstaller.mount(packageName) + 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) + } + } } } - } catch (e: Exception) { - Log.e(tag, "Failed to mount", e) - app.toast(app.getString(R.string.failed_to_mount, e.simpleMessage())) } } 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 47fbc556..331548d6 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 @@ -47,10 +47,12 @@ class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent { private val packageName = input.app.packageName val appVersion = input.app.version - var pendingSelectionAction by mutableStateOf<(() -> Unit)?>(null) + var pendingUniversalPatchAction by mutableStateOf<(() -> Unit)?>(null) var selectionWarningEnabled by mutableStateOf(true) private set + var universalPatchWarningEnabled by mutableStateOf(true) + private set val allowIncompatiblePatches = get().disablePatchVersionCompatCheck.getBlocking() @@ -59,6 +61,8 @@ class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent { init { viewModelScope.launch { + universalPatchWarningEnabled = !prefs.disableUniversalPatchWarning.get() + if (prefs.disableSelectionWarning.get()) { selectionWarningEnabled = false return@launch @@ -131,21 +135,15 @@ class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent { customPatchSelection = selection.put(bundle, newPatches) } - fun confirmSelectionWarning(dismissPermanently: Boolean) { - selectionWarningEnabled = false + fun confirmUniversalPatchWarning() { + universalPatchWarningEnabled = false - pendingSelectionAction?.invoke() - pendingSelectionAction = null - - if (!dismissPermanently) return - - viewModelScope.launch { - prefs.disableSelectionWarning.update(true) - } + pendingUniversalPatchAction?.invoke() + pendingUniversalPatchAction = null } - fun dismissSelectionWarning() { - pendingSelectionAction = null + fun dismissUniversalPatchWarning() { + pendingUniversalPatchAction = null } fun reset() { diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt index 306397ad..d9f73264 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/VersionSelectorViewModel.kt @@ -162,12 +162,6 @@ class VersionSelectorViewModel( nonSuggestedVersionDialogSubject = null } - fun continueWithNonSuggestedVersion(dismissPermanently: Boolean) = viewModelScope.launch { - if (dismissPermanently) prefs.suggestedVersionSafeguard.update(false) - selectedVersion = nonSuggestedVersionDialogSubject - dismissNonSuggestedVersionDialog() - } - fun select(app: SelectedApp) { if (requiredVersion != null && app.version != requiredVersion) { nonSuggestedVersionDialogSubject = app diff --git a/app/src/main/res/resources.properties b/app/src/main/res/resources.properties new file mode 100644 index 00000000..467b3efe --- /dev/null +++ b/app/src/main/res/resources.properties @@ -0,0 +1 @@ +unqualifiedResLocale=en-US diff --git a/app/src/main/res/values/plurals.xml b/app/src/main/res/values/plurals.xml index d0178073..9bcfcc08 100644 --- a/app/src/main/res/values/plurals.xml +++ b/app/src/main/res/values/plurals.xml @@ -11,4 +11,8 @@ %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 64841342..304c3c68 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -71,10 +71,15 @@ Choose between light or dark theme Multi-threaded DEX file writer Use multiple cores to write DEX files. This is faster, but uses more memory + Safeguards Disable version compatibility check The check restricts patches to supported app versions Require suggested app version Enforce selection of the suggested app version + Allow changing patch selection + Do not prevent selecting or deselecting patches + Disable universal patch warning + Disables the warning that appears when you try to select universal patches Import keystore Import a custom keystore Enter keystore credentials @@ -133,6 +138,7 @@ Apply Help Back + Warning Add Close System @@ -181,8 +187,6 @@ Failed to load updated patch bundle: %s Failed to update integrations: %s No patched apps found - No patches available to view - %d Patches available, tap to view Tap on the patches to get more information about them %s selected Unsupported app @@ -191,10 +195,10 @@ Patch selection and options has been reset to recommended defaults Patch options have been reset Non suggested version - The version of the app you have selected does not match the suggested version.\nPlease use the suggested version: %s + The version of the app you have selected does not match the suggested version.\nPlease use the suggested version: %s\n\nTo continue anyway, disable \"Require suggested app version\" in the advanced settings. Stop using defaults? - You may encounter issues when not using the default patch selection and options. - Continue (%ds) + It is recommended to use the default patch selection and options. Changing them may result in unexpected issues.\n\nYou need to turn on \"Allow changing patch selection\" in the advanced settings before toggling patches. + Universal patches have a more generalized use and do not work as reliably as patches that target specific apps. You may encounter issues while using them.\n\nThis warning can be disabled in the advanced settings. Supported Universal Unsupported @@ -299,8 +303,8 @@ Source URL Successfully updated %s No update available for %s - Automatically update - Automatically update this bundle when ReVanced starts + Auto update + Automatically update this bundle when ReVanced starts Bundle type Choose the type of bundle you want About ReVanced Manager @@ -319,7 +323,7 @@ Loading changelog Failed to download changelog: %s Check out the latest changes in this update - Battery optimization must be turned off in order for ReVanced Manager to work correctly in the background. Click here to turn off. + Battery optimizations must be turned off in order for ReVanced Manager to work correctly in the background. Click here to turn off optimizations. Installing update… Downloading update… Failed to download update: %s diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index da6b412d..05e854ed 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,14 +1,14 @@ [versions] -ktx = "1.12.0" +ktx = "1.13.1" material3 = "1.2.1" -ui-tooling = "1.6.4" -viewmodel-lifecycle = "2.7.0" +ui-tooling = "1.6.8" +viewmodel-lifecycle = "2.8.3" splash-screen = "1.0.1" -compose-activity = "1.8.2" -paging = "3.2.1" -preferences-datastore = "1.0.0" +compose-activity = "1.9.0" +paging = "3.3.0" +preferences-datastore = "1.1.1" work-runtime = "2.9.0" -compose-bom = "2024.03.00" +compose-bom = "2024.06.00" accompanist = "0.34.0" placeholder = "1.1.2" reorderable = "1.5.2" @@ -23,7 +23,7 @@ reimagined-navigation = "1.5.0" ktor = "2.3.9" markdown-renderer = "0.22.0" fading-edges = "1.0.4" -android-gradle-plugin = "8.3.0" +android-gradle-plugin = "8.3.2" kotlin-gradle-plugin = "1.9.22" dev-tools-gradle-plugin = "1.9.22-1.0.17" about-libraries-gradle-plugin = "11.1.1"