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' java-version: '17'
distribution: 'temurin' distribution: 'temurin'
- name: Setup Gradle - name: Set up Gradle
uses: gradle/gradle-build-action@v2 uses: gradle/actions/setup-gradle@v3
- name: Build with Gradle - name: Build with Gradle
env: env:
@ -38,7 +38,7 @@ jobs:
run: mv app/build/outputs/apk/release/app-release.apk revanced-manager-${{ env.COMMIT_HASH }}.apk run: mv app/build/outputs/apk/release/app-release.apk revanced-manager-${{ env.COMMIT_HASH }}.apk
- name: Upload build - name: Upload build
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: revanced-manager name: revanced-manager
path: revanced-manager-${{ env.COMMIT_HASH }}.apk path: revanced-manager-${{ env.COMMIT_HASH }}.apk

View File

@ -20,10 +20,8 @@ jobs:
java-version: '17' java-version: '17'
distribution: 'temurin' distribution: 'temurin'
- name: Setup Gradle - name: Set up Gradle
uses: gradle/gradle-build-action@v2 uses: gradle/actions/setup-gradle@v3
with:
cache-disabled: true
- name: Build with Gradle - name: Build with Gradle
env: env:

View File

@ -11,7 +11,7 @@ jobs:
name: Dispatch event to documentation repository name: Dispatch event to documentation repository
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
steps: steps:
- uses: peter-evans/repository-dispatch@v2 - uses: peter-evans/repository-dispatch@v3
with: with:
token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }} token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }}
repository: revanced/revanced-documentation repository: revanced/revanced-documentation

View File

@ -20,9 +20,6 @@ android {
targetSdk = 34 targetSdk = 34
versionCode = 1 versionCode = 1
versionName = "0.0.1" versionName = "0.0.1"
resourceConfigurations.addAll(listOf(
"en",
))
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
} }
@ -88,6 +85,12 @@ android {
buildFeatures.aidl = true buildFeatures.aidl = true
buildFeatures.buildConfig=true buildFeatures.buildConfig=true
android {
androidResources {
generateLocaleConfig = true
}
}
composeOptions.kotlinCompilerExtensionVersion = "1.5.10" composeOptions.kotlinCompilerExtensionVersion = "1.5.10"
externalNativeBuild { externalNativeBuild {
cmake { cmake {

View File

@ -1,23 +1,18 @@
package app.revanced.manager package app.revanced.manager
import android.app.Application import android.app.Application
import android.content.Intent
import app.revanced.manager.di.* import app.revanced.manager.di.*
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.service.ManagerRootService
import app.revanced.manager.service.RootConnection
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import coil.Coil import coil.Coil
import coil.ImageLoader import coil.ImageLoader
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.internal.BuilderImpl import com.topjohnwu.superuser.internal.BuilderImpl
import com.topjohnwu.superuser.ipc.RootService
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.zhanghai.android.appiconloader.coil.AppIconFetcher import me.zhanghai.android.appiconloader.coil.AppIconFetcher
import me.zhanghai.android.appiconloader.coil.AppIconKeyer 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.android.inject
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger import org.koin.android.ext.koin.androidLogger
@ -61,9 +56,6 @@ class ManagerApplication : Application() {
val shellBuilder = BuilderImpl.create().setFlags(Shell.FLAG_MOUNT_MASTER) val shellBuilder = BuilderImpl.create().setFlags(Shell.FLAG_MOUNT_MASTER)
Shell.setDefaultBuilder(shellBuilder) Shell.setDefaultBuilder(shellBuilder)
val intent = Intent(this, ManagerRootService::class.java)
RootService.bind(intent, get<RootConnection>())
scope.launch { scope.launch {
prefs.preload() prefs.preload()
} }

View File

@ -1,11 +1,9 @@
package app.revanced.manager.di package app.revanced.manager.di
import app.revanced.manager.domain.installer.RootInstaller import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.service.RootConnection
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module import org.koin.dsl.module
val rootModule = module { val rootModule = module {
singleOf(::RootConnection)
singleOf(::RootInstaller) singleOf(::RootInstaller)
} }

View File

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

View File

@ -9,9 +9,11 @@ import app.revanced.manager.R
import app.revanced.manager.domain.repository.PatchBundlePersistenceRepository import app.revanced.manager.domain.repository.PatchBundlePersistenceRepository
import app.revanced.manager.patcher.patch.PatchBundle import app.revanced.manager.patcher.patch.PatchBundle
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject 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]. * 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. * The flow will emit null if the associated [PatchBundleSource] is deleted.
*/ */
fun propsFlow() = configRepository.getProps(uid) fun propsFlow() = configRepository.getProps(uid).flowOn(Dispatchers.Default)
suspend fun getProps() = configRepository.getProps(uid).first()!! suspend fun getProps() = propsFlow().first()!!
suspend fun currentVersion() = getProps().versionInfo suspend fun currentVersion() = getProps().versionInfo
protected suspend fun saveVersion(patches: String?, integrations: String?) = protected suspend fun saveVersion(patches: String?, integrations: String?) =

View File

@ -1,49 +1,93 @@
package app.revanced.manager.domain.installer package app.revanced.manager.domain.installer
import android.app.Application 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 app.revanced.manager.util.PM
import com.topjohnwu.superuser.Shell 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.Dispatchers
import kotlinx.coroutines.time.withTimeoutOrNull
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.time.Duration
class RootInstaller( class RootInstaller(
private val app: Application, private val app: Application,
private val rootConnection: RootConnection,
private val pm: PM 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 hasRootAccess() = Shell.isAppGrantedRoot() ?: false
fun isAppInstalled(packageName: String) = suspend fun isAppInstalled(packageName: String) =
rootConnection.remoteFS?.getFile("$modulesPath/$packageName-revanced") awaitRemoteFS().getFile("$modulesPath/$packageName-revanced").exists()
?.exists() ?: throw RootServiceException()
fun isAppMounted(packageName: String): Boolean { suspend fun isAppMounted(packageName: String) = withContext(Dispatchers.IO) {
return pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir?.let { pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir?.let {
Shell.cmd("mount | grep \"$it\"").exec().isSuccess execute("mount | grep \"$it\"").isSuccess
} ?: false } ?: false
} }
fun mount(packageName: String) { suspend fun mount(packageName: String) {
if (isAppMounted(packageName)) return if (isAppMounted(packageName)) return
withContext(Dispatchers.IO) {
val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir
?: throw Exception("Failed to load application info") ?: throw Exception("Failed to load application info")
val patchedAPK = "$modulesPath/$packageName-revanced/$packageName.apk" val patchedAPK = "$modulesPath/$packageName-revanced/$packageName.apk"
Shell.cmd("mount -o bind \"$patchedAPK\" \"$stockAPK\"").exec() execute("mount -o bind \"$patchedAPK\" \"$stockAPK\"").assertSuccess("Failed to mount APK")
.also { if (!it.isSuccess) throw Exception("Failed to mount APK") } }
} }
fun unmount(packageName: String) { suspend fun unmount(packageName: String) {
if (!isAppMounted(packageName)) return if (!isAppMounted(packageName)) return
withContext(Dispatchers.IO) {
val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir
?: throw Exception("Failed to load application info") ?: throw Exception("Failed to load application info")
Shell.cmd("umount -l \"$stockAPK\"").exec() execute("umount -l \"$stockAPK\"").assertSuccess("Failed to unmount APK")
.also { if (!it.isSuccess) throw Exception("Failed to unmount APK") } }
} }
suspend fun install( suspend fun install(
@ -52,9 +96,8 @@ class RootInstaller(
packageName: String, packageName: String,
version: String, version: String,
label: String label: String
) { ) = withContext(Dispatchers.IO) {
withContext(Dispatchers.IO) { val remoteFS = awaitRemoteFS()
rootConnection.remoteFS?.let { remoteFS ->
val assets = app.assets val assets = app.assets
val modulePath = "$modulesPath/$packageName-revanced" val modulePath = "$modulesPath/$packageName-revanced"
@ -63,12 +106,10 @@ class RootInstaller(
stockAPK?.let { stockApp -> stockAPK?.let { stockApp ->
pm.getPackageInfo(packageName)?.let { packageInfo -> pm.getPackageInfo(packageName)?.let { packageInfo ->
if (packageInfo.versionName <= version) if (packageInfo.versionName <= version)
Shell.cmd("pm uninstall -k --user 0 $packageName").exec() execute("pm uninstall -k --user 0 $packageName").assertSuccess("Failed to uninstall stock app")
.also { if (!it.isSuccess) throw Exception("Failed to uninstall stock app") }
} }
Shell.cmd("pm install \"${stockApp.absolutePath}\"").exec() execute("pm install \"${stockApp.absolutePath}\"").assertSuccess("Failed to install stock app")
.also { if (!it.isSuccess) throw Exception("Failed to install stock app") }
} }
remoteFS.getFile(modulePath).mkdir() remoteFS.getFile(modulePath).mkdir()
@ -101,31 +142,31 @@ class RootInstaller(
} }
} }
Shell.cmd( execute(
"chmod 644 $apkPath", "chmod 644 $apkPath",
"chown system:system $apkPath", "chown system:system $apkPath",
"chcon u:object_r:apk_data_file:s0 $apkPath", "chcon u:object_r:apk_data_file:s0 $apkPath",
"chmod +x $modulePath/service.sh" "chmod +x $modulePath/service.sh"
).exec() ).assertSuccess("Failed to set file permissions")
.let { if (!it.isSuccess) throw Exception("Failed to set file permissions") }
}
} ?: throw RootServiceException()
} }
} }
fun uninstall(packageName: String) { suspend fun uninstall(packageName: String) {
rootConnection.remoteFS?.let { remoteFS -> val remoteFS = awaitRemoteFS()
if (isAppMounted(packageName)) if (isAppMounted(packageName))
unmount(packageName) unmount(packageName)
remoteFS.getFile("$modulesPath/$packageName-revanced").deleteRecursively() remoteFS.getFile("$modulesPath/$packageName-revanced").deleteRecursively()
.also { if (!it) throw Exception("Failed to delete files") } .also { if (!it) throw Exception("Failed to delete files") }
} ?: throw RootServiceException()
} }
companion object { companion object {
const val modulesPath = "/data/adb/modules" 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 multithreadingDexFileWriter = booleanPreference("multithreading_dex_file_writer", true)
val useProcessRuntime = booleanPreference("use_process_runtime", false) val useProcessRuntime = booleanPreference("use_process_runtime", false)
val patcherProcessMemoryLimit = intPreference("process_runtime_memory_limit", 700) 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 keystoreCommonName = stringPreference("keystore_cn", KeystoreManager.DEFAULT)
val keystorePass = stringPreference("keystore_pass", KeystoreManager.DEFAULT) val keystorePass = stringPreference("keystore_pass", KeystoreManager.DEFAULT)
@ -25,8 +24,8 @@ class PreferencesManager(
val firstLaunch = booleanPreference("first_launch", true) val firstLaunch = booleanPreference("first_launch", true)
val managerAutoUpdates = booleanPreference("manager_auto_updates", false) val managerAutoUpdates = booleanPreference("manager_auto_updates", false)
val disablePatchVersionCompatCheck = booleanPreference("disable_patch_version_compatibility_check", false)
val disableSelectionWarning = booleanPreference("disable_selection_warning", 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) val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true)
} }

View File

@ -1,8 +1,6 @@
package app.revanced.manager.service package app.revanced.manager.service
import android.content.ComponentName
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder import android.os.IBinder
import app.revanced.manager.IRootSystemService import app.revanced.manager.IRootSystemService
import com.topjohnwu.superuser.ipc.RootService import com.topjohnwu.superuser.ipc.RootService
@ -14,23 +12,5 @@ class ManagerRootService : RootService() {
FileSystemManager.getService() FileSystemManager.getService()
} }
override fun onBind(intent: Intent): IBinder { override fun onBind(intent: Intent): IBinder = RootSystemService()
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
}
} }

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.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import app.revanced.manager.R import app.revanced.manager.R
@ -115,8 +116,8 @@ fun BaseBundleDialog(
if (remoteUrl != null) { if (remoteUrl != null) {
BundleListItem( BundleListItem(
headlineText = stringResource(R.string.automatically_update), headlineText = stringResource(R.string.bundle_auto_update),
supportingText = stringResource(R.string.automatically_update_description), supportingText = stringResource(R.string.bundle_auto_update_description),
trailingContent = { trailingContent = {
Switch( Switch(
checked = autoUpdate, checked = autoUpdate,
@ -163,8 +164,7 @@ fun BaseBundleDialog(
val patchesClickable = LocalContext.current.isDebuggable && patchCount > 0 val patchesClickable = LocalContext.current.isDebuggable && patchCount > 0
BundleListItem( BundleListItem(
headlineText = stringResource(R.string.patches), headlineText = stringResource(R.string.patches),
supportingText = if (patchCount == 0) stringResource(R.string.no_patches) supportingText = pluralStringResource(R.plurals.bundle_patches_available, patchCount, patchCount),
else stringResource(R.string.patches_available, patchCount),
modifier = Modifier.clickable(enabled = patchesClickable, onClick = onPatchesClick) modifier = Modifier.clickable(enabled = patchesClickable, onClick = onPatchesClick)
) { ) {
if (patchesClickable) if (patchesClickable)

View File

@ -6,35 +6,38 @@ import kotlinx.parcelize.Parcelize
sealed interface SettingsDestination : Parcelable { sealed interface SettingsDestination : Parcelable {
@Parcelize @Parcelize
object Settings : SettingsDestination data object Settings : SettingsDestination
@Parcelize @Parcelize
object General : SettingsDestination data object General : SettingsDestination
@Parcelize @Parcelize
object Advanced : SettingsDestination data object Advanced : SettingsDestination
@Parcelize @Parcelize
object Updates : SettingsDestination data object Updates : SettingsDestination
@Parcelize @Parcelize
object Downloads : SettingsDestination data object Downloads : SettingsDestination
@Parcelize @Parcelize
object ImportExport : SettingsDestination data object ImportExport : SettingsDestination
@Parcelize @Parcelize
object About : SettingsDestination data object About : SettingsDestination
@Parcelize @Parcelize
data class Update(val downloadOnScreenEntry: Boolean = false) : SettingsDestination data class Update(val downloadOnScreenEntry: Boolean = false) : SettingsDestination
@Parcelize @Parcelize
object Changelogs : SettingsDestination data object Changelogs : SettingsDestination
@Parcelize @Parcelize
object Contributors: SettingsDestination data object Contributors: SettingsDestination
@Parcelize @Parcelize
object Licenses: SettingsDestination data object Licenses: SettingsDestination
@Parcelize
data object DeveloperOptions: SettingsDestination
} }

View File

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

View File

@ -1,5 +1,9 @@
package app.revanced.manager.ui.screen 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.activity.compose.BackHandler
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement 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.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add 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.filled.Close
import androidx.compose.material.icons.outlined.Apps import androidx.compose.material.icons.outlined.Apps
import androidx.compose.material.icons.outlined.DeleteOutline import androidx.compose.material.icons.outlined.DeleteOutline
@ -67,6 +72,7 @@ enum class DashboardPage(
BUNDLES(R.string.tab_bundles, Icons.Outlined.Source), BUNDLES(R.string.tab_bundles, Icons.Outlined.Source),
} }
@SuppressLint("BatteryLife")
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable @Composable
fun DashboardScreen( fun DashboardScreen(
@ -211,6 +217,20 @@ fun DashboardScreen(
) )
} }
} else null, } 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 { vm.updatedManagerVersion?.let {
{ {
NotificationCard( NotificationCard(

View File

@ -83,7 +83,7 @@ fun InstalledAppInfoScreen(
if (viewModel.installedApp.installType == InstallType.ROOT) { if (viewModel.installedApp.installType == InstallType.ROOT) {
Text( Text(
text = if (viewModel.rootInstaller.isAppMounted(viewModel.installedApp.currentPackageName)) { text = if (viewModel.isMounted) {
stringResource(R.string.mounted) stringResource(R.string.mounted)
} else { } else {
stringResource(R.string.not_mounted) 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.Save
import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.WarningAmber
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -37,7 +38,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -48,17 +48,16 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R 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.Option
import app.revanced.manager.patcher.patch.PatchInfo import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.Countdown import app.revanced.manager.ui.component.SafeguardDialog
import app.revanced.manager.ui.component.DangerousActionDialogBase
import app.revanced.manager.ui.component.LazyColumnWithScrollbar import app.revanced.manager.ui.component.LazyColumnWithScrollbar
import app.revanced.manager.ui.component.SearchView import app.revanced.manager.ui.component.SearchView
import app.revanced.manager.ui.component.patches.OptionItem 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.PatchSelection
import app.revanced.manager.util.isScrollingUp import app.revanced.manager.util.isScrollingUp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.compose.koinInject
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable @Composable
@ -160,10 +158,16 @@ fun PatchesSelectorScreen(
) )
} }
vm.pendingSelectionAction?.let { var showSelectionWarning by rememberSaveable {
SelectionWarningDialog( mutableStateOf(false)
onCancel = vm::dismissSelectionWarning, }
onConfirm = vm::confirmSelectionWarning if (showSelectionWarning) {
SelectionWarningDialog(onDismiss = { showSelectionWarning = false })
}
vm.pendingUniversalPatchAction?.let {
UniversalPatchWarningDialog(
onCancel = vm::dismissUniversalPatchWarning,
onConfirm = vm::confirmUniversalPatchWarning
) )
} }
@ -196,9 +200,9 @@ fun PatchesSelectorScreen(
), ),
onToggle = { onToggle = {
if (vm.selectionWarningEnabled) { if (vm.selectionWarningEnabled) {
vm.pendingSelectionAction = { showSelectionWarning = true
vm.togglePatch(uid, patch) } else if (vm.universalPatchWarningEnabled && patch.compatiblePackages == null) {
} vm.pendingUniversalPatchAction = { vm.togglePatch(uid, patch) }
} else { } else {
vm.togglePatch(uid, patch) vm.togglePatch(uid, patch)
} }
@ -369,36 +373,43 @@ fun PatchesSelectorScreen(
} }
@Composable @Composable
fun SelectionWarningDialog( fun SelectionWarningDialog(onDismiss: () -> Unit) {
onCancel: () -> Unit, SafeguardDialog(
onConfirm: (Boolean) -> Unit onDismiss = onDismiss,
) { title = R.string.warning,
val prefs: PreferencesManager = koinInject() body = stringResource(R.string.selection_warning_description),
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) }
}
@Composable
fun UniversalPatchWarningDialog(
onCancel: () -> Unit,
onConfirm: () -> Unit
) {
AlertDialog(
onDismissRequest = onCancel,
confirmButton = {
TextButton(onClick = onConfirm) {
Text(stringResource(R.string.continue_))
} }
}, },
title = R.string.selection_warning_title, dismissButton = {
body = stringResource(R.string.selection_warning_description), 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 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.clickable
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BatteryAlert
import androidx.compose.material.icons.outlined.* import androidx.compose.material.icons.outlined.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable 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.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ColumnWithScrollbar 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.component.settings.SettingsListItem
import app.revanced.manager.ui.destination.SettingsDestination import app.revanced.manager.ui.destination.SettingsDestination
import app.revanced.manager.ui.screen.settings.* 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.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
@SuppressLint("BatteryLife")
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SettingsScreen( fun SettingsScreen(
@ -54,10 +38,6 @@ fun SettingsScreen(
else navController.pop() 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( val settingsSections = listOf(
Triple( Triple(
R.string.general, R.string.general,
@ -122,7 +102,8 @@ fun SettingsScreen(
is SettingsDestination.About -> AboutSettingsScreen( is SettingsDestination.About -> AboutSettingsScreen(
onBackClick = backClick, onBackClick = backClick,
onContributorsClick = { navController.navigate(SettingsDestination.Contributors) }, onContributorsClick = { navController.navigate(SettingsDestination.Contributors) },
onLicensesClick = { navController.navigate(SettingsDestination.Licenses) } onDeveloperOptionsClick = { navController.navigate(SettingsDestination.DeveloperOptions) },
onLicensesClick = { navController.navigate(SettingsDestination.Licenses) },
) )
is SettingsDestination.Update -> UpdateScreen( is SettingsDestination.Update -> UpdateScreen(
@ -146,6 +127,8 @@ fun SettingsScreen(
onBackClick = backClick, onBackClick = backClick,
) )
is SettingsDestination.DeveloperOptions -> DeveloperOptionsScreen(onBackClick = backClick)
is SettingsDestination.Settings -> { is SettingsDestination.Settings -> {
Scaffold( Scaffold(
topBar = { topBar = {
@ -160,21 +143,6 @@ fun SettingsScreen(
.padding(paddingValues) .padding(paddingValues)
.fillMaxSize() .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) -> settingsSections.forEach { (titleDescIcon, destination) ->
SettingsListItem( SettingsListItem(
modifier = Modifier.clickable { navController.navigate(destination) }, modifier = Modifier.clickable { navController.navigate(destination) },

View File

@ -69,8 +69,7 @@ fun VersionSelectorScreen(
if (viewModel.showNonSuggestedVersionDialog) if (viewModel.showNonSuggestedVersionDialog)
NonSuggestedVersionDialog( NonSuggestedVersionDialog(
suggestedVersion = viewModel.requiredVersion.orEmpty(), suggestedVersion = viewModel.requiredVersion.orEmpty(),
onCancel = viewModel::dismissNonSuggestedVersionDialog, onDismiss = viewModel::dismissNonSuggestedVersionDialog
onContinue = viewModel::continueWithNonSuggestedVersion,
) )
val lazyListState = rememberLazyListState() 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.component.settings.SettingsListItem
import app.revanced.manager.ui.viewmodel.AboutViewModel import app.revanced.manager.ui.viewmodel.AboutViewModel
import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.getSocialIcon import app.revanced.manager.ui.viewmodel.AboutViewModel.Companion.getSocialIcon
import app.revanced.manager.util.isDebuggable
import app.revanced.manager.util.openUrl import app.revanced.manager.util.openUrl
import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.google.accompanist.drawablepainter.rememberDrawablePainter
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
@ -50,6 +49,7 @@ fun AboutSettingsScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
onContributorsClick: () -> Unit, onContributorsClick: () -> Unit,
onLicensesClick: () -> Unit, onLicensesClick: () -> Unit,
onDeveloperOptionsClick: () -> Unit,
viewModel: AboutViewModel = koinViewModel() viewModel: AboutViewModel = koinViewModel()
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -116,9 +116,11 @@ fun AboutSettingsScreen(
stringResource(R.string.contributors_description), stringResource(R.string.contributors_description),
third = onContributorsClick third = onContributorsClick
), ),
Triple(stringResource(R.string.developer_options), Triple(
stringResource(R.string.developer_options),
stringResource(R.string.developer_options_description), stringResource(R.string.developer_options_description),
third = { /*TODO*/ }).takeIf { context.isDebuggable }, third = onDeveloperOptionsClick
),
Triple( Triple(
stringResource(R.string.opensource_licenses), stringResource(R.string.opensource_licenses),
stringResource(R.string.opensource_licenses_description), stringResource(R.string.opensource_licenses_description),

View File

@ -72,6 +72,8 @@ fun AdvancedSettingsScreen(
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
) { ) {
GroupHeader(stringResource(R.string.manager))
val apiUrl by vm.prefs.api.getAsState() val apiUrl by vm.prefs.api.getAsState()
var showApiUrlDialog by rememberSaveable { mutableStateOf(false) } var showApiUrlDialog by rememberSaveable { mutableStateOf(false) }
@ -111,12 +113,26 @@ fun AdvancedSettingsScreen(
headline = R.string.process_runtime_memory_limit, headline = R.string.process_runtime_memory_limit,
description = R.string.process_runtime_memory_limit_description, 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( BooleanItem(
preference = vm.prefs.disablePatchVersionCompatCheck, preference = vm.prefs.disablePatchVersionCompatCheck,
coroutineScope = vm.viewModelScope, coroutineScope = vm.viewModelScope,
headline = R.string.patch_compat_check, headline = R.string.patch_compat_check,
description = R.string.patch_compat_check_description 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( BooleanItem(
preference = vm.prefs.suggestedVersionSafeguard, preference = vm.prefs.suggestedVersionSafeguard,
coroutineScope = vm.viewModelScope, coroutineScope = vm.viewModelScope,
@ -124,24 +140,10 @@ fun AdvancedSettingsScreen(
description = R.string.suggested_version_safeguard_description description = R.string.suggested_version_safeguard_description
) )
BooleanItem( BooleanItem(
preference = vm.prefs.multithreadingDexFileWriter, preference = vm.prefs.disableSelectionWarning,
coroutineScope = vm.viewModelScope, coroutineScope = vm.viewModelScope,
headline = R.string.multithreaded_dex_file_writer, headline = R.string.patch_selection_safeguard,
description = R.string.multithreaded_dex_file_writer_description, description = R.string.patch_selection_safeguard_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()
}
) )
GroupHeader(stringResource(R.string.debugging)) 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 = { confirmButton = {
Button( TextButton(
onClick = { onClick = {
onConfirm(selectedTheme) onConfirm(selectedTheme)
onDismiss() onDismiss()

View File

@ -6,12 +6,10 @@ import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.domain.bundles.RemotePatchBundle
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import app.revanced.manager.util.uiSafe
import com.github.pgreze.process.Redirect import com.github.pgreze.process.Redirect
import com.github.pgreze.process.process import com.github.pgreze.process.process
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
@ -43,16 +41,6 @@ class AdvancedSettingsViewModel(
patchBundleRepository.reloadApiBundles() 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 { fun exportDebugLogs(target: Uri) = viewModelScope.launch {
val exitCode = try { val exitCode = try {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {

View File

@ -3,14 +3,12 @@ package app.revanced.manager.ui.viewmodel
import android.app.Application import android.app.Application
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.net.Uri import android.net.Uri
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchBundleRepository import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.ui.model.SelectedApp import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.util.PM import app.revanced.manager.util.PM
@ -25,8 +23,7 @@ import java.nio.file.Files
class AppSelectorViewModel( class AppSelectorViewModel(
private val app: Application, private val app: Application,
private val pm: PM, private val pm: PM,
private val patchBundleRepository: PatchBundleRepository, private val patchBundleRepository: PatchBundleRepository
private val prefs: PreferencesManager,
) : ViewModel() { ) : ViewModel() {
private val inputFile = File(app.cacheDir, "input.apk").also { private val inputFile = File(app.cacheDir, "input.apk").also {
it.delete() it.delete()
@ -46,13 +43,6 @@ class AppSelectorViewModel(
nonSuggestedVersionDialogSubject = null nonSuggestedVersionDialogSubject = null
} }
fun continueWithNonSuggestedVersion(dismissPermanently: Boolean) = viewModelScope.launch {
if (dismissPermanently) prefs.suggestedVersionSafeguard.update(false)
nonSuggestedVersionDialogSubject?.let(onStorageClick)
dismissNonSuggestedVersionDialog()
}
fun handleStorageResult(uri: Uri) = viewModelScope.launch { fun handleStorageResult(uri: Uri) = viewModelScope.launch {
val selectedApp = withContext(Dispatchers.IO) { val selectedApp = withContext(Dispatchers.IO) {
loadSelectedFile(uri) loadSelectedFile(uri)

View File

@ -3,10 +3,12 @@ package app.revanced.manager.ui.viewmodel
import android.app.Application import android.app.Application
import android.content.ContentResolver import android.content.ContentResolver
import android.net.Uri import android.net.Uri
import android.os.PowerManager
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.core.content.getSystemService
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.R import app.revanced.manager.R
@ -33,15 +35,21 @@ class DashboardViewModel(
val availablePatches = val availablePatches =
patchBundleRepository.bundles.map { it.values.sumOf { bundle -> bundle.patches.size } } patchBundleRepository.bundles.map { it.values.sumOf { bundle -> bundle.patches.size } }
private val contentResolver: ContentResolver = app.contentResolver private val contentResolver: ContentResolver = app.contentResolver
private val powerManager = app.getSystemService<PowerManager>()!!
val sources = patchBundleRepository.sources val sources = patchBundleRepository.sources
val selectedSources = mutableStateListOf<PatchBundleSource>() val selectedSources = mutableStateListOf<PatchBundleSource>()
var updatedManagerVersion: String? by mutableStateOf(null) var updatedManagerVersion: String? by mutableStateOf(null)
private set private set
var showBatteryOptimizationsWarning by mutableStateOf(false)
private set
init { init {
viewModelScope.launch { checkForManagerUpdates() } viewModelScope.launch {
checkForManagerUpdates()
showBatteryOptimizationsWarning =
!powerManager.isIgnoringBatteryOptimizations(app.packageName)
}
} }
fun dismissUpdateDialog() { fun dismissUpdateDialog() {
@ -80,10 +88,12 @@ class DashboardViewModel(
fun cancelSourceSelection() { fun cancelSourceSelection() {
selectedSources.clear() selectedSources.clear()
} }
fun createLocalSource(patchBundle: Uri, integrations: Uri?) = fun createLocalSource(patchBundle: Uri, integrations: Uri?) =
viewModelScope.launch { viewModelScope.launch {
contentResolver.openInputStream(patchBundle)!!.use { patchesStream -> contentResolver.openInputStream(patchBundle)!!.use { patchesStream ->
integrations?.let { contentResolver.openInputStream(it) }.use { integrationsStream -> integrations?.let { contentResolver.openInputStream(it) }
.use { integrationsStream ->
patchBundleRepository.createLocal(patchesStream, 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) var appInfo: PackageInfo? by mutableStateOf(null)
private set private set
var appliedPatches: PatchSelection? by mutableStateOf(null) var appliedPatches: PatchSelection? by mutableStateOf(null)
var isMounted by mutableStateOf(rootInstaller.isAppMounted(installedApp.currentPackageName)) var isMounted by mutableStateOf(false)
private set private set
init {
viewModelScope.launch {
isMounted = rootInstaller.isAppMounted(installedApp.currentPackageName)
}
}
fun launch() = pm.launch(installedApp.currentPackageName) fun launch() = pm.launch(installedApp.currentPackageName)
fun mountOrUnmount() { fun mountOrUnmount() = viewModelScope.launch {
try { try {
if (isMounted) if (isMounted)
rootInstaller.unmount(installedApp.currentPackageName) rootInstaller.unmount(installedApp.currentPackageName)

View File

@ -1,5 +1,6 @@
package app.revanced.manager.ui.viewmodel package app.revanced.manager.ui.viewmodel
import android.app.Activity
import android.app.Application import android.app.Application
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
@ -39,7 +40,7 @@ class MainViewModel(
val launcher = componentActivity.registerForActivityResult( val launcher = componentActivity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult() ActivityResultContracts.StartActivityForResult()
) { result: ActivityResult -> ) { result: ActivityResult ->
if (result.resultCode == ComponentActivity.RESULT_OK) { if (result.resultCode == Activity.RESULT_OK) {
result.data?.getStringExtra("data")?.let { result.data?.getStringExtra("data")?.let {
applyLegacySettings(it) applyLegacySettings(it)
} ?: app.toast(app.getString(R.string.legacy_import_failed)) } ?: 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.simpleMessage
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.time.withTimeout
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import java.io.File import java.io.File
import java.nio.file.Files import java.nio.file.Files
import java.time.Duration
import java.util.UUID import java.util.UUID
@Stable @Stable
@ -177,6 +183,7 @@ class PatcherViewModel(
} }
} }
@OptIn(DelicateCoroutinesApi::class)
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
app.unregisterReceiver(installBroadcastReceiver) app.unregisterReceiver(installBroadcastReceiver)
@ -188,15 +195,16 @@ class PatcherViewModel(
} }
is SelectedApp.Installed -> { is SelectedApp.Installed -> {
try { GlobalScope.launch(Dispatchers.Main) {
uiSafe(app, R.string.failed_to_mount, "Failed to mount") {
installedApp?.let { installedApp?.let {
if (it.installType == InstallType.ROOT) { if (it.installType == InstallType.ROOT) {
withTimeout(Duration.ofMinutes(1L)) {
rootInstaller.mount(packageName) 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 private val packageName = input.app.packageName
val appVersion = input.app.version val appVersion = input.app.version
var pendingSelectionAction by mutableStateOf<(() -> Unit)?>(null) var pendingUniversalPatchAction by mutableStateOf<(() -> Unit)?>(null)
var selectionWarningEnabled by mutableStateOf(true) var selectionWarningEnabled by mutableStateOf(true)
private set private set
var universalPatchWarningEnabled by mutableStateOf(true)
private set
val allowIncompatiblePatches = val allowIncompatiblePatches =
get<PreferencesManager>().disablePatchVersionCompatCheck.getBlocking() get<PreferencesManager>().disablePatchVersionCompatCheck.getBlocking()
@ -59,6 +61,8 @@ class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
init { init {
viewModelScope.launch { viewModelScope.launch {
universalPatchWarningEnabled = !prefs.disableUniversalPatchWarning.get()
if (prefs.disableSelectionWarning.get()) { if (prefs.disableSelectionWarning.get()) {
selectionWarningEnabled = false selectionWarningEnabled = false
return@launch return@launch
@ -131,21 +135,15 @@ class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
customPatchSelection = selection.put(bundle, newPatches) customPatchSelection = selection.put(bundle, newPatches)
} }
fun confirmSelectionWarning(dismissPermanently: Boolean) { fun confirmUniversalPatchWarning() {
selectionWarningEnabled = false universalPatchWarningEnabled = false
pendingSelectionAction?.invoke() pendingUniversalPatchAction?.invoke()
pendingSelectionAction = null pendingUniversalPatchAction = null
if (!dismissPermanently) return
viewModelScope.launch {
prefs.disableSelectionWarning.update(true)
}
} }
fun dismissSelectionWarning() { fun dismissUniversalPatchWarning() {
pendingSelectionAction = null pendingUniversalPatchAction = null
} }
fun reset() { fun reset() {

View File

@ -162,12 +162,6 @@ class VersionSelectorViewModel(
nonSuggestedVersionDialogSubject = null nonSuggestedVersionDialogSubject = null
} }
fun continueWithNonSuggestedVersion(dismissPermanently: Boolean) = viewModelScope.launch {
if (dismissPermanently) prefs.suggestedVersionSafeguard.update(false)
selectedVersion = nonSuggestedVersionDialogSubject
dismissNonSuggestedVersionDialog()
}
fun select(app: SelectedApp) { fun select(app: SelectedApp) {
if (requiredVersion != null && app.version != requiredVersion) { if (requiredVersion != null && app.version != requiredVersion) {
nonSuggestedVersionDialogSubject = app nonSuggestedVersionDialogSubject = app

View File

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

View File

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

View File

@ -71,10 +71,15 @@
<string name="theme_description">Choose between light or dark theme</string> <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">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="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">Disable version compatibility check</string>
<string name="patch_compat_check_description">The check restricts patches to supported app versions</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">Require suggested app version</string>
<string name="suggested_version_safeguard_description">Enforce selection of the 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">Import keystore</string>
<string name="import_keystore_description">Import a custom keystore</string> <string name="import_keystore_description">Import a custom keystore</string>
<string name="import_keystore_dialog_title">Enter keystore credentials</string> <string name="import_keystore_dialog_title">Enter keystore credentials</string>
@ -133,6 +138,7 @@
<string name="apply">Apply</string> <string name="apply">Apply</string>
<string name="help">Help</string> <string name="help">Help</string>
<string name="back">Back</string> <string name="back">Back</string>
<string name="warning">Warning</string>
<string name="add">Add</string> <string name="add">Add</string>
<string name="close">Close</string> <string name="close">Close</string>
<string name="system">System</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_fail">Failed to load updated patch bundle: %s</string>
<string name="source_replace_integrations_fail">Failed to update integrations: %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_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="tap_on_patches">Tap on the patches to get more information about them</string>
<string name="bundles_selected">%s selected</string> <string name="bundles_selected">%s selected</string>
<string name="unsupported_app">Unsupported app</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_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="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_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_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_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="selection_warning_continue_countdown">Continue (%ds)</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="supported">Supported</string>
<string name="universal">Universal</string> <string name="universal">Universal</string>
<string name="unsupported">Unsupported</string> <string name="unsupported">Unsupported</string>
@ -299,8 +303,8 @@
<string name="bundle_input_source_url">Source URL</string> <string name="bundle_input_source_url">Source URL</string>
<string name="bundle_update_success">Successfully updated %s</string> <string name="bundle_update_success">Successfully updated %s</string>
<string name="bundle_update_unavailable">No update available for %s</string> <string name="bundle_update_unavailable">No update available for %s</string>
<string name="automatically_update">Automatically update</string> <string name="bundle_auto_update">Auto update</string>
<string name="automatically_update_description">Automatically update this bundle when ReVanced starts</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">Bundle type</string>
<string name="bundle_type_description">Choose the type of bundle you want</string> <string name="bundle_type_description">Choose the type of bundle you want</string>
<string name="about_revanced_manager">About ReVanced Manager</string> <string name="about_revanced_manager">About ReVanced Manager</string>
@ -319,7 +323,7 @@
<string name="changelog_loading">Loading changelog</string> <string name="changelog_loading">Loading changelog</string>
<string name="changelog_download_fail">Failed to download changelog: %s</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="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="installing_manager_update">Installing update…</string>
<string name="downloading_manager_update">Downloading update…</string> <string name="downloading_manager_update">Downloading update…</string>
<string name="download_manager_failed">Failed to download update: %s</string> <string name="download_manager_failed">Failed to download update: %s</string>

View File

@ -1,14 +1,14 @@
[versions] [versions]
ktx = "1.12.0" ktx = "1.13.1"
material3 = "1.2.1" material3 = "1.2.1"
ui-tooling = "1.6.4" ui-tooling = "1.6.8"
viewmodel-lifecycle = "2.7.0" viewmodel-lifecycle = "2.8.3"
splash-screen = "1.0.1" splash-screen = "1.0.1"
compose-activity = "1.8.2" compose-activity = "1.9.0"
paging = "3.2.1" paging = "3.3.0"
preferences-datastore = "1.0.0" preferences-datastore = "1.1.1"
work-runtime = "2.9.0" work-runtime = "2.9.0"
compose-bom = "2024.03.00" compose-bom = "2024.06.00"
accompanist = "0.34.0" accompanist = "0.34.0"
placeholder = "1.1.2" placeholder = "1.1.2"
reorderable = "1.5.2" reorderable = "1.5.2"
@ -23,7 +23,7 @@ reimagined-navigation = "1.5.0"
ktor = "2.3.9" ktor = "2.3.9"
markdown-renderer = "0.22.0" markdown-renderer = "0.22.0"
fading-edges = "1.0.4" fading-edges = "1.0.4"
android-gradle-plugin = "8.3.0" android-gradle-plugin = "8.3.2"
kotlin-gradle-plugin = "1.9.22" kotlin-gradle-plugin = "1.9.22"
dev-tools-gradle-plugin = "1.9.22-1.0.17" dev-tools-gradle-plugin = "1.9.22-1.0.17"
about-libraries-gradle-plugin = "11.1.1" about-libraries-gradle-plugin = "11.1.1"