Merge branch 'compose-dev' into fix/minor-issues

This commit is contained in:
Ushie 2024-07-11 01:08:34 +03:00 committed by GitHub
commit d39804f7ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 456 additions and 452 deletions

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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 {

View file

@ -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<RootConnection>())
scope.launch {
prefs.preload()
}

View file

@ -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)
}

View file

@ -18,6 +18,7 @@ val viewModelModule = module {
viewModelOf(::ChangelogsViewModel)
viewModelOf(::ImportExportViewModel)
viewModelOf(::AboutViewModel)
viewModelOf(::DeveloperOptionsViewModel)
viewModelOf(::ContributorViewModel)
viewModelOf(::DownloadsViewModel)
viewModelOf(::InstalledAppsViewModel)

View file

@ -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?) =

View file

@ -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<FileSystemManager>()
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>()) {
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")
class RootServiceException : Exception("Root not available")

View file

@ -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)
}

View file

@ -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()
}

View file

@ -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)
}

View file

@ -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))
}
}
}
)
}

View file

@ -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),
)
}

View file

@ -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),
)
}

View file

@ -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)

View file

@ -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
}

View file

@ -70,8 +70,7 @@ fun AppSelectorScreen(
vm.nonSuggestedVersionDialogSubject?.let {
NonSuggestedVersionDialog(
suggestedVersion = suggestedVersions[it.packageName].orEmpty(),
onCancel = vm::dismissNonSuggestedVersionDialog,
onContinue = vm::continueWithNonSuggestedVersion,
onDismiss = vm::dismissNonSuggestedVersionDialog
)
}

View file

@ -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(

View file

@ -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)

View file

@ -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))
}
)
}

View file

@ -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) },

View file

@ -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()

View file

@ -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),

View file

@ -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))

View file

@ -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)
)
}
}
}

View file

@ -112,7 +112,7 @@ private fun ThemePicker(
}
},
confirmButton = {
Button(
TextButton(
onClick = {
onConfirm(selectedTheme)
onDismiss()

View file

@ -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) {

View file

@ -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)

View file

@ -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<PowerManager>()!!
val sources = patchBundleRepository.sources
val selectedSources = mutableStateListOf<PatchBundleSource>()
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)
}
}
}

View file

@ -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()
}
}

View file

@ -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)

View file

@ -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))

View file

@ -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()))
}
}

View file

@ -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<PreferencesManager>().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() {

View file

@ -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

View file

@ -0,0 +1 @@
unqualifiedResLocale=en-US

View file

@ -11,4 +11,8 @@
<plurals name="selected_count">
<item quantity="other">%d selected</item>
</plurals>
<plurals name="bundle_patches_available">
<item quantity="one">%d patch available</item>
<item quantity="other">%d patches available</item>
</plurals>
</resources>

View file

@ -71,10 +71,15 @@
<string name="theme_description">Choose between light or dark theme</string>
<string name="multithreaded_dex_file_writer">Multi-threaded DEX file writer</string>
<string name="multithreaded_dex_file_writer_description">Use multiple cores to write DEX files. This is faster, but uses more memory</string>
<string name="safeguards">Safeguards</string>
<string name="patch_compat_check">Disable version compatibility check</string>
<string name="patch_compat_check_description">The check restricts patches to supported app versions</string>
<string name="suggested_version_safeguard">Require suggested app version</string>
<string name="suggested_version_safeguard_description">Enforce selection of the suggested app version</string>
<string name="patch_selection_safeguard">Allow changing patch selection</string>
<string name="patch_selection_safeguard_description">Do not prevent selecting or deselecting patches</string>
<string name="universal_patches_safeguard">Disable universal patch warning</string>
<string name="universal_patches_safeguard_description">Disables the warning that appears when you try to select universal patches</string>
<string name="import_keystore">Import keystore</string>
<string name="import_keystore_description">Import a custom keystore</string>
<string name="import_keystore_dialog_title">Enter keystore credentials</string>
@ -133,6 +138,7 @@
<string name="apply">Apply</string>
<string name="help">Help</string>
<string name="back">Back</string>
<string name="warning">Warning</string>
<string name="add">Add</string>
<string name="close">Close</string>
<string name="system">System</string>
@ -181,8 +187,6 @@
<string name="source_replace_fail">Failed to load updated patch bundle: %s</string>
<string name="source_replace_integrations_fail">Failed to update integrations: %s</string>
<string name="no_patched_apps_found">No patched apps found</string>
<string name="no_patches">No patches available to view</string>
<string name="patches_available">%d Patches available, tap to view</string>
<string name="tap_on_patches">Tap on the patches to get more information about them</string>
<string name="bundles_selected">%s selected</string>
<string name="unsupported_app">Unsupported app</string>
@ -191,10 +195,10 @@
<string name="patch_selection_reset_toast">Patch selection and options has been reset to recommended defaults</string>
<string name="patch_options_reset_toast">Patch options have been reset</string>
<string name="non_suggested_version_warning_title">Non suggested version</string>
<string name="non_suggested_version_warning_description">The version of the app you have selected does not match the suggested version.\nPlease use the suggested version: %s</string>
<string name="non_suggested_version_warning_description">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.</string>
<string name="selection_warning_title">Stop using defaults?</string>
<string name="selection_warning_description">You may encounter issues when not using the default patch selection and options.</string>
<string name="selection_warning_continue_countdown">Continue (%ds)</string>
<string name="selection_warning_description">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.</string>
<string name="universal_patch_warning_description">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.</string>
<string name="supported">Supported</string>
<string name="universal">Universal</string>
<string name="unsupported">Unsupported</string>
@ -299,8 +303,8 @@
<string name="bundle_input_source_url">Source URL</string>
<string name="bundle_update_success">Successfully updated %s</string>
<string name="bundle_update_unavailable">No update available for %s</string>
<string name="automatically_update">Automatically update</string>
<string name="automatically_update_description">Automatically update this bundle when ReVanced starts</string>
<string name="bundle_auto_update">Auto update</string>
<string name="bundle_auto_update_description">Automatically update this bundle when ReVanced starts</string>
<string name="bundle_type">Bundle type</string>
<string name="bundle_type_description">Choose the type of bundle you want</string>
<string name="about_revanced_manager">About ReVanced Manager</string>
@ -319,7 +323,7 @@
<string name="changelog_loading">Loading changelog</string>
<string name="changelog_download_fail">Failed to download changelog: %s</string>
<string name="changelog_description">Check out the latest changes in this update</string>
<string name="battery_optimization_notification">Battery optimization must be turned off in order for ReVanced Manager to work correctly in the background. Click here to turn off.</string>
<string name="battery_optimization_notification">Battery optimizations must be turned off in order for ReVanced Manager to work correctly in the background. Click here to turn off optimizations.</string>
<string name="installing_manager_update">Installing update…</string>
<string name="downloading_manager_update">Downloading update…</string>
<string name="download_manager_failed">Failed to download update: %s</string>

View file

@ -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"