From 3df991d45bfc33a93e8786c1c907c3c18efc000a Mon Sep 17 00:00:00 2001
From: Lennoard
Date: Sun, 10 Aug 2025 23:24:52 -0300
Subject: [PATCH 01/18] refactor: updated build scripts + version catalog
---
app/build.gradle.kts | 55 ++++++++-----
.../ui/params/edit/EditKernelParamActivity.kt | 5 --
build.gradle.kts | 38 ++-------
buildSrc/src/main/kotlin/AndroidX.kt | 1 -
buildSrc/src/main/kotlin/AppConfig.kt | 8 +-
common/design/build.gradle.kts | 34 ++++----
common/utils/build.gradle.kts | 13 +--
data/build.gradle.kts | 39 ++++-----
.../sysctlgui/data/utils/RootUtils.kt | 9 +--
domain/build.gradle.kts | 34 ++++++--
gradle/libs.versions.toml | 81 +++++++++++++++++++
gradle/wrapper/gradle-wrapper.properties | 2 +-
settings.gradle.kts | 29 ++++++-
13 files changed, 232 insertions(+), 116 deletions(-)
create mode 100644 gradle/libs.versions.toml
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 4aa5fe0..b202615 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,11 +1,12 @@
-import org.jetbrains.kotlin.config.KotlinCompilerVersion
import java.util.Properties
plugins {
- id("com.android.application")
- kotlin("android")
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.compose)
+ alias(libs.plugins.ksp)
+ alias(libs.plugins.jetbrains.kotlin.serialization)
kotlin("kapt")
- id("com.google.devtools.ksp")
id("kotlin-parcelize")
}
@@ -93,28 +94,38 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}
- kotlin {
- jvmToolchain(17)
- }
-
- composeOptions {
- kotlinCompilerExtensionVersion = Compose.kotlinCompilerExtensionVersion
+ kotlinOptions {
+ jvmTarget = "17"
}
}
dependencies {
- implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
- implementation(kotlin("stdlib-jdk8", KotlinCompilerVersion.VERSION))
+ implementation(project(":common:design"))
+ implementation(project(":common:utils"))
+ implementation(project(":domain"))
+ implementation(project(":data"))
+
+ implementation(libs.kotlin.stdlib)
+ implementation(libs.kotlinx.coroutines.android)
+
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.androidx.material)
+ implementation(libs.androidx.navigation.compose)
+ implementation(libs.androidx.window)
+
+ // Lifecycle
+ implementation(libs.androidx.lifecycle.runtime.ktx)
+ implementation(libs.androidx.lifecycle.runtime.compose)
+ implementation(libs.androidx.lifecycle.viewmodel.ktx)
+ implementation(libs.androidx.lifecycle.viewmodel.navigation3)
+ implementation(libs.androidx.lifecycle.viewmodel.compose)
+ implementation(libs.androidx.lifecycle.viewmodel.savedstate)
+ //ksp(libs.androidx.lifecycle.compiler)
- implementation(project(Modules.domain))
- implementation(project(Modules.data))
- implementation(project(Modules.utils))
- implementation(project(Modules.design))
-
- implementation(AndroidX.activity)
implementation(AndroidX.splashScreen)
implementation(AndroidX.lifecycleLiveData)
- implementation(AndroidX.lifecycleRuntimeCompose)
implementation(AndroidX.navigationFragment)
implementation(AndroidX.navigationUi)
implementation(AndroidX.preference)
@@ -125,8 +136,10 @@ dependencies {
implementation(Google.gson)
- implementation(Dependencies.koinAndroid)
- implementation(Dependencies.libSuCore)
+ implementation(libs.koin)
+ implementation(libs.koin.compose)
+ implementation(libs.bundles.libsu)
+
implementation(Dependencies.libSuIo)
implementation(Dependencies.liveEvent)
implementation(Dependencies.tapTargetView)
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditKernelParamActivity.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditKernelParamActivity.kt
index 3719b12..459e4e1 100644
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditKernelParamActivity.kt
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditKernelParamActivity.kt
@@ -37,11 +37,6 @@ class EditKernelParamActivity : ComponentActivity() {
handleIntent(intent)
}
- override fun onNewIntent(intent: Intent?) {
- super.onNewIntent(intent)
- handleIntent(intent ?: return)
- }
-
private fun handleIntent(intent: Intent) {
val param = intent.getParcelableExtra(EXTRA_PARAM) as? KernelParam
if (param != null) {
diff --git a/build.gradle.kts b/build.gradle.kts
index d11fcfb..09c0244 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,33 +1,9 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
- id("com.google.devtools.ksp") version "1.9.24-1.0.20" apply false
-}
-
-buildscript {
- repositories {
- google()
- mavenCentral()
- }
-
- dependencies {
- classpath("com.android.tools.build:gradle:8.5.0")
- classpath(BuildPlugins.kotlin)
- }
-}
-
-allprojects {
- repositories {
- google()
- mavenCentral()
- maven {
- url = uri("https://maven.google.com")
- }
- maven {
- url = uri("https://jitpack.io")
- }
- }
-}
-
-tasks.register("clean", Delete::class) {
- delete(rootProject.buildDir)
-}
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.kotlin.android) apply false
+ alias(libs.plugins.kotlin.compose) apply false
+ alias(libs.plugins.jetbrains.kotlin.jvm) apply false
+ alias(libs.plugins.android.library) apply false
+ alias(libs.plugins.ksp) apply false
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/AndroidX.kt b/buildSrc/src/main/kotlin/AndroidX.kt
index 6f22260..52d512d 100644
--- a/buildSrc/src/main/kotlin/AndroidX.kt
+++ b/buildSrc/src/main/kotlin/AndroidX.kt
@@ -8,7 +8,6 @@ object AndroidX {
private const val lifecycleVersion = "2.6.1"
const val lifecycleLiveData = "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
const val lifecycleViewModel = "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
- const val lifecycleRuntimeCompose = "androidx.lifecycle:lifecycle-runtime-compose:$lifecycleVersion"
const val preference = "androidx.preference:preference-ktx:1.2.0"
const val swipeRefreshLayout = "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
diff --git a/buildSrc/src/main/kotlin/AppConfig.kt b/buildSrc/src/main/kotlin/AppConfig.kt
index a2448da..3e47b91 100644
--- a/buildSrc/src/main/kotlin/AppConfig.kt
+++ b/buildSrc/src/main/kotlin/AppConfig.kt
@@ -1,10 +1,10 @@
object AppConfig {
- val devCycle = false
+ val devCycle = true
const val appId = "com.androidvip.sysctlgui"
- const val compileSdkVersion = 34
- const val minSdkVersion = 21
- const val targetSdkVersion = 34
+ const val compileSdkVersion = 36
+ const val minSdkVersion = 24
+ const val targetSdkVersion = 36
const val testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
const val proguardConsumerRules = "consumer-rules.pro"
diff --git a/common/design/build.gradle.kts b/common/design/build.gradle.kts
index e2da347..d9ddb38 100644
--- a/common/design/build.gradle.kts
+++ b/common/design/build.gradle.kts
@@ -1,6 +1,7 @@
plugins {
- id("com.android.library")
- id("org.jetbrains.kotlin.android")
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.compose)
}
android {
@@ -9,7 +10,6 @@ android {
defaultConfig {
minSdk = AppConfig.minSdkVersion
- targetSdk = AppConfig.targetSdkVersion
testInstrumentationRunner = AppConfig.testInstrumentationRunner
consumerProguardFiles(AppConfig.proguardConsumerRules)
@@ -38,27 +38,27 @@ android {
kotlinOptions {
jvmTarget = "17"
}
-
- composeOptions {
- kotlinCompilerExtensionVersion = Compose.kotlinCompilerExtensionVersion
- }
}
dependencies {
- val composeBom = platform(Compose.BoM)
- api(composeBom)
- androidTestImplementation(composeBom)
+ implementation(libs.androidx.core.ktx)
+
+ api(platform(libs.androidx.compose.bom))
+ api(libs.androidx.ui)
+ api(libs.androidx.ui.graphics)
+ api(libs.androidx.ui.tooling.preview)
+ api(libs.androidx.material3)
+ api(libs.androidx.material.icons.core)
+ api(libs.androidx.window)
- api(AndroidX.activity)
- api(AndroidX.appCompat)
api(AndroidX.constraintLayout)
- api(AndroidX.core)
api(AndroidX.swipeRefreshLayout)
- api(Compose.material3)
api(Compose.material)
- api(Compose.activity)
- api(Compose.uiTooling)
- debugApi(Compose.uiTooling)
implementation(AndroidX.splashScreen)
implementation(Google.material)
+
+ androidTestApi(platform(libs.androidx.compose.bom))
+ debugApi(libs.androidx.ui.tooling)
+ debugApi(libs.androidx.ui.test.manifest)
}
+
diff --git a/common/utils/build.gradle.kts b/common/utils/build.gradle.kts
index 6f0ac70..532ccf1 100644
--- a/common/utils/build.gradle.kts
+++ b/common/utils/build.gradle.kts
@@ -1,6 +1,6 @@
plugins {
- id("com.android.library")
- id("org.jetbrains.kotlin.android")
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
}
android {
@@ -9,7 +9,6 @@ android {
defaultConfig {
minSdk = AppConfig.minSdkVersion
- targetSdk = AppConfig.targetSdkVersion
testInstrumentationRunner = AppConfig.testInstrumentationRunner
consumerProguardFiles(AppConfig.proguardConsumerRules)
@@ -36,6 +35,10 @@ android {
}
dependencies {
- implementation(AndroidX.lifecycleViewModel)
- api(Dependencies.coroutinesCore)
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.viewmodel.ktx)
+ implementation(libs.androidx.lifecycle.viewmodel.savedstate)
+
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
}
diff --git a/data/build.gradle.kts b/data/build.gradle.kts
index 9e7f726..28f6e5e 100644
--- a/data/build.gradle.kts
+++ b/data/build.gradle.kts
@@ -1,7 +1,8 @@
plugins {
- id("com.android.library")
- kotlin("android")
- id("com.google.devtools.ksp")
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.jetbrains.kotlin.serialization)
+ alias(libs.plugins.ksp)
}
android {
@@ -10,7 +11,7 @@ android {
defaultConfig {
minSdk = AppConfig.minSdkVersion
- targetSdk = AppConfig.targetSdkVersion
+
javaCompileOptions {
annotationProcessorOptions {
arguments += mapOf(
@@ -33,8 +34,8 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}
- kotlin {
- jvmToolchain(17)
+ kotlinOptions {
+ jvmTarget = "17"
}
sourceSets {
@@ -43,20 +44,22 @@ android {
}
dependencies {
- implementation(project(Modules.domain))
- implementation(project(Modules.utils))
+ implementation(project(":common:utils"))
+ implementation(project(":domain"))
- implementation(AndroidX.preference)
- implementation(AndroidX.room)
- implementation(AndroidX.roomRuntime)
- ksp(AndroidX.roomCompiler)
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.preference)
- implementation(Dependencies.libSuCore)
- implementation(Google.gson)
+ // Room
+ implementation(libs.androidx.room.runtime)
+ implementation(libs.androidx.room.ktx)
+ ksp(libs.androidx.room.compiler)
- implementation(Dependencies.koinAndroid)
+ implementation(libs.kotlinx.coroutines.android)
+ implementation(libs.kotlinx.serialization.json)
+ implementation(libs.koin)
+ implementation(libs.bundles.libsu)
+ implementation(Google.gson)
- testImplementation("junit:junit:4.+")
- androidTestImplementation("androidx.test.ext:junit:1.1.3")
- androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
+ testImplementation(libs.junit)
}
diff --git a/data/src/main/java/com/androidvip/sysctlgui/data/utils/RootUtils.kt b/data/src/main/java/com/androidvip/sysctlgui/data/utils/RootUtils.kt
index 54c0620..901304a 100644
--- a/data/src/main/java/com/androidvip/sysctlgui/data/utils/RootUtils.kt
+++ b/data/src/main/java/com/androidvip/sysctlgui/data/utils/RootUtils.kt
@@ -9,10 +9,9 @@ import kotlinx.coroutines.withContext
class RootUtils(private val dispatcher: CoroutineDispatcher = Dispatchers.Default) {
suspend fun isBusyboxAvailable(): Boolean = withContext(dispatcher) {
- val results: List = Shell.sh("which busybox").exec().out
- return@withContext if (ShellUtils.isValidOutput(results)) {
- results.first().isNotEmpty()
- } else false
+ val results: List = Shell.cmd("which busybox").exec().out
+ return@withContext ShellUtils.isValidOutput(results) && results.firstOrNull()
+ ?.isNotEmpty() == true
}
suspend fun executeWithOutput(
@@ -22,7 +21,7 @@ class RootUtils(private val dispatcher: CoroutineDispatcher = Dispatchers.Defaul
): String = withContext(dispatcher) {
return@withContext runCatching {
buildString {
- val outputs = Shell.su(command).exec().out
+ val outputs = Shell.cmd(command).exec().out
if (!ShellUtils.isValidOutput(outputs)) {
append(defaultOutput)
return@buildString
diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts
index 495951c..63160d8 100644
--- a/domain/build.gradle.kts
+++ b/domain/build.gradle.kts
@@ -1,14 +1,34 @@
plugins {
- id("java-library")
- id("kotlin")
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
}
-java {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
+android {
+ namespace = "${AppConfig.appId}.domain"
+ compileSdk = AppConfig.compileSdkVersion
+
+ defaultConfig {
+ minSdk = AppConfig.minSdkVersion
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = "17"
+ }
}
dependencies {
- implementation(Dependencies.koinCore)
-}
+ implementation(project(":common:utils"))
+ implementation(libs.androidx.core.ktx)
+
+ implementation(libs.koin)
+ testImplementation(libs.junit)
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..f41fe46
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,81 @@
+[versions]
+agp = "8.12.0"
+libsu = "6.0.0"
+jsoup = "1.21.1"
+kotlin = "2.2.0"
+kotlinxCoroutinesAndroid = "1.10.2"
+ksp = "2.2.0-2.0.2"
+coreKtx = "1.16.0"
+junit = "4.13.2"
+junitVersion = "1.3.0"
+espressoCore = "3.7.0"
+ktor = "3.2.3"
+lifecycle = "2.9.2"
+activityCompose = "1.10.1"
+composeBom = "2025.07.00"
+appcompat = "1.7.1"
+material = "1.8.3"
+materialIconsCore = "1.7.8"
+navigationCompose = "2.9.3"
+preference = "1.2.1"
+room = "2.7.2"
+nav3Lifecycle = "1.0.0-alpha03"
+material3 = "1.5.0-alpha01"
+kotlinxSerializationCore = "1.9.0"
+koin = "4.1.0"
+window = "1.4.0"
+
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-material = { module = "androidx.compose.material:material", version.ref = "material" }
+androidx-material-icons-core = { module = "androidx.compose.material:material-icons-core", version.ref = "materialIconsCore" }
+androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
+androidx-preference = { module = "androidx.preference:preference", version.ref = "preference" }
+androidx-window = { module = "androidx.window:window", version.ref = "window" }
+libsu-core = { module = "com.github.topjohnwu.libsu:core", version.ref = "libsu" }
+libsu-nio = { module = "com.github.topjohnwu.libsu:nio", version.ref = "libsu" }
+jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
+androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
+androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
+androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
+androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "nav3Lifecycle" }
+androidx-lifecycle-viewmodel-savedstate = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-savedstate", version.ref = "lifecycle" }
+androidx-lifecycle-compiler = { group = "androidx.lifecycle", name = "lifecycle-compiler", version.ref = "lifecycle" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
+androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
+androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
+kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
+kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationCore" }
+kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationCore" }
+koin = { module = "io.insert-koin:koin-android", version.ref = "koin" }
+koin-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" }
+ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" }
+ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
+ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
+
+[bundles]
+ktor-clients = ["ktor-client-core", "ktor-client-android", "ktor-client-logging"]
+libsu = ["libsu-core", "libsu-nio"]
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+android-library = { id = "com.android.library", version.ref = "agp" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
+jetbrains-kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"}
+ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 6372a16..24dffe3 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 1d3e2c5..8228a9a 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,5 +1,32 @@
+@file:Suppress("UnstableApiUsage")
+
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ maven {
+ url = uri("https://jitpack.io")
+ }
+ }
+}
+
+rootProject.name = "SysctlGUI"
include(":app")
include(":data")
include(":domain")
-include(":common:utils")
include(":common:design")
+include(":common:utils")
From 0d6ee55b768b50d397a040541ded7420f5a81742 Mon Sep 17 00:00:00 2001
From: Lennoard
Date: Mon, 11 Aug 2025 20:48:13 -0300
Subject: [PATCH 02/18] refactor: [WIP] updated domain layer
---
domain/consumer-rules.pro | 0
domain/proguard-rules.pro | 21 +++
.../sysctlgui/domain/StringProvider.kt | 12 ++
.../datasource/LocalDataSourceContract.kt | 10 --
.../datasource/RuntimeDataSourceContract.kt | 9 --
.../sysctlgui/domain/di/DomainModule.kt | 44 +++---
.../sysctlgui/domain/enums/CommitMode.kt | 27 ++++
.../sysctlgui/domain/enums/SettingItemType.kt | 27 ++++
.../domain/exceptions/ApplyValueException.kt | 18 +++
.../domain/exceptions/ExportExceptions.kt | 3 +
.../domain/exceptions/ImportExceptions.kt | 8 +-
.../sysctlgui/domain/models/AppSetting.kt | 31 ++++
.../domain/models/DomainKernelParam.kt | 69 ---------
.../sysctlgui/domain/models/KernelParam.kt | 133 ++++++++++++++++++
.../domain/models/KernelParamContract.kt | 10 --
.../domain/models/ParamDocumentation.kt | 16 +++
.../sysctlgui/domain/repository/AppPrefs.kt | 12 +-
.../repository/AppSettingsRepository.kt | 7 +
.../repository/DocumentationRepository.kt | 18 +++
.../domain/repository/ParamsRepository.kt | 82 ++++++++---
.../domain/repository/PresetRepository.kt | 44 ++++++
.../domain/repository/UserRepository.kt | 47 +++++++
.../domain/usecase/AddUserParamUseCase.kt | 14 --
.../domain/usecase/AddUserParamsUseCase.kt | 16 ++-
.../domain/usecase/ApplyParamUseCase.kt | 76 ++++++++++
.../domain/usecase/ApplyParamsUseCase.kt | 19 ---
.../domain/usecase/BackupParamsUseCase.kt | 8 +-
.../domain/usecase/ClearUserParamUseCase.kt | 4 +-
.../domain/usecase/ExportParamsUseCase.kt | 18 ++-
.../domain/usecase/GetAppSettingsUseCase.kt | 20 +++
.../domain/usecase/GetJsonParamsUseCase.kt | 7 -
.../usecase/GetParamDocumentationUseCase.kt | 15 ++
.../usecase/GetParamsFromFilesUseCase.kt | 7 +-
.../domain/usecase/GetRuntimeParamUseCase.kt | 25 ++++
.../domain/usecase/GetRuntimeParamsUseCase.kt | 16 ++-
.../usecase/GetUserParamByNameUseCase.kt | 7 +
.../domain/usecase/GetUserParamsUseCase.kt | 7 +-
.../domain/usecase/ImportParamsUseCase.kt | 45 ------
.../usecase/IsTaskerInstalledUseCase.kt | 27 ++++
.../PerformDatabaseMigrationUseCase.kt | 9 --
.../domain/usecase/RemoveUserParamUseCase.kt | 10 +-
.../domain/usecase/UpdateUserParamUseCase.kt | 14 --
.../domain/usecase/UpsertUserParamUseCase.kt | 26 ++++
43 files changed, 758 insertions(+), 280 deletions(-)
create mode 100644 domain/consumer-rules.pro
create mode 100644 domain/proguard-rules.pro
create mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/StringProvider.kt
delete mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/datasource/LocalDataSourceContract.kt
delete mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/datasource/RuntimeDataSourceContract.kt
create mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/enums/CommitMode.kt
create mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/enums/SettingItemType.kt
create mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/models/AppSetting.kt
delete mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/models/DomainKernelParam.kt
create mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/models/KernelParam.kt
delete mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/models/KernelParamContract.kt
create mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/models/ParamDocumentation.kt
create mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/repository/AppSettingsRepository.kt
create mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/repository/DocumentationRepository.kt
create mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/repository/PresetRepository.kt
create mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/repository/UserRepository.kt
delete mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/AddUserParamUseCase.kt
create mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/ApplyParamUseCase.kt
delete mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/ApplyParamsUseCase.kt
create mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetAppSettingsUseCase.kt
delete mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetJsonParamsUseCase.kt
create mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetParamDocumentationUseCase.kt
create mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetRuntimeParamUseCase.kt
create mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetUserParamByNameUseCase.kt
delete mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/ImportParamsUseCase.kt
create mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/IsTaskerInstalledUseCase.kt
delete mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/PerformDatabaseMigrationUseCase.kt
delete mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/UpdateUserParamUseCase.kt
create mode 100644 domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/UpsertUserParamUseCase.kt
diff --git a/domain/consumer-rules.pro b/domain/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/domain/proguard-rules.pro b/domain/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/domain/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/StringProvider.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/StringProvider.kt
new file mode 100644
index 0000000..b141cda
--- /dev/null
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/StringProvider.kt
@@ -0,0 +1,12 @@
+package com.androidvip.sysctlgui.domain
+
+import androidx.annotation.StringRes
+
+/**
+ * Provides access to string resources.
+ * This interface allows for fetching localized strings, potentially with formatting arguments.
+ */
+interface StringProvider {
+ fun getString(@StringRes resId: Int): String
+ fun getString(@StringRes resId: Int, vararg formatArgs: Any): String
+}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/datasource/LocalDataSourceContract.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/datasource/LocalDataSourceContract.kt
deleted file mode 100644
index 7a47fe0..0000000
--- a/domain/src/main/java/com/androidvip/sysctlgui/domain/datasource/LocalDataSourceContract.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.androidvip.sysctlgui.domain.datasource
-
-interface LocalDataSourceContract {
- suspend fun add(param: T, allowBlank: Boolean)
- suspend fun addAll(params: List, allowBlank: Boolean)
- suspend fun remove(param: T)
- suspend fun edit(param: T, allowBlank: Boolean)
- suspend fun clear()
- suspend fun getData(): List
-}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/datasource/RuntimeDataSourceContract.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/datasource/RuntimeDataSourceContract.kt
deleted file mode 100644
index 40d9ede..0000000
--- a/domain/src/main/java/com/androidvip/sysctlgui/domain/datasource/RuntimeDataSourceContract.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.androidvip.sysctlgui.domain.datasource
-
-import java.io.File
-
-interface RuntimeDataSourceContract {
- suspend fun edit(param: T, commitMode: String, useBusybox: Boolean, allowBlank: Boolean)
- suspend fun getData(useBusybox: Boolean): List
- suspend fun getParamsFromFiles(files: List): List
-}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/di/DomainModule.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/di/DomainModule.kt
index ca17635..dc7075a 100644
--- a/domain/src/main/java/com/androidvip/sysctlgui/domain/di/DomainModule.kt
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/di/DomainModule.kt
@@ -1,34 +1,38 @@
package com.androidvip.sysctlgui.domain.di
-import com.androidvip.sysctlgui.domain.usecase.AddUserParamUseCase
import com.androidvip.sysctlgui.domain.usecase.AddUserParamsUseCase
-import com.androidvip.sysctlgui.domain.usecase.ApplyParamsUseCase
+import com.androidvip.sysctlgui.domain.usecase.ApplyParamUseCase
import com.androidvip.sysctlgui.domain.usecase.BackupParamsUseCase
import com.androidvip.sysctlgui.domain.usecase.ClearUserParamUseCase
import com.androidvip.sysctlgui.domain.usecase.ExportParamsUseCase
-import com.androidvip.sysctlgui.domain.usecase.GetJsonParamsUseCase
+import com.androidvip.sysctlgui.domain.usecase.GetAppSettingsUseCase
+import com.androidvip.sysctlgui.domain.usecase.GetParamDocumentationUseCase
import com.androidvip.sysctlgui.domain.usecase.GetParamsFromFilesUseCase
+import com.androidvip.sysctlgui.domain.usecase.GetRuntimeParamUseCase
import com.androidvip.sysctlgui.domain.usecase.GetRuntimeParamsUseCase
+import com.androidvip.sysctlgui.domain.usecase.GetUserParamByNameUseCase
import com.androidvip.sysctlgui.domain.usecase.GetUserParamsUseCase
-import com.androidvip.sysctlgui.domain.usecase.ImportParamsUseCase
-import com.androidvip.sysctlgui.domain.usecase.PerformDatabaseMigrationUseCase
+import com.androidvip.sysctlgui.domain.usecase.IsTaskerInstalledUseCase
import com.androidvip.sysctlgui.domain.usecase.RemoveUserParamUseCase
-import com.androidvip.sysctlgui.domain.usecase.UpdateUserParamUseCase
+import com.androidvip.sysctlgui.domain.usecase.UpsertUserParamUseCase
+import org.koin.android.ext.koin.androidContext
+import org.koin.core.module.dsl.factoryOf
import org.koin.dsl.module
val domainModule = module {
- factory { AddUserParamsUseCase(get(), get()) }
- factory { AddUserParamUseCase(get(), get()) }
- factory { ApplyParamsUseCase(get(), get()) }
- factory { ClearUserParamUseCase(get()) }
- factory { GetJsonParamsUseCase(get()) }
- factory { GetParamsFromFilesUseCase(get()) }
- factory { GetUserParamsUseCase(get()) }
- factory { GetRuntimeParamsUseCase(get(), get()) }
- factory { PerformDatabaseMigrationUseCase(get()) }
- factory { RemoveUserParamUseCase(get()) }
- factory { UpdateUserParamUseCase(get(), get()) }
- factory { ImportParamsUseCase(get(), get(), get(), get()) }
- factory { BackupParamsUseCase(get(), get()) }
- factory { ExportParamsUseCase(get(), get()) }
+ factoryOf(::AddUserParamsUseCase)
+ factoryOf(::ApplyParamUseCase)
+ factoryOf(::ClearUserParamUseCase)
+ factoryOf(::GetParamsFromFilesUseCase)
+ factoryOf(::GetUserParamsUseCase)
+ factoryOf(::GetRuntimeParamsUseCase)
+ factoryOf(::GetRuntimeParamUseCase)
+ factoryOf(::GetUserParamByNameUseCase)
+ factoryOf(::RemoveUserParamUseCase)
+ factoryOf(::UpsertUserParamUseCase)
+ factoryOf(::BackupParamsUseCase)
+ factoryOf(::ExportParamsUseCase)
+ factoryOf(::GetAppSettingsUseCase)
+ factoryOf(::GetParamDocumentationUseCase)
+ factory { IsTaskerInstalledUseCase(androidContext()) }
}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/enums/CommitMode.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/enums/CommitMode.kt
new file mode 100644
index 0000000..6b4b178
--- /dev/null
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/enums/CommitMode.kt
@@ -0,0 +1,27 @@
+package com.androidvip.sysctlgui.domain.enums
+
+/**
+ * Defines the method used to commit kernel parameter changes.
+ */
+enum class CommitMode {
+ /**
+ * Commits the value using the `sysctl -w` command.
+ * This is the default mode.
+ */
+ SYSCTL,
+ /**
+ * Commits the value to the file using `echo` command.
+ * This method is generally safer and more reliable.
+ */
+ ECHO;
+
+ companion object {
+ fun parse(value: String): CommitMode {
+ return when (value) {
+ "sysctl" -> SYSCTL
+ "echo" -> ECHO
+ else -> SYSCTL
+ }
+ }
+ }
+}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/enums/SettingItemType.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/enums/SettingItemType.kt
new file mode 100644
index 0000000..52a17e9
--- /dev/null
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/enums/SettingItemType.kt
@@ -0,0 +1,27 @@
+package com.androidvip.sysctlgui.domain.enums
+
+/**
+ * Represents the different types of settings component that can be displayed in the UI.
+ * Each type corresponds to a specific UI element used to interact with the setting.
+ */
+enum class SettingItemType {
+ /**
+ * Simple setting header with no behavior
+ */
+ Text,
+
+ /**
+ * Represents a switch setting component that can be toggled on or off.
+ */
+ Switch,
+
+ /**
+ * Represents a list of options that can be selected from.
+ */
+ List,
+
+ /**
+ * Represents a slider component that allows the user to select a value within a range.
+ */
+ Slider
+}
\ No newline at end of file
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/exceptions/ApplyValueException.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/exceptions/ApplyValueException.kt
index dc60125..dd90a5f 100644
--- a/domain/src/main/java/com/androidvip/sysctlgui/domain/exceptions/ApplyValueException.kt
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/exceptions/ApplyValueException.kt
@@ -1,4 +1,22 @@
package com.androidvip.sysctlgui.domain.exceptions
+// TODO: Use sealed classes instead of exceptions
+/**
+ * Exception thrown when a value commit fails or refuses to be applied (value remains the same)
+ */
class ApplyValueException(message: String) : Exception(message)
+
+/**
+ * Exception thrown when a value commit fails and the commit mode is "sysctl"
+ */
class CommitModeException(message: String) : Exception(message)
+
+/**
+ * Exception thrown when a value to be committed is blank and blank values are not allowed
+ */
+class BlankValueNotAllowedException() : IllegalArgumentException()
+
+/**
+ * Exception thrown when a shell command fails
+ */
+class ShellCommandException(message: String, cause: Throwable) : Exception(message, cause)
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/exceptions/ExportExceptions.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/exceptions/ExportExceptions.kt
index 3d774bd..f4ced82 100644
--- a/domain/src/main/java/com/androidvip/sysctlgui/domain/exceptions/ExportExceptions.kt
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/exceptions/ExportExceptions.kt
@@ -1,3 +1,6 @@
package com.androidvip.sysctlgui.domain.exceptions
+/**
+ * Exception thrown when no parameter is found for a given kernel file path.
+ */
class NoParameterFoundException : Exception()
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/exceptions/ImportExceptions.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/exceptions/ImportExceptions.kt
index 2e29597..1c5ca30 100644
--- a/domain/src/main/java/com/androidvip/sysctlgui/domain/exceptions/ImportExceptions.kt
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/exceptions/ImportExceptions.kt
@@ -1,6 +1,12 @@
package com.androidvip.sysctlgui.domain.exceptions
class InvalidFileExtensionException : Exception()
+/**
+ * Thrown when an imported file is empty
+ */
class EmptyFileException : Exception()
-class MalformedLineException : Exception()
+/**
+ * Thrown when an invalid line is found during import.
+ */
+class MalformedLineException(message: String, cause: Throwable? = null) : Exception(message, cause)
class NoValidParamException : Exception()
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/models/AppSetting.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/models/AppSetting.kt
new file mode 100644
index 0000000..7754d4a
--- /dev/null
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/models/AppSetting.kt
@@ -0,0 +1,31 @@
+package com.androidvip.sysctlgui.domain.models
+
+import com.androidvip.sysctlgui.domain.enums.SettingItemType
+
+
+/**
+ * Represents an application setting.
+ *
+ * This data class encapsulates the properties of a single application setting,
+ * including its key, current value, enabled state, display information, and type.
+ *
+ * @param T The type of the setting's value.
+ * @property key A unique identifier for the setting.
+ * @property value The current value of the setting.
+ * @property enabled Indicates whether the setting is currently active or can be modified. Defaults to `true`.
+ * @property title A user-friendly name for the setting, displayed in the UI.
+ * @property description An optional detailed explanation of what the setting does. Defaults to `null`.
+ * @property category The group or section this setting belongs to, used for organization in the UI.
+ * @property type Defines how the setting is presented and interacted with in the UI (e.g., switch, list).
+ * @property values An optional list of possible values for the setting, typically used for dropdowns or selection lists.
+ */
+data class AppSetting(
+ val key: String,
+ val value: T,
+ val enabled: Boolean = true,
+ val title: String,
+ val description: String? = null,
+ val category: String,
+ val type: SettingItemType,
+ val values: List? = null
+)
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/models/DomainKernelParam.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/models/DomainKernelParam.kt
deleted file mode 100644
index 4ae163a..0000000
--- a/domain/src/main/java/com/androidvip/sysctlgui/domain/models/DomainKernelParam.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-package com.androidvip.sysctlgui.domain.models
-
-open class DomainKernelParam(
- open var id: Int = 0,
- open var name: String = "",
- open var path: String = "",
- open var value: String = "",
- open var favorite: Boolean = false,
- open var taskerParam: Boolean = false,
- open var taskerList: Int = LIST_NUMBER_PRIMARY_TASKER
-) : KernelParamContract {
- override val shortName: String get() = name.split(".").last()
-
- val configName: String get() = name.removeSuffix(shortName).removeSuffix(".")
-
- override fun setNameFromPath(path: String) {
- if (path.trim().isEmpty() || !path.startsWith(PROC_SYS)) return
- if (path.contains(".")) return
-
- name = path.removeSuffix("/")
- .removePrefix(PROC_SYS)
- .replace("/", ".")
- .removePrefix(".")
- }
-
- override fun setPathFromName(kernelParam: String) {
- if (kernelParam.trim().isEmpty() || kernelParam.contains("/")) return
- if (kernelParam.startsWith(".") || kernelParam.endsWith(".")) return
-
- path = "$PROC_SYS/${kernelParam.replace(".", "/")}"
- }
-
- override fun hasValidPath(): Boolean {
- if (path.trim().isEmpty() || !path.startsWith(PROC_SYS)) return false
- if (path.contains(".")) return false
-
- return true
- }
-
- override fun hasValidName(): Boolean {
- if (name.trim().isEmpty() || name.contains("/")) return false
- if (name.startsWith(".") || name.endsWith(".")) return false
-
- return true
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as DomainKernelParam
- if (name != other.name) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- return name.hashCode()
- }
-
- override fun toString(): String {
- return "$name = $value"
- }
-
- companion object {
- private const val PROC_SYS = "/proc/sys"
- const val LIST_NUMBER_PRIMARY_TASKER: Int = 0
- }
-}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/models/KernelParam.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/models/KernelParam.kt
new file mode 100644
index 0000000..4128970
--- /dev/null
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/models/KernelParam.kt
@@ -0,0 +1,133 @@
+package com.androidvip.sysctlgui.domain.models
+
+import com.androidvip.sysctlgui.utils.Consts
+
+/**
+ * Represents a kernel parameter.
+ */
+open class KernelParam(
+ /**
+ * The name of the kernel parameter (e.g., "vm.swappiness")
+ */
+ open val name: String,
+
+ /**
+ * The path of the kernel parameter (e.g., "/proc/sys/vm/swappiness")
+ */
+ open val path: String,
+
+ /**
+ * The value of the kernel parameter (e.g., "60")
+ */
+ open val value: String,
+
+ /**
+ * Indicates whether the parameter is marked as a favorite by the user.
+ */
+ open val isFavorite: Boolean = false,
+
+ /**
+ * Indicates whether the parameter is used in a Tasker profile
+ */
+ open val isTaskerParam: Boolean = false,
+
+ /**
+ * Indicates the Tasker list number (primary or secondary)
+ */
+ open val taskerList: Int = Consts.LIST_NUMBER_INVALID,
+) {
+
+ open val lastNameSegment: String
+ get() = name.substringAfterLast('.', name)
+
+ /**
+ * The configuration part of the name, excluding the lastNameSegment.
+ * For example, for `vm.swappiness`, configName would be `vm`.
+ */
+ open val groupName: String
+ get() = name.substringBeforeLast('.', "")
+
+ /**
+ * Checks if the [path] is valid for a kernel parameter.
+ */
+ fun hasValidPath() = path.isKernelPathValid()
+
+ /**
+ * Checks if the [name] is valid for a kernel parameter.
+ */
+ fun hasValidName() = name.isKernelNameValid()
+
+ companion object {
+
+ /**
+ * Creates a new instance with its `path` derived from a given `newName`.
+ * Example: If `newName` is "vm.swappiness", the derived path will be "/proc/sys/vm/swappiness".
+ *
+ * @param name The name to derive the path from. It must be a valid kernel parameter name.
+ * @return A new [KernelParam] instance with the derived path.
+ * @throws IllegalArgumentException if `newName` is not a valid kernel parameter name.
+ */
+ fun createFromName(
+ name: String,
+ value: String,
+ isFavorite: Boolean = false
+ ): KernelParam {
+ require(name.isKernelNameValid()) { "Invalid name: $name" }
+ val derivedPath = "${Consts.PROC_SYS}/${name.replace(".", "/")}"
+ return KernelParam(name, value, derivedPath, isFavorite)
+ }
+
+ /**
+ * Creates a [KernelParam] instance from a given path and value.
+ * The name is derived from the path.
+ * For example, for `/proc/sys/vm/swappiness/`, the derived name will be `vm.swappiness`.
+ *
+ * @param path The path of the kernel parameter (e.g., "/proc/sys/vm/swappiness").
+ * It must be a valid path as defined by [isKernelPathValid].
+ * @param value The value of the kernel parameter (e.g., "60").
+ * @return A new [KernelParam] instance.
+ * @throws IllegalArgumentException if the provided [path] is invalid.
+ */
+ fun createFromPath(path: String, value: String): KernelParam {
+ require(path.isKernelPathValid()) { "Invalid path: $path" }
+
+ val derivedName = path.removeSuffix("/")
+ .removePrefix(Consts.PROC_SYS)
+ .replace("/", ".")
+ .removePrefix(".")
+
+ return KernelParam(derivedName, value, path)
+ }
+ }
+}
+
+/**
+ * Checks if the path of this kernel parameter is valid.
+ * A path is considered valid if:
+ * - It is not empty after trimming whitespace.
+ * - It starts with [Consts.PROC_SYS].
+ * - It does not contain any "." characters (as paths use "/" as separators).
+ *
+ * @return `true` if the path is valid, `false` otherwise.
+ */
+private fun String.isKernelPathValid(): Boolean {
+ if (this.trim().isEmpty() || !this.startsWith(Consts.PROC_SYS)) return false
+ if (this.contains(".")) return false
+ return true
+}
+
+/**
+ * Checks if a string is a valid kernel parameter name.
+ * A valid name:
+ * - Is not empty or blank.
+ * - Does not contain forward slashes ('/').
+ * - Does not start or end with a dot ('.').
+ *
+ * @return `true` if the string is a valid name, `false` otherwise.
+ */
+private fun String.isKernelNameValid(): Boolean {
+ if (this.trim().isEmpty() || this.contains("/")) return false
+ if (this.startsWith(".") || this.endsWith(".")) return false
+ return true
+}
+
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/models/KernelParamContract.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/models/KernelParamContract.kt
deleted file mode 100644
index 0affdb3..0000000
--- a/domain/src/main/java/com/androidvip/sysctlgui/domain/models/KernelParamContract.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.androidvip.sysctlgui.domain.models
-
-interface KernelParamContract {
- val shortName: String
-
- fun setNameFromPath(path: String)
- fun setPathFromName(kernelParam: String)
- fun hasValidPath(): Boolean
- fun hasValidName(): Boolean
-}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/models/ParamDocumentation.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/models/ParamDocumentation.kt
new file mode 100644
index 0000000..671d7af
--- /dev/null
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/models/ParamDocumentation.kt
@@ -0,0 +1,16 @@
+package com.androidvip.sysctlgui.domain.models
+
+/**
+ * Represents documentation for a kernel parameter.
+ *
+ * @property title The title of the documentation.
+ * @property documentationText The plain text documentation.
+ * @property documentationHtml The HTML formatted documentation, if available.
+ * @property url The URL to the online documentation, if available.
+ */
+data class ParamDocumentation(
+ val title: String = "",
+ val documentationText: String = "",
+ val documentationHtml: String? = null,
+ val url: String? = null
+)
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/repository/AppPrefs.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/repository/AppPrefs.kt
index b2dc3e5..251b8cc 100644
--- a/domain/src/main/java/com/androidvip/sysctlgui/domain/repository/AppPrefs.kt
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/repository/AppPrefs.kt
@@ -1,5 +1,11 @@
package com.androidvip.sysctlgui.domain.repository
+/**
+ * Interface for accessing and modifying application preferences.
+ *
+ * This interface defines the contract for interacting with the application's settings,
+ * allowing various parts of the app to read and write preference values.
+ */
interface AppPrefs {
var listFoldersFirst: Boolean
var guessInputType: Boolean
@@ -9,8 +15,12 @@ interface AppPrefs {
var runOnStartUp: Boolean
var startUpDelay: Int
var showTaskerToast: Boolean
- var migrationCompleted: Boolean
var forceDark: Boolean
var dynamicColors: Boolean
var askedForNotificationPermission: Boolean
+ var useOnlineDocs: Boolean
+ var contrastLevel: Int
+ val searchHistory: Set
+ fun addSearchToHistory(query: String)
+ fun removeSearchFromHistory(query: String)
}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/repository/AppSettingsRepository.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/repository/AppSettingsRepository.kt
new file mode 100644
index 0000000..ae0f1ed
--- /dev/null
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/repository/AppSettingsRepository.kt
@@ -0,0 +1,7 @@
+package com.androidvip.sysctlgui.domain.repository
+
+import com.androidvip.sysctlgui.domain.models.AppSetting
+
+fun interface AppSettingsRepository {
+ suspend fun getAppSettings(): List>
+}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/repository/DocumentationRepository.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/repository/DocumentationRepository.kt
new file mode 100644
index 0000000..dd28609
--- /dev/null
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/repository/DocumentationRepository.kt
@@ -0,0 +1,18 @@
+package com.androidvip.sysctlgui.domain.repository
+
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import com.androidvip.sysctlgui.domain.models.ParamDocumentation
+
+/**
+ * Repository interface for fetching documentation for kernel parameters.
+ */
+fun interface DocumentationRepository {
+ /**
+ * Retrieves documentation for a given kernel parameter.
+ *
+ * @param param The kernel parameter for which to fetch documentation.
+ * @param online Whether to use the online documentation source.
+ * @return The documentation if found, null otherwise.
+ */
+ suspend fun getDocumentation(param: KernelParam, online: Boolean): ParamDocumentation?
+}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/repository/ParamsRepository.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/repository/ParamsRepository.kt
index 81bf7aa..79849aa 100644
--- a/domain/src/main/java/com/androidvip/sysctlgui/domain/repository/ParamsRepository.kt
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/repository/ParamsRepository.kt
@@ -1,33 +1,69 @@
package com.androidvip.sysctlgui.domain.repository
-import com.androidvip.sysctlgui.domain.models.DomainKernelParam
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import com.androidvip.sysctlgui.domain.enums.CommitMode
+import kotlinx.coroutines.flow.Flow
import java.io.File
-import java.io.FileDescriptor
-import java.io.InputStream
+/**
+ * Repository interface for managing kernel parameters.
+ */
interface ParamsRepository {
- suspend fun getUserParams(): List
- suspend fun getJsonParams(): List
- suspend fun getRuntimeParams(useBusybox: Boolean): List
- suspend fun getParamsFromFiles(files: List): List
-
- suspend fun applyParam(
- param: DomainKernelParam,
- commitMode: String,
+ /**
+ * Gets all available kernel parameters at runtime.
+ *
+ * @param useBusybox whether to use busybox or not.
+ * @param userParams optional user params list to be merged with runtime params.
+ * @return a [List] of [KernelParam]s.
+ */
+ fun getRuntimeParams(
useBusybox: Boolean,
- allowBlank: Boolean
- )
- suspend fun updateUserParam(param: DomainKernelParam, allowBlank: Boolean)
+ userParams: List = emptyList()
+ ): Flow>
+
+ /**
+ * Gets a kernel parameter value at runtime.
+ *
+ * @param paramName the name of the parameter to get, in the group.name format:
+ * - **vm.admin_reserve_kbytes (OK ✅)**
+ * - admin_reserve_kbytes (NO ❌)
+ * - vm/admin_reserve_kbytes (NO ❌)
+ * - /proc/sys/vm/admin_reserve_kbytes (NO ❌)
+ * @param useBusybox whether to use busybox or not.
+ * @return the [KernelParam] or null if not found or an error occurred.
+ */
+ suspend fun getRuntimeParam(paramName: String, useBusybox: Boolean): KernelParam?
+
+ /**
+ * Sets the value of a kernel parameter at runtime.
+ * @param param The [KernelParam] object representing the kernel parameter to be set.
+ * @param commitMode The commit mode to use when setting the parameter.
+ * @param useBusybox Whether to use busybox or not.
+ */
+ suspend fun setRuntimeParam(
+ param: KernelParam,
+ commitMode: CommitMode,
+ useBusybox: Boolean
+ ): String
- suspend fun addUserParam(param: DomainKernelParam, allowBlank: Boolean)
- suspend fun addUserParams(params: List, allowBlank: Boolean)
- suspend fun removeUserParam(param: DomainKernelParam)
- suspend fun clearUserParams()
+ /**
+ * Reads kernel parameters from a list of files.
+ * Each file is expected to contain a single line with the parameter value.
+ *
+ * @param files A list of [File] objects representing the files to read parameters from.
+ * @return A [Flow] emitting a list of [KernelParam] objects.
+ */
+ fun getParamsFromFiles(files: List): Flow>
- suspend fun performDatabaseMigration()
+ /**
+ * Gets a list of kernel parameters from the given [path].
+ *
+ * @param path The path to search for kernel parameters.
+ * @return A list of [KernelParam] objects found in the given path.
+ */
+ fun getParamsFromPath(path: String): Flow>
- suspend fun importParamsFromJson(stream: InputStream): List
- suspend fun importParamsFromConf(stream: InputStream): List
- suspend fun exportParams(params: List, fileDescriptor: FileDescriptor)
- suspend fun backupParams(params: List, fileDescriptor: FileDescriptor)
+ companion object {
+ const val DEFAULT_ERROR_MESSAGE = "error"
+ }
}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/repository/PresetRepository.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/repository/PresetRepository.kt
new file mode 100644
index 0000000..dabc145
--- /dev/null
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/repository/PresetRepository.kt
@@ -0,0 +1,44 @@
+package com.androidvip.sysctlgui.domain.repository
+
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import java.io.FileDescriptor
+import java.io.InputStream
+
+
+/**
+ * Interface defining operations for managing kernel parameter presets.
+ * This interface provides methods to read and write kernel parameter presets,
+ * allowing users to save and load configurations.
+ */
+interface PresetRepository {
+ /**
+ * Reads a preset of kernel parameters from an input stream.
+ *
+ * This function attempts to determine if the input stream contains JSON or CONF formatted data
+ * and parses it accordingly.
+ *
+ * @param stream The input stream containing the kernel parameter preset.
+ * @return A list of [KernelParam] objects parsed from the stream.
+ * @throws IllegalArgumentException if the stream format cannot be determined or if parsing fails.
+ */
+ suspend fun readPreset(stream: InputStream): List
+
+ /**
+ * Exports a list of kernel parameters to a preset file.
+ *
+ * This function writes the provided kernel parameters to a specified file descriptor,
+ * typically for creating a user-defined preset.
+ *
+ * @param params The list of [KernelParam] objects to export.
+ * @param fileDescriptor The `FileDescriptor` of the file to write the parameters to.
+ */
+ suspend fun exportToPreset(params: List, fileDescriptor: FileDescriptor)
+
+ /**
+ * Backs up a list of kernel parameters.
+ *
+ * @param params The list of `KernelParam` objects to backup.
+ * @param fileDescriptor The `FileDescriptor` of the file to write the backup to.
+ */
+ suspend fun backupParams(params: List, fileDescriptor: FileDescriptor)
+}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/repository/UserRepository.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/repository/UserRepository.kt
new file mode 100644
index 0000000..b577cdc
--- /dev/null
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/repository/UserRepository.kt
@@ -0,0 +1,47 @@
+package com.androidvip.sysctlgui.domain.repository
+
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Interface for managing user-specific kernel parameters.
+ */
+interface UserRepository {
+ /**
+ * Retrieves a [Flow] that emits a list of user-configurable kernel parameters.
+ * The [Flow] will emit a new list whenever the underlying data changes.
+ */
+ val userParams: Flow>
+
+ suspend fun getParamByName(name: String): KernelParam?
+
+ /**
+ * Inserts or updates a user-configurable kernel parameter.
+ * If a parameter with the same ID already exists, it will be updated.
+ * Otherwise, a new parameter will be inserted.
+ *
+ * @param param The [KernelParam] to upsert.
+ * @return The row ID of the inserted or updated parameter.
+ */
+ suspend fun upsertUserParam(param: KernelParam): Long
+
+ /**
+ * Adds a list of kernel parameters to the list of user-configurable parameters.
+ *
+ * @param params The list of [KernelParam] objects to be added.
+ * @return A list of Long values representing the row IDs of the newly inserted parameters.
+ */
+ suspend fun upsertUserParams(params: List): List
+
+ /**
+ * Removes a kernel parameter from the list of user-configurable parameters.
+ * @param param The [KernelParam] to be removed.
+ * @return The number of rows deleted.
+ */
+ suspend fun removeUserParam(param: KernelParam): Int
+
+ /**
+ * Clears all user-configurable kernel parameters.
+ */
+ suspend fun clearUserParams()
+}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/AddUserParamUseCase.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/AddUserParamUseCase.kt
deleted file mode 100644
index 2ad4552..0000000
--- a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/AddUserParamUseCase.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.androidvip.sysctlgui.domain.usecase
-
-import com.androidvip.sysctlgui.domain.models.DomainKernelParam
-import com.androidvip.sysctlgui.domain.repository.AppPrefs
-import com.androidvip.sysctlgui.domain.repository.ParamsRepository
-
-class AddUserParamUseCase(
- private val repository: ParamsRepository,
- private val appPrefs: AppPrefs
-) {
- suspend operator fun invoke(param: DomainKernelParam) {
- return repository.addUserParam(param, appPrefs.allowBlankValues)
- }
-}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/AddUserParamsUseCase.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/AddUserParamsUseCase.kt
index dbd5d6f..d723500 100644
--- a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/AddUserParamsUseCase.kt
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/AddUserParamsUseCase.kt
@@ -1,14 +1,20 @@
package com.androidvip.sysctlgui.domain.usecase
-import com.androidvip.sysctlgui.domain.models.DomainKernelParam
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import com.androidvip.sysctlgui.domain.exceptions.BlankValueNotAllowedException
import com.androidvip.sysctlgui.domain.repository.AppPrefs
-import com.androidvip.sysctlgui.domain.repository.ParamsRepository
+import com.androidvip.sysctlgui.domain.repository.UserRepository
class AddUserParamsUseCase(
- private val repository: ParamsRepository,
+ private val repository: UserRepository,
private val appPrefs: AppPrefs
) {
- suspend operator fun invoke(params: List) {
- return repository.addUserParams(params, appPrefs.allowBlankValues)
+ suspend operator fun invoke(params: List): List {
+ if (!appPrefs.allowBlankValues) {
+ if (params.any { it.value.isBlank() }) {
+ throw BlankValueNotAllowedException()
+ }
+ }
+ return repository.upsertUserParams(params)
}
}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/ApplyParamUseCase.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/ApplyParamUseCase.kt
new file mode 100644
index 0000000..1001864
--- /dev/null
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/ApplyParamUseCase.kt
@@ -0,0 +1,76 @@
+package com.androidvip.sysctlgui.domain.usecase
+
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import com.androidvip.sysctlgui.domain.enums.CommitMode
+import com.androidvip.sysctlgui.domain.exceptions.ApplyValueException
+import com.androidvip.sysctlgui.domain.exceptions.BlankValueNotAllowedException
+import com.androidvip.sysctlgui.domain.exceptions.CommitModeException
+import com.androidvip.sysctlgui.domain.exceptions.ShellCommandException
+import com.androidvip.sysctlgui.domain.repository.AppPrefs
+import com.androidvip.sysctlgui.domain.repository.ParamsRepository
+
+class ApplyParamUseCase(
+ private val repository: ParamsRepository,
+ private val appPrefs: AppPrefs
+) {
+ suspend operator fun invoke(param: KernelParam) {
+ if (param.value.isBlank() && !appPrefs.allowBlankValues) {
+ throw BlankValueNotAllowedException()
+ }
+
+ val commitMode = CommitMode.parse(appPrefs.commitMode)
+
+ try {
+ val output = repository.setRuntimeParam(
+ param = param,
+ commitMode = commitMode,
+ useBusybox = appPrefs.useBusybox,
+ )
+ when (commitMode) {
+ CommitMode.SYSCTL -> {
+ if (!output.contains(param.name)) {
+ throw CommitModeException(
+ "Sysctl command for '${param.name}' executed, but output did not confirm the change. " +
+ "Output: '$output'. Try using '${CommitMode.ECHO}' mode."
+ )
+ }
+ }
+
+ CommitMode.ECHO -> {
+ if (output.isEmpty().not()) {
+ throw CommitModeException(
+ "Echo command for '${param.path}' executed, but output was not empty. " +
+ "Output: '$output'. Try using '${CommitMode.SYSCTL}' mode."
+ )
+ }
+ }
+ }
+
+ } catch (e: ShellCommandException) {
+ val message = e.cause?.message.orEmpty()
+ throwApplyValueException(
+ message = "$message <- ${e.message}",
+ commitMode = commitMode,
+ param = param
+ )
+ } catch (e: Exception) {
+ throwApplyValueException(
+ message = e.message.orEmpty(),
+ commitMode = commitMode,
+ param = param
+ )
+ }
+ }
+
+ private fun throwApplyValueException(
+ message: String,
+ commitMode: CommitMode,
+ param: KernelParam
+ ) {
+ val errorMessage = when (commitMode) {
+ CommitMode.SYSCTL -> "Failed to execute sysctl command for '${param.name}'"
+ CommitMode.ECHO -> "Failed to write value '${param.value}' to '${param.path}'"
+ }
+ throw ApplyValueException(errorMessage)
+ }
+}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/ApplyParamsUseCase.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/ApplyParamsUseCase.kt
deleted file mode 100644
index a8b3284..0000000
--- a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/ApplyParamsUseCase.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.androidvip.sysctlgui.domain.usecase
-
-import com.androidvip.sysctlgui.domain.models.DomainKernelParam
-import com.androidvip.sysctlgui.domain.repository.AppPrefs
-import com.androidvip.sysctlgui.domain.repository.ParamsRepository
-
-class ApplyParamsUseCase(
- private val repository: ParamsRepository,
- private val appPrefs: AppPrefs
-) {
- suspend operator fun invoke(param: DomainKernelParam) {
- return repository.applyParam(
- param,
- appPrefs.commitMode,
- appPrefs.useBusybox,
- appPrefs.allowBlankValues
- )
- }
-}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/BackupParamsUseCase.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/BackupParamsUseCase.kt
index e6dff15..372acd7 100644
--- a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/BackupParamsUseCase.kt
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/BackupParamsUseCase.kt
@@ -1,15 +1,15 @@
package com.androidvip.sysctlgui.domain.usecase
import com.androidvip.sysctlgui.domain.exceptions.NoParameterFoundException
-import com.androidvip.sysctlgui.domain.repository.ParamsRepository
+import com.androidvip.sysctlgui.domain.repository.PresetRepository
import java.io.FileDescriptor
class BackupParamsUseCase(
- private val getRuntimeParamsUseCase: GetRuntimeParamsUseCase,
- private val repository: ParamsRepository
+ private val getRuntimeParams: GetRuntimeParamsUseCase,
+ private val repository: PresetRepository
) {
suspend operator fun invoke(fileDescriptor: FileDescriptor) {
- val params = getRuntimeParamsUseCase()
+ val params = getRuntimeParams()
if (params.isEmpty()) throw NoParameterFoundException()
return repository.backupParams(params, fileDescriptor)
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/ClearUserParamUseCase.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/ClearUserParamUseCase.kt
index 0d70a25..fb3eea2 100644
--- a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/ClearUserParamUseCase.kt
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/ClearUserParamUseCase.kt
@@ -1,7 +1,7 @@
package com.androidvip.sysctlgui.domain.usecase
-import com.androidvip.sysctlgui.domain.repository.ParamsRepository
+import com.androidvip.sysctlgui.domain.repository.UserRepository
-class ClearUserParamUseCase(private val repository: ParamsRepository) {
+class ClearUserParamUseCase(private val repository: UserRepository) {
suspend operator fun invoke() = repository.clearUserParams()
}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/ExportParamsUseCase.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/ExportParamsUseCase.kt
index 7179b4a..0d32551 100644
--- a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/ExportParamsUseCase.kt
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/ExportParamsUseCase.kt
@@ -1,17 +1,25 @@
package com.androidvip.sysctlgui.domain.usecase
import com.androidvip.sysctlgui.domain.exceptions.NoParameterFoundException
-import com.androidvip.sysctlgui.domain.repository.ParamsRepository
+import com.androidvip.sysctlgui.domain.repository.PresetRepository
import java.io.FileDescriptor
+/**
+ * Exports the current user parameters to a preset file.
+ *
+ * @throws NoParameterFoundException if there are no parameters to export.
+ */
class ExportParamsUseCase(
- private val getUserParamUseCase: GetUserParamsUseCase,
- private val repository: ParamsRepository
+ private val getUserParams: GetUserParamsUseCase,
+ private val repository: PresetRepository
) {
+ /**
+ * @param fileDescriptor The file descriptor to write the preset to.
+ */
suspend operator fun invoke(fileDescriptor: FileDescriptor) {
- val params = getUserParamUseCase()
+ val params = getUserParams()
if (params.isEmpty()) throw NoParameterFoundException()
- return repository.exportParams(params, fileDescriptor)
+ return repository.exportToPreset(params, fileDescriptor)
}
}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetAppSettingsUseCase.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetAppSettingsUseCase.kt
new file mode 100644
index 0000000..32ff388
--- /dev/null
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetAppSettingsUseCase.kt
@@ -0,0 +1,20 @@
+package com.androidvip.sysctlgui.domain.usecase
+
+import com.androidvip.sysctlgui.domain.models.AppSetting
+import com.androidvip.sysctlgui.domain.repository.AppSettingsRepository
+
+/**
+ * Use case for retrieving app settings.
+ *
+ * This class provides a way to fetch app settings, optionally filtering them based on a
+ * provided predicate.
+ *
+ * @property repository The [AppSettingsRepository] used to access app settings data.
+ */
+class GetAppSettingsUseCase(private val repository: AppSettingsRepository) {
+ suspend operator fun invoke(
+ filterPredicate: (AppSetting<*>) -> Boolean = { true }
+ ): List> {
+ return repository.getAppSettings().filter(filterPredicate)
+ }
+}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetJsonParamsUseCase.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetJsonParamsUseCase.kt
deleted file mode 100644
index 157dc69..0000000
--- a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetJsonParamsUseCase.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.androidvip.sysctlgui.domain.usecase
-
-import com.androidvip.sysctlgui.domain.repository.ParamsRepository
-
-class GetJsonParamsUseCase(private val repository: ParamsRepository) {
- suspend operator fun invoke() = repository.getJsonParams()
-}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetParamDocumentationUseCase.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetParamDocumentationUseCase.kt
new file mode 100644
index 0000000..021a2cc
--- /dev/null
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetParamDocumentationUseCase.kt
@@ -0,0 +1,15 @@
+package com.androidvip.sysctlgui.domain.usecase
+
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import com.androidvip.sysctlgui.domain.models.ParamDocumentation
+import com.androidvip.sysctlgui.domain.repository.AppPrefs
+import com.androidvip.sysctlgui.domain.repository.DocumentationRepository
+
+class GetParamDocumentationUseCase(
+ private val repository: DocumentationRepository,
+ private val appPrefs: AppPrefs
+) {
+ suspend operator fun invoke(param: KernelParam): ParamDocumentation? {
+ return repository.getDocumentation(param, appPrefs.useOnlineDocs)
+ }
+}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetParamsFromFilesUseCase.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetParamsFromFilesUseCase.kt
index 3c3347e..4a91398 100644
--- a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetParamsFromFilesUseCase.kt
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetParamsFromFilesUseCase.kt
@@ -1,11 +1,12 @@
package com.androidvip.sysctlgui.domain.usecase
-import com.androidvip.sysctlgui.domain.models.DomainKernelParam
+import com.androidvip.sysctlgui.domain.models.KernelParam
import com.androidvip.sysctlgui.domain.repository.ParamsRepository
+import kotlinx.coroutines.flow.single
import java.io.File
class GetParamsFromFilesUseCase(private val repository: ParamsRepository) {
- suspend operator fun invoke(files: List): List {
- return repository.getParamsFromFiles(files)
+ suspend operator fun invoke(files: List): List {
+ return repository.getParamsFromFiles(files).single()
}
}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetRuntimeParamUseCase.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetRuntimeParamUseCase.kt
new file mode 100644
index 0000000..62bb645
--- /dev/null
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetRuntimeParamUseCase.kt
@@ -0,0 +1,25 @@
+package com.androidvip.sysctlgui.domain.usecase
+
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import com.androidvip.sysctlgui.domain.repository.AppPrefs
+import com.androidvip.sysctlgui.domain.repository.ParamsRepository
+import kotlinx.coroutines.flow.single
+
+
+/**
+ * Fetches a single runtime kernel parameter by its name.
+ *
+ * This use case interacts with the [ParamsRepository] to retrieve a specific kernel parameter
+ * and respects the user's preference for using BusyBox, as defined in [AppPrefs].
+ */
+class GetRuntimeParamUseCase(
+ private val repository: ParamsRepository,
+ private val appPrefs: AppPrefs
+) {
+ suspend operator fun invoke(paramName: String): KernelParam? {
+ return repository.getRuntimeParam(
+ useBusybox = appPrefs.useBusybox,
+ paramName = paramName
+ )
+ }
+}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetRuntimeParamsUseCase.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetRuntimeParamsUseCase.kt
index 2746fae..6876b1d 100644
--- a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetRuntimeParamsUseCase.kt
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetRuntimeParamsUseCase.kt
@@ -1,14 +1,24 @@
package com.androidvip.sysctlgui.domain.usecase
-import com.androidvip.sysctlgui.domain.models.DomainKernelParam
+import com.androidvip.sysctlgui.domain.models.KernelParam
import com.androidvip.sysctlgui.domain.repository.AppPrefs
import com.androidvip.sysctlgui.domain.repository.ParamsRepository
+import kotlinx.coroutines.flow.single
+/**
+ * Fetches the list of runtime kernel parameters.
+ *
+ * This use case interacts with the [ParamsRepository] to retrieve the current kernel parameters
+ * and respects the user's preference for using BusyBox, as defined in [AppPrefs].
+ */
class GetRuntimeParamsUseCase(
private val repository: ParamsRepository,
private val appPrefs: AppPrefs
) {
- suspend operator fun invoke(): List {
- return repository.getRuntimeParams(appPrefs.useBusybox)
+ suspend operator fun invoke(userParams: List = emptyList()): List {
+ return repository.getRuntimeParams(
+ useBusybox = appPrefs.useBusybox,
+ userParams = userParams
+ ).single()
}
}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetUserParamByNameUseCase.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetUserParamByNameUseCase.kt
new file mode 100644
index 0000000..5569c21
--- /dev/null
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetUserParamByNameUseCase.kt
@@ -0,0 +1,7 @@
+package com.androidvip.sysctlgui.domain.usecase
+
+import com.androidvip.sysctlgui.domain.repository.UserRepository
+
+class GetUserParamByNameUseCase(private val repository: UserRepository) {
+ suspend operator fun invoke(paramName: String) = repository.getParamByName(paramName)
+}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetUserParamsUseCase.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetUserParamsUseCase.kt
index 73222bf..529fdca 100644
--- a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetUserParamsUseCase.kt
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/GetUserParamsUseCase.kt
@@ -1,7 +1,8 @@
package com.androidvip.sysctlgui.domain.usecase
-import com.androidvip.sysctlgui.domain.repository.ParamsRepository
+import com.androidvip.sysctlgui.domain.repository.UserRepository
+import kotlinx.coroutines.flow.single
-class GetUserParamsUseCase(private val repository: ParamsRepository) {
- suspend operator fun invoke() = repository.getUserParams()
+class GetUserParamsUseCase(private val repository: UserRepository) {
+ suspend operator fun invoke() = repository.userParams.single()
}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/ImportParamsUseCase.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/ImportParamsUseCase.kt
deleted file mode 100644
index b7c92c7..0000000
--- a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/ImportParamsUseCase.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package com.androidvip.sysctlgui.domain.usecase
-
-import com.androidvip.sysctlgui.domain.exceptions.InvalidFileExtensionException
-import com.androidvip.sysctlgui.domain.exceptions.NoValidParamException
-import com.androidvip.sysctlgui.domain.models.DomainKernelParam
-import com.androidvip.sysctlgui.domain.repository.ParamsRepository
-import java.io.InputStream
-
-class ImportParamsUseCase(
- private val clearUserParamUseCase: ClearUserParamUseCase,
- private val addUserParamsUseCase: AddUserParamsUseCase,
- private val applyParamsUseCase: ApplyParamsUseCase,
- private val repository: ParamsRepository
-) {
- suspend operator fun invoke(
- stream: InputStream,
- fileExtension: String
- ): List {
- val isBackup = fileExtension.endsWith(".conf")
- val params = when {
- fileExtension.endsWith(".json") -> repository.importParamsFromJson(stream)
- isBackup -> repository.importParamsFromConf(stream)
- else -> throw InvalidFileExtensionException()
- }
-
- if (params.isEmpty()) throw NoValidParamException()
-
- val successfulParams = mutableListOf()
- params.forEach { param ->
- // Apply the param to check if valid
- runCatching { applyParamsUseCase(param) }.onSuccess {
- successfulParams.add(param)
- }
- }
-
- clearUserParamUseCase()
-
- // Prevent adding full backups to the apply-on-boot list
- if (!isBackup) {
- addUserParamsUseCase(successfulParams)
- }
-
- return successfulParams
- }
-}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/IsTaskerInstalledUseCase.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/IsTaskerInstalledUseCase.kt
new file mode 100644
index 0000000..52706a9
--- /dev/null
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/IsTaskerInstalledUseCase.kt
@@ -0,0 +1,27 @@
+package com.androidvip.sysctlgui.domain.usecase
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+
+class IsTaskerInstalledUseCase(private val context: Context) {
+ operator fun invoke(): Boolean {
+ val packageManager = context.packageManager
+
+ return runCatching {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ packageManager.getPackageInfo(
+ TASKER_PACKAGE_NAME,
+ PackageManager.PackageInfoFlags.of(0L)
+ )
+ } else {
+ packageManager.getPackageInfo(TASKER_PACKAGE_NAME, 0)
+ }
+ true
+ }.getOrDefault(false)
+ }
+
+ companion object {
+ private const val TASKER_PACKAGE_NAME = "net.dinglisch.android.taskerm"
+ }
+}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/PerformDatabaseMigrationUseCase.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/PerformDatabaseMigrationUseCase.kt
deleted file mode 100644
index eea6e29..0000000
--- a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/PerformDatabaseMigrationUseCase.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.androidvip.sysctlgui.domain.usecase
-
-import com.androidvip.sysctlgui.domain.repository.ParamsRepository
-
-class PerformDatabaseMigrationUseCase(private val repository: ParamsRepository) {
- suspend operator fun invoke() {
- return repository.performDatabaseMigration()
- }
-}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/RemoveUserParamUseCase.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/RemoveUserParamUseCase.kt
index de0bd61..f592deb 100644
--- a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/RemoveUserParamUseCase.kt
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/RemoveUserParamUseCase.kt
@@ -1,8 +1,10 @@
package com.androidvip.sysctlgui.domain.usecase
-import com.androidvip.sysctlgui.domain.models.DomainKernelParam
-import com.androidvip.sysctlgui.domain.repository.ParamsRepository
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import com.androidvip.sysctlgui.domain.repository.UserRepository
-class RemoveUserParamUseCase(private val repository: ParamsRepository) {
- suspend fun execute(param: DomainKernelParam) = repository.removeUserParam(param)
+class RemoveUserParamUseCase(private val repository: UserRepository) {
+ suspend operator fun invoke(param: KernelParam) {
+ repository.removeUserParam(param)
+ }
}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/UpdateUserParamUseCase.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/UpdateUserParamUseCase.kt
deleted file mode 100644
index 97192e0..0000000
--- a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/UpdateUserParamUseCase.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.androidvip.sysctlgui.domain.usecase
-
-import com.androidvip.sysctlgui.domain.models.DomainKernelParam
-import com.androidvip.sysctlgui.domain.repository.AppPrefs
-import com.androidvip.sysctlgui.domain.repository.ParamsRepository
-
-class UpdateUserParamUseCase(
- private val repository: ParamsRepository,
- private val appPrefs: AppPrefs
-) {
- suspend operator fun invoke(param: DomainKernelParam) {
- return repository.updateUserParam(param, appPrefs.allowBlankValues)
- }
-}
diff --git a/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/UpsertUserParamUseCase.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/UpsertUserParamUseCase.kt
new file mode 100644
index 0000000..3ce75c7
--- /dev/null
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/usecase/UpsertUserParamUseCase.kt
@@ -0,0 +1,26 @@
+package com.androidvip.sysctlgui.domain.usecase
+
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import com.androidvip.sysctlgui.domain.exceptions.BlankValueNotAllowedException
+import com.androidvip.sysctlgui.domain.repository.AppPrefs
+import com.androidvip.sysctlgui.domain.repository.UserRepository
+
+/**
+ * Updates a user-defined kernel parameter.
+ *
+ * @property repository The [UserRepository] to interact with user parameters.
+ * @property appPrefs The [AppPrefs] to check application preferences
+ * @throws BlankValueNotAllowedException if the parameter value is blank and blank values are not allowed.
+ */
+class UpsertUserParamUseCase(
+ private val repository: UserRepository,
+ private val appPrefs: AppPrefs
+) {
+ suspend operator fun invoke(param: KernelParam): Long {
+ if (param.value.isBlank() && !appPrefs.allowBlankValues) {
+ throw BlankValueNotAllowedException()
+ }
+
+ return repository.upsertUserParam(param)
+ }
+}
From 760a4f7a9104c8095caffb4b2ba0d8353f091bc7 Mon Sep 17 00:00:00 2001
From: Lennoard
Date: Mon, 11 Aug 2025 21:29:18 -0300
Subject: [PATCH 03/18] refactor: [WIP] updated data layer
---
.../sysctlgui/utils/BaseViewModel.kt | 5 +-
.../com/androidvip/sysctlgui/utils/Consts.kt | 15 -
.../sysctlgui/utils/ContextUtils.kt | 11 +
.../com/androidvip/sysctlgui/utils/Misc.kt | 32 ++
.../androidvip/sysctlgui/utils/ViewState.kt | 16 -
data/build.gradle.kts | 13 +-
data/consumer-rules.pro | 0
.../com/androidvip/sysctlgui/data/Prefs.kt | 18 ++
.../data/datasource/JsonParamDataSource.kt | 93 ------
.../data/datasource/RoomParamDataSource.kt | 61 ----
.../data/datasource/RuntimeParamDataSource.kt | 93 ------
.../androidvip/sysctlgui/data/db/ParamDao.kt | 31 +-
.../sysctlgui/data/db/ParamDatabase.kt | 4 +-
.../sysctlgui/data/di/DataModule.kt | 78 ++++-
.../sysctlgui/data/mapper/Mapper.kt | 6 -
.../sysctlgui/data/mapper/RoomParamMapper.kt | 26 --
.../sysctlgui/data/models/KernelParamDTO.kt | 30 ++
.../sysctlgui/data/models/RoomKernelParam.kt | 24 --
.../sysctlgui/data/repository/AppPrefsImpl.kt | 78 +++--
.../repository/AppSettingsRepositoryImpl.kt | 145 +++++++++
.../repository/DocumentationRepositoryImpl.kt | 34 ++
.../data/repository/ParamsRepositoryImpl.kt | 298 +++++++-----------
.../data/repository/PresetRepositoryImpl.kt | 70 ++++
.../data/repository/UserRepositoryImpl.kt | 45 +++
.../data/source/DocumentationDataSource.kt | 20 ++
.../source/OfflineDocumentationDataSource.kt | 122 +++++++
.../source/OnlineDocumentationDataSource.kt | 100 ++++++
.../data/utils/AndroidStringProvider.kt | 11 +
.../data/utils/KernelParamSerializer.kt | 65 ++++
.../data/utils/PresetsFileProcessor.kt | 52 +++
.../sysctlgui/data/utils/RootUtils.kt | 57 ++--
{app => data}/src/main/res/raw/abi.txt | 0
{app => data}/src/main/res/raw/fs.txt | 0
{app => data}/src/main/res/raw/kernel.txt | 0
{app => data}/src/main/res/raw/net.txt | 0
{app => data}/src/main/res/raw/vm.txt | 0
36 files changed, 1046 insertions(+), 607 deletions(-)
create mode 100644 common/utils/src/main/kotlin/com/androidvip/sysctlgui/utils/ContextUtils.kt
create mode 100644 common/utils/src/main/kotlin/com/androidvip/sysctlgui/utils/Misc.kt
delete mode 100644 common/utils/src/main/kotlin/com/androidvip/sysctlgui/utils/ViewState.kt
create mode 100644 data/consumer-rules.pro
create mode 100644 data/src/main/java/com/androidvip/sysctlgui/data/Prefs.kt
delete mode 100644 data/src/main/java/com/androidvip/sysctlgui/data/datasource/JsonParamDataSource.kt
delete mode 100644 data/src/main/java/com/androidvip/sysctlgui/data/datasource/RoomParamDataSource.kt
delete mode 100644 data/src/main/java/com/androidvip/sysctlgui/data/datasource/RuntimeParamDataSource.kt
delete mode 100644 data/src/main/java/com/androidvip/sysctlgui/data/mapper/Mapper.kt
delete mode 100644 data/src/main/java/com/androidvip/sysctlgui/data/mapper/RoomParamMapper.kt
create mode 100644 data/src/main/java/com/androidvip/sysctlgui/data/models/KernelParamDTO.kt
delete mode 100644 data/src/main/java/com/androidvip/sysctlgui/data/models/RoomKernelParam.kt
create mode 100644 data/src/main/java/com/androidvip/sysctlgui/data/repository/AppSettingsRepositoryImpl.kt
create mode 100644 data/src/main/java/com/androidvip/sysctlgui/data/repository/DocumentationRepositoryImpl.kt
create mode 100644 data/src/main/java/com/androidvip/sysctlgui/data/repository/PresetRepositoryImpl.kt
create mode 100644 data/src/main/java/com/androidvip/sysctlgui/data/repository/UserRepositoryImpl.kt
create mode 100644 data/src/main/java/com/androidvip/sysctlgui/data/source/DocumentationDataSource.kt
create mode 100644 data/src/main/java/com/androidvip/sysctlgui/data/source/OfflineDocumentationDataSource.kt
create mode 100644 data/src/main/java/com/androidvip/sysctlgui/data/source/OnlineDocumentationDataSource.kt
create mode 100644 data/src/main/java/com/androidvip/sysctlgui/data/utils/AndroidStringProvider.kt
create mode 100644 data/src/main/java/com/androidvip/sysctlgui/data/utils/KernelParamSerializer.kt
create mode 100644 data/src/main/java/com/androidvip/sysctlgui/data/utils/PresetsFileProcessor.kt
rename {app => data}/src/main/res/raw/abi.txt (100%)
rename {app => data}/src/main/res/raw/fs.txt (100%)
rename {app => data}/src/main/res/raw/kernel.txt (100%)
rename {app => data}/src/main/res/raw/net.txt (100%)
rename {app => data}/src/main/res/raw/vm.txt (100%)
diff --git a/common/utils/src/main/kotlin/com/androidvip/sysctlgui/utils/BaseViewModel.kt b/common/utils/src/main/kotlin/com/androidvip/sysctlgui/utils/BaseViewModel.kt
index 0e06803..b5da4e6 100644
--- a/common/utils/src/main/kotlin/com/androidvip/sysctlgui/utils/BaseViewModel.kt
+++ b/common/utils/src/main/kotlin/com/androidvip/sysctlgui/utils/BaseViewModel.kt
@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
abstract class BaseViewModel : ViewModel() {
@@ -28,7 +29,7 @@ abstract class BaseViewModel : ViewModel() {
abstract fun onEvent(event: Event)
protected fun setState(block: State.() -> State) {
- _uiState.value = currentState.block()
+ _uiState.update(block)
}
protected fun setEffect(block: () -> Effect) {
@@ -36,4 +37,4 @@ abstract class BaseViewModel : ViewModel() {
_effect.send(block())
}
}
-}
\ No newline at end of file
+}
diff --git a/common/utils/src/main/kotlin/com/androidvip/sysctlgui/utils/Consts.kt b/common/utils/src/main/kotlin/com/androidvip/sysctlgui/utils/Consts.kt
index fa34402..1c16f66 100644
--- a/common/utils/src/main/kotlin/com/androidvip/sysctlgui/utils/Consts.kt
+++ b/common/utils/src/main/kotlin/com/androidvip/sysctlgui/utils/Consts.kt
@@ -8,19 +8,4 @@ object Consts {
const val LIST_NUMBER_SECONDARY_TASKER: Int = 1
const val LIST_NUMBER_FAVORITES: Int = 2
const val LIST_NUMBER_APPLY_ON_BOOT: Int = 3
-
- object Prefs {
- const val LIST_FOLDERS_FIRST = "list_folders_first"
- const val GUESS_INPUT_TYPE = "guess_input_type"
- const val COMMIT_MODE = "commit_mode"
- const val ALLOW_BLANK = "allow_blank_values"
- const val USE_BUSYBOX = "use_busybox"
- const val RUN_ON_START_UP = "run_on_start_up"
- const val START_UP_DELAY = "startup_delay"
- const val SHOW_TASKER_TOAST = "show_tasker_toast"
- const val MIGRATION_COMPLETED = "migration_completed"
- const val FORCE_DARK_THEME = "force_dark_theme"
- const val DYNAMIC_COLORS = "dynamic_colors"
- const val ASKED_NOTIFICATION_PERMISSION = "asked_notification_permission"
- }
}
diff --git a/common/utils/src/main/kotlin/com/androidvip/sysctlgui/utils/ContextUtils.kt b/common/utils/src/main/kotlin/com/androidvip/sysctlgui/utils/ContextUtils.kt
new file mode 100644
index 0000000..d084717
--- /dev/null
+++ b/common/utils/src/main/kotlin/com/androidvip/sysctlgui/utils/ContextUtils.kt
@@ -0,0 +1,11 @@
+package com.androidvip.sysctlgui.utils
+
+import android.content.Context
+import android.content.Intent
+import androidx.core.net.toUri
+
+fun Context.browse(url: String) {
+ val intent = Intent(Intent.ACTION_VIEW, url.toUri())
+ runCatching { startActivity(intent) }
+}
+
diff --git a/common/utils/src/main/kotlin/com/androidvip/sysctlgui/utils/Misc.kt b/common/utils/src/main/kotlin/com/androidvip/sysctlgui/utils/Misc.kt
new file mode 100644
index 0000000..806811e
--- /dev/null
+++ b/common/utils/src/main/kotlin/com/androidvip/sysctlgui/utils/Misc.kt
@@ -0,0 +1,32 @@
+package com.androidvip.sysctlgui.utils
+
+import android.view.View
+import androidx.core.view.HapticFeedbackConstantsCompat
+import androidx.core.view.ViewCompat
+
+/**
+ * Checks if a string is a valid sysctl line.
+ * A valid sysctl line must:
+ * - Contain exactly one "=" character.
+ * - Not have blank parts before or after the "=".
+ * - Have a key that matches the pattern: `^[a-zA-Z0-9_]+(\\.[a-zA-Z0-9_]+)+$`
+ * (e.g., "vm.swappiness", "net.ipv4.tcp_congestion_control").
+ *
+ * @return `true` if the string is a valid sysctl line, `false` otherwise.
+ */
+fun String.isValidSysctlLine(): Boolean {
+ val parts = this.split("=", limit = 2)
+ if (parts.size != 2 || parts.any { it.isBlank() }) return false
+
+ val keyPattern = "^[a-zA-Z0-9_]+(\\.[a-zA-Z0-9_]+)+$".toRegex()
+ return keyPattern.matches(parts.first())
+}
+
+fun performHapticFeedbackForToggle(newState: Boolean, view: View) {
+ val feedbackConst = if (newState) {
+ HapticFeedbackConstantsCompat.TOGGLE_ON
+ } else {
+ HapticFeedbackConstantsCompat.TOGGLE_OFF
+ }
+ ViewCompat.performHapticFeedback(view, feedbackConst)
+}
diff --git a/common/utils/src/main/kotlin/com/androidvip/sysctlgui/utils/ViewState.kt b/common/utils/src/main/kotlin/com/androidvip/sysctlgui/utils/ViewState.kt
deleted file mode 100644
index d44fac1..0000000
--- a/common/utils/src/main/kotlin/com/androidvip/sysctlgui/utils/ViewState.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.androidvip.sysctlgui.utils
-
-open class ViewState(
- var data: List = listOf(),
- var isLoading: Boolean = true,
- var showEmptyState: Boolean = false,
- var searchExpression: String = "",
-) {
- fun copyState(
- data: List = this.data,
- isLoading: Boolean = this.isLoading,
- showEmptyState: Boolean = this.showEmptyState
- ): ViewState {
- return ViewState(data, isLoading, showEmptyState)
- }
-}
diff --git a/data/build.gradle.kts b/data/build.gradle.kts
index 28f6e5e..56b4f2d 100644
--- a/data/build.gradle.kts
+++ b/data/build.gradle.kts
@@ -50,16 +50,17 @@ dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.preference)
- // Room
- implementation(libs.androidx.room.runtime)
- implementation(libs.androidx.room.ktx)
- ksp(libs.androidx.room.compiler)
-
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.serialization.json)
implementation(libs.koin)
+ implementation(libs.jsoup)
+ implementation(libs.bundles.ktor.clients)
implementation(libs.bundles.libsu)
- implementation(Google.gson)
+
+ // Room
+ implementation(libs.androidx.room.runtime)
+ implementation(libs.androidx.room.ktx)
+ ksp(libs.androidx.room.compiler)
testImplementation(libs.junit)
}
diff --git a/data/consumer-rules.pro b/data/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/data/src/main/java/com/androidvip/sysctlgui/data/Prefs.kt b/data/src/main/java/com/androidvip/sysctlgui/data/Prefs.kt
new file mode 100644
index 0000000..94c6827
--- /dev/null
+++ b/data/src/main/java/com/androidvip/sysctlgui/data/Prefs.kt
@@ -0,0 +1,18 @@
+package com.androidvip.sysctlgui.data
+
+enum class Prefs(val key: String) {
+ ListFoldersFirst("list_folders_first"),
+ GuessInputType("guess_input_type"),
+ CommitMode("commit_mode"),
+ ALLOW_BLANK("allow_blank_values"),
+ UseBusybox("use_busybox"),
+ RunOnStartup("run_on_start_up"),
+ StartupDelay("startup_delay"),
+ ShowTaskerToast("show_tasker_toast"),
+ ForceDarkTheme("force_dark_theme"),
+ DynamicColors("dynamic_colors"),
+ AskedNotificationPermission("asked_notification_permission"),
+ UseOnlineDocs("use_online_docs"),
+ ContrastLevel("contrast_level"),
+ SearchHistory("search_history")
+}
diff --git a/data/src/main/java/com/androidvip/sysctlgui/data/datasource/JsonParamDataSource.kt b/data/src/main/java/com/androidvip/sysctlgui/data/datasource/JsonParamDataSource.kt
deleted file mode 100644
index ec5e755..0000000
--- a/data/src/main/java/com/androidvip/sysctlgui/data/datasource/JsonParamDataSource.kt
+++ /dev/null
@@ -1,93 +0,0 @@
-package com.androidvip.sysctlgui.data.datasource
-
-import android.content.Context
-import com.androidvip.sysctlgui.utils.Consts
-import com.androidvip.sysctlgui.domain.datasource.LocalDataSourceContract
-import com.androidvip.sysctlgui.domain.models.DomainKernelParam
-import com.google.gson.Gson
-import com.google.gson.reflect.TypeToken
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import java.io.File
-import java.lang.reflect.Type
-
-class JsonParamDataSource(
- private val context: Context,
- private val dispatcher: CoroutineDispatcher = Dispatchers.IO
-) : LocalDataSourceContract {
- @Deprecated(
- "JSON database is no longer updated.",
- replaceWith = ReplaceWith("roomParamDatasource.add(param)"),
- level = DeprecationLevel.ERROR
- )
- override suspend fun add(param: DomainKernelParam, allowBlank: Boolean) {
- throw UnsupportedOperationException("Adding json params is not supported")
- }
-
- @Deprecated(
- "JSON database is no longer updated.",
- replaceWith = ReplaceWith("roomParamDatasource.addAll(param)"),
- level = DeprecationLevel.ERROR
- )
- override suspend fun addAll(params: List, allowBlank: Boolean){
- throw UnsupportedOperationException("Adding json params is not supported")
- }
-
- @Deprecated(
- "JSON database is no longer updated.",
- replaceWith = ReplaceWith("roomParamDatasource.remove(param)"),
- level = DeprecationLevel.ERROR
- )
- override suspend fun remove(param: DomainKernelParam) {
- throw UnsupportedOperationException("Deleting params is only supported in room database")
- }
-
- @Deprecated(
- "JSON database is no longer updated.",
- replaceWith = ReplaceWith("roomParamDatasource.edit(param)"),
- level = DeprecationLevel.ERROR
- )
- override suspend fun edit(
- param: DomainKernelParam,
- allowBlank: Boolean
- ) {
- throw UnsupportedOperationException("Updating json params is no longer supported")
- }
-
- override suspend fun clear() = withContext(dispatcher) {
- arrayOf(
- "favorites-params",
- "user-params",
- "tasker-params-${Consts.LIST_NUMBER_PRIMARY_TASKER}",
- "tasker-params-${Consts.LIST_NUMBER_SECONDARY_TASKER}",
- "tasker-params-${Consts.LIST_NUMBER_FAVORITES}",
- "tasker-params-${Consts.LIST_NUMBER_APPLY_ON_BOOT}"
- ).forEach { fileName ->
- val paramFile = File(context.filesDir, fileName)
- paramFile.writeText("[]")
- }
- }
-
- override suspend fun getData(): List = withContext(dispatcher) {
- val gson = Gson()
- val params = mutableListOf()
-
- arrayOf(
- "favorites-params",
- "user-params",
- "tasker-params-${Consts.LIST_NUMBER_PRIMARY_TASKER}",
- "tasker-params-${Consts.LIST_NUMBER_SECONDARY_TASKER}",
- "tasker-params-${Consts.LIST_NUMBER_FAVORITES}",
- "tasker-params-${Consts.LIST_NUMBER_APPLY_ON_BOOT}"
- ).forEach { fileName ->
- val paramsFile = File(context.filesDir, "$fileName.json")
- if (!paramsFile.exists()) return@forEach
-
- val type: Type = object : TypeToken>() {}.type
- params.addAll(gson.fromJson(paramsFile.readText(), type))
- }
-
- return@withContext params.distinct()
- }
-}
diff --git a/data/src/main/java/com/androidvip/sysctlgui/data/datasource/RoomParamDataSource.kt b/data/src/main/java/com/androidvip/sysctlgui/data/datasource/RoomParamDataSource.kt
deleted file mode 100644
index 0bd2815..0000000
--- a/data/src/main/java/com/androidvip/sysctlgui/data/datasource/RoomParamDataSource.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-package com.androidvip.sysctlgui.data.datasource
-
-import com.androidvip.sysctlgui.data.db.ParamDao
-import com.androidvip.sysctlgui.data.mapper.RoomParamMapper
-import com.androidvip.sysctlgui.domain.datasource.LocalDataSourceContract
-import com.androidvip.sysctlgui.domain.models.DomainKernelParam
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-
-class RoomParamDataSource(
- private val paramDao: ParamDao,
- private val dispatcher: CoroutineDispatcher = Dispatchers.IO
-) : LocalDataSourceContract {
- override suspend fun add(
- param: DomainKernelParam,
- allowBlank: Boolean
- ) = withContext(dispatcher) {
- if (!allowBlank) require(param.value.isNotBlank()) {
- "Param contains blank value while ALLOW_BLANK is not active"
- }
- paramDao.insert(RoomParamMapper.unmap(param))
- }
-
- override suspend fun addAll(
- params: List,
- allowBlank: Boolean
- ) = withContext(dispatcher) {
- val filteredParams = if (allowBlank) {
- params
- } else params.filter {
- it.value.isNotEmpty()
- }
-
- paramDao.insert(*filteredParams.map { RoomParamMapper.unmap(it) }.toTypedArray())
- }
-
- override suspend fun remove(param: DomainKernelParam) = withContext(dispatcher) {
- paramDao.delete(RoomParamMapper.unmap(param))
- }
-
- override suspend fun edit(
- param: DomainKernelParam,
- allowBlank: Boolean
- ) = withContext(dispatcher) {
- if (!allowBlank) require(param.value.isNotBlank()) {
- "Param contains blank value while ALLOW_BLANK is not active"
- }
- paramDao.update(RoomParamMapper.unmap(param))
- }
-
- override suspend fun clear() = withContext(dispatcher) {
- paramDao.clearTable()
- }
-
- override suspend fun getData(): List = withContext(dispatcher) {
- paramDao.getAll()?.map {
- RoomParamMapper.map(it)
- } ?: throw Exception("Failed to get params from the local database")
- }
-}
diff --git a/data/src/main/java/com/androidvip/sysctlgui/data/datasource/RuntimeParamDataSource.kt b/data/src/main/java/com/androidvip/sysctlgui/data/datasource/RuntimeParamDataSource.kt
deleted file mode 100644
index 39a0b2b..0000000
--- a/data/src/main/java/com/androidvip/sysctlgui/data/datasource/RuntimeParamDataSource.kt
+++ /dev/null
@@ -1,93 +0,0 @@
-package com.androidvip.sysctlgui.data.datasource
-
-import com.androidvip.sysctlgui.data.utils.RootUtils
-import com.androidvip.sysctlgui.domain.datasource.RuntimeDataSourceContract
-import com.androidvip.sysctlgui.domain.exceptions.ApplyValueException
-import com.androidvip.sysctlgui.domain.exceptions.CommitModeException
-import com.androidvip.sysctlgui.domain.models.DomainKernelParam
-import java.io.File
-import java.lang.IllegalArgumentException
-
-class RuntimeParamDataSource(
- private val rootUtils: RootUtils
-) : RuntimeDataSourceContract {
- override suspend fun edit(
- param: DomainKernelParam,
- commitMode: String,
- useBusybox: Boolean,
- allowBlank: Boolean
- ) {
- val commitResult = commitChanges(param, commitMode, useBusybox, allowBlank)
-
- when {
- commitMode == "sysctl" -> {
- if (commitResult == "error" || !commitResult.contains(param.name)) {
- throw CommitModeException("Value refused to apply. Try using 'echo' mode.")
- }
- }
- commitResult == "error" -> {
- throw ApplyValueException("Value refused to apply")
- }
- }
- }
-
- override suspend fun getData(useBusybox: Boolean): List {
- val command = if (useBusybox) "busybox sysctl -a" else "sysctl -a"
- val lines = mutableListOf()
- rootUtils.executeWithOutput(command) { lines += it }
-
- return lines.filter {
- it.isValidSysctlOutput()
- }.map {
- // Expected output: grandparent.parent.name = value
- val split = it.split("=")
- split.first().trim() to split.last().trim()
- }.mapIndexed { index, paramPair ->
- DomainKernelParam(
- id = index + 1,
- name = paramPair.first,
- value = paramPair.second
- ).apply {
- setPathFromName(paramPair.first)
- }
- }
- }
-
- override suspend fun getParamsFromFiles(files: List): List {
- return files.map {
- it.absolutePath
- }.mapIndexed { index, path ->
- DomainKernelParam(
- id = index + 1,
- path = path
- ).apply {
- setNameFromPath(path)
- value = rootUtils.executeWithOutput("cat $path", "")
- }
- }
- }
-
- private suspend fun commitChanges(
- param: DomainKernelParam,
- commitMode: String,
- useBusybox: Boolean,
- allowBlank: Boolean
- ): String {
- if (!allowBlank && param.value.isBlank()) throw IllegalArgumentException(
- "Param contains blank value while ALLOW_BLANK is not active"
- )
-
- val prefix = if (useBusybox) "busybox " else ""
- val command = when (commitMode) {
- "sysctl" -> "${prefix}sysctl -w ${param.name}=${param.value}"
- "echo" -> "echo '${param.value}' > ${param.path}"
- else -> "busybox sysctl -w ${param.name}=${param.value}"
- }
-
- return rootUtils.executeWithOutput(command, "error")
- }
-
- private fun String.isValidSysctlOutput(): Boolean {
- return !contains("denied") && !startsWith("sysctl") && contains("=")
- }
-}
diff --git a/data/src/main/java/com/androidvip/sysctlgui/data/db/ParamDao.kt b/data/src/main/java/com/androidvip/sysctlgui/data/db/ParamDao.kt
index 6870cdb..968166c 100644
--- a/data/src/main/java/com/androidvip/sysctlgui/data/db/ParamDao.kt
+++ b/data/src/main/java/com/androidvip/sysctlgui/data/db/ParamDao.kt
@@ -2,25 +2,32 @@ package com.androidvip.sysctlgui.data.db
import androidx.room.Dao
import androidx.room.Delete
-import androidx.room.Insert
import androidx.room.Query
-import androidx.room.Update
-import com.androidvip.sysctlgui.data.models.RoomKernelParam
+import androidx.room.Upsert
+import com.androidvip.sysctlgui.data.models.KernelParamDTO
+import com.androidvip.sysctlgui.data.models.PARAMS_TABLE_NAME
+import kotlinx.coroutines.flow.Flow
@Dao
interface ParamDao {
- @Query("SELECT * FROM roomKernelParam")
- suspend fun getAll(): List?
+ @Query("SELECT * FROM $PARAMS_TABLE_NAME")
+ suspend fun getAll(): List
- @Insert
- suspend fun insert(vararg params: RoomKernelParam)
+ @Query("SELECT * FROM $PARAMS_TABLE_NAME")
+ fun getAllAsFlow(): Flow>
- @Delete
- suspend fun delete(param: RoomKernelParam)
+ @Query("SELECT * FROM $PARAMS_TABLE_NAME WHERE name = :name")
+ suspend fun getParamByName(name: String): KernelParamDTO?
+
+ @Upsert
+ suspend fun upsert(param: KernelParamDTO): Long
- @Update
- suspend fun update(param: RoomKernelParam)
+ @Upsert
+ suspend fun upsertAll(params: List): List
+
+ @Delete
+ suspend fun delete(param: KernelParamDTO): Int
- @Query("DELETE FROM roomKernelParam")
+ @Query("DELETE FROM $PARAMS_TABLE_NAME")
suspend fun clearTable()
}
diff --git a/data/src/main/java/com/androidvip/sysctlgui/data/db/ParamDatabase.kt b/data/src/main/java/com/androidvip/sysctlgui/data/db/ParamDatabase.kt
index ff2a1ed..7376bd3 100644
--- a/data/src/main/java/com/androidvip/sysctlgui/data/db/ParamDatabase.kt
+++ b/data/src/main/java/com/androidvip/sysctlgui/data/db/ParamDatabase.kt
@@ -2,9 +2,9 @@ package com.androidvip.sysctlgui.data.db
import androidx.room.Database
import androidx.room.RoomDatabase
-import com.androidvip.sysctlgui.data.models.RoomKernelParam
+import com.androidvip.sysctlgui.data.models.KernelParamDTO
-@Database(entities = [RoomKernelParam::class], version = 1, exportSchema = false)
+@Database(entities = [KernelParamDTO::class], version = 1, exportSchema = false)
abstract class ParamDatabase : RoomDatabase() {
abstract fun paramDao(): ParamDao
}
diff --git a/data/src/main/java/com/androidvip/sysctlgui/data/di/DataModule.kt b/data/src/main/java/com/androidvip/sysctlgui/data/di/DataModule.kt
index 9251daa..1cbd751 100644
--- a/data/src/main/java/com/androidvip/sysctlgui/data/di/DataModule.kt
+++ b/data/src/main/java/com/androidvip/sysctlgui/data/di/DataModule.kt
@@ -1,22 +1,44 @@
package com.androidvip.sysctlgui.data.di
+import android.util.Log
import androidx.preference.PreferenceManager
-import com.androidvip.sysctlgui.data.datasource.JsonParamDataSource
-import com.androidvip.sysctlgui.data.datasource.RoomParamDataSource
-import com.androidvip.sysctlgui.data.datasource.RuntimeParamDataSource
import com.androidvip.sysctlgui.data.db.ParamDatabase
import com.androidvip.sysctlgui.data.db.ParamDatabaseManager
import com.androidvip.sysctlgui.data.repository.AppPrefsImpl
+import com.androidvip.sysctlgui.data.repository.AppSettingsRepositoryImpl
+import com.androidvip.sysctlgui.data.repository.DocumentationRepositoryImpl
import com.androidvip.sysctlgui.data.repository.ParamsRepositoryImpl
+import com.androidvip.sysctlgui.data.repository.PresetRepositoryImpl
+import com.androidvip.sysctlgui.data.repository.UserRepositoryImpl
+import com.androidvip.sysctlgui.data.source.DocumentationDataSource
+import com.androidvip.sysctlgui.data.source.OfflineDocumentationDataSource
+import com.androidvip.sysctlgui.data.source.OnlineDocumentationDataSource
+import com.androidvip.sysctlgui.data.utils.AndroidStringProvider
+import com.androidvip.sysctlgui.data.utils.PresetsFileProcessor
import com.androidvip.sysctlgui.data.utils.RootUtils
+import com.androidvip.sysctlgui.domain.StringProvider
import com.androidvip.sysctlgui.domain.repository.AppPrefs
+import com.androidvip.sysctlgui.domain.repository.AppSettingsRepository
+import com.androidvip.sysctlgui.domain.repository.DocumentationRepository
import com.androidvip.sysctlgui.domain.repository.ParamsRepository
-import kotlinx.coroutines.Dispatchers
+import com.androidvip.sysctlgui.domain.repository.PresetRepository
+import com.androidvip.sysctlgui.domain.repository.UserRepository
+import io.ktor.client.HttpClient
+import io.ktor.client.engine.android.Android
+import io.ktor.client.plugins.logging.LogLevel
+import io.ktor.client.plugins.logging.Logger
+import io.ktor.client.plugins.logging.Logging
+import org.koin.android.ext.koin.androidApplication
import org.koin.android.ext.koin.androidContext
+import org.koin.core.module.dsl.factoryOf
+import org.koin.core.qualifier.named
+import org.koin.dsl.bind
import org.koin.dsl.module
val utilsModule = module {
- factory { RootUtils(Dispatchers.Default) }
+ factoryOf(::RootUtils)
+ factory { PresetsFileProcessor(androidContext().contentResolver) }
+ factory { AndroidStringProvider(androidApplication()) }
}
val dbModule = module {
@@ -25,17 +47,49 @@ val dbModule = module {
}
val repositoryModule = module {
- factory { AppPrefsImpl(get()) }
- single { ParamsRepositoryImpl(get(), get(), get(), get()) }
+ factoryOf(::AppPrefsImpl) bind AppPrefs::class
+ factoryOf(::ParamsRepositoryImpl) bind ParamsRepository::class
+ factoryOf(::PresetRepositoryImpl) bind PresetRepository::class
+ factoryOf(::AppSettingsRepositoryImpl) bind AppSettingsRepository::class
+
+ single { UserRepositoryImpl(paramDao = get().paramDao()) }
+
+ factory {
+ DocumentationRepositoryImpl(
+ offlineDataSource = get(named()),
+ onlineDataSource = get(named())
+ )
+ }
}
val dataSourceModule = module {
- single { JsonParamDataSource(androidContext()) }
- single { RuntimeParamDataSource(rootUtils = get()) }
+ factory(named()) {
+ OfflineDocumentationDataSource(androidContext())
+ }
+
+ factory(named()) {
+ OnlineDocumentationDataSource(get())
+ }
+}
+
+val networkModule = module {
single {
- val db: ParamDatabase = get()
- RoomParamDataSource(db.paramDao())
+ HttpClient(engineFactory = Android) {
+ engine {
+ connectTimeout = 5000
+ socketTimeout = 5000
+ }
+
+ install(Logging) {
+ logger = object : Logger {
+ override fun log(message: String) {
+ Log.v("KtorHttpClient", message)
+ }
+ }
+ level = LogLevel.BODY
+ }
+ }
}
}
-val dataModules = utilsModule + dbModule + repositoryModule + dataSourceModule
+val dataModules = utilsModule + dbModule + repositoryModule + dataSourceModule + networkModule
diff --git a/data/src/main/java/com/androidvip/sysctlgui/data/mapper/Mapper.kt b/data/src/main/java/com/androidvip/sysctlgui/data/mapper/Mapper.kt
deleted file mode 100644
index 0adeea9..0000000
--- a/data/src/main/java/com/androidvip/sysctlgui/data/mapper/Mapper.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.androidvip.sysctlgui.data.mapper
-
-interface Mapper {
- fun map(from: F): T
- fun unmap(from: T): F
-}
diff --git a/data/src/main/java/com/androidvip/sysctlgui/data/mapper/RoomParamMapper.kt b/data/src/main/java/com/androidvip/sysctlgui/data/mapper/RoomParamMapper.kt
deleted file mode 100644
index 8047c20..0000000
--- a/data/src/main/java/com/androidvip/sysctlgui/data/mapper/RoomParamMapper.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.androidvip.sysctlgui.data.mapper
-
-import com.androidvip.sysctlgui.data.models.RoomKernelParam
-import com.androidvip.sysctlgui.domain.models.DomainKernelParam
-
-object RoomParamMapper : Mapper {
- override fun map(from: RoomKernelParam): DomainKernelParam = DomainKernelParam().apply {
- id = from.id
- name = from.name
- path = from.path
- value = from.value
- favorite = from.favorite
- taskerParam = from.taskerParam
- taskerList = from.taskerList
- }
-
- override fun unmap(from: DomainKernelParam): RoomKernelParam = RoomKernelParam().apply {
- id = from.id
- name = from.name
- path = from.path
- value = from.value
- favorite = from.favorite
- taskerParam = from.taskerParam
- taskerList = from.taskerList
- }
-}
diff --git a/data/src/main/java/com/androidvip/sysctlgui/data/models/KernelParamDTO.kt b/data/src/main/java/com/androidvip/sysctlgui/data/models/KernelParamDTO.kt
new file mode 100644
index 0000000..fd31d0e
--- /dev/null
+++ b/data/src/main/java/com/androidvip/sysctlgui/data/models/KernelParamDTO.kt
@@ -0,0 +1,30 @@
+package com.androidvip.sysctlgui.data.models
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import com.androidvip.sysctlgui.data.utils.KernelParamSerializer
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import com.androidvip.sysctlgui.utils.Consts
+import kotlinx.serialization.Serializable
+
+@Entity(tableName = PARAMS_TABLE_NAME)
+@Serializable(with = KernelParamSerializer::class)
+data class KernelParamDTO(
+ @PrimaryKey(autoGenerate = true)
+ val id: Int = 0,
+ @ColumnInfo(name = "name")
+ override val name: String = "",
+ @ColumnInfo(name = "path")
+ override val path: String = "",
+ @ColumnInfo(name = "value")
+ override val value: String = "",
+ @ColumnInfo(name = "favorite")
+ override val isFavorite: Boolean = false,
+ @ColumnInfo(name = "tasker_param")
+ override val isTaskerParam: Boolean = false,
+ @ColumnInfo(name = "tasker_list")
+ override val taskerList: Int = Consts.LIST_NUMBER_PRIMARY_TASKER
+) : KernelParam(name, path, value, isFavorite, isTaskerParam, taskerList)
+
+internal const val PARAMS_TABLE_NAME = "roomKernelParam"
diff --git a/data/src/main/java/com/androidvip/sysctlgui/data/models/RoomKernelParam.kt b/data/src/main/java/com/androidvip/sysctlgui/data/models/RoomKernelParam.kt
deleted file mode 100644
index 143e3a6..0000000
--- a/data/src/main/java/com/androidvip/sysctlgui/data/models/RoomKernelParam.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.androidvip.sysctlgui.data.models
-
-import androidx.room.ColumnInfo
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-import com.androidvip.sysctlgui.utils.Consts
-
-@Entity
-data class RoomKernelParam(
- @PrimaryKey(autoGenerate = true)
- var id: Int = 0,
- @ColumnInfo(name = "name")
- var name: String = "",
- @ColumnInfo(name = "path")
- var path: String = "",
- @ColumnInfo(name = "value")
- var value: String = "",
- @ColumnInfo(name = "favorite")
- var favorite: Boolean = false,
- @ColumnInfo(name = "tasker_param")
- var taskerParam: Boolean = false,
- @ColumnInfo(name = "tasker_list")
- var taskerList: Int = Consts.LIST_NUMBER_PRIMARY_TASKER
-)
diff --git a/data/src/main/java/com/androidvip/sysctlgui/data/repository/AppPrefsImpl.kt b/data/src/main/java/com/androidvip/sysctlgui/data/repository/AppPrefsImpl.kt
index a48e7cc..4528454 100644
--- a/data/src/main/java/com/androidvip/sysctlgui/data/repository/AppPrefsImpl.kt
+++ b/data/src/main/java/com/androidvip/sysctlgui/data/repository/AppPrefsImpl.kt
@@ -2,68 +2,90 @@ package com.androidvip.sysctlgui.data.repository
import android.content.SharedPreferences
import androidx.core.content.edit
+import com.androidvip.sysctlgui.data.Prefs
import com.androidvip.sysctlgui.domain.repository.AppPrefs
-import com.androidvip.sysctlgui.utils.Consts
+/**
+ * Implementation of [AppPrefs] that uses [SharedPreferences] to store and retrieve app preferences.
+ */
class AppPrefsImpl(private val prefs: SharedPreferences) : AppPrefs {
override var listFoldersFirst: Boolean
- get() = prefs.getBoolean(Consts.Prefs.LIST_FOLDERS_FIRST, true)
+ get() = prefs.getBoolean(Prefs.ListFoldersFirst.key, true)
set(value) {
- prefs.edit { putBoolean(Consts.Prefs.LIST_FOLDERS_FIRST, value) }
+ prefs.edit { putBoolean(Prefs.ListFoldersFirst.key, value) }
}
override var guessInputType: Boolean
- get() = prefs.getBoolean(Consts.Prefs.GUESS_INPUT_TYPE, true)
+ get() = prefs.getBoolean(Prefs.GuessInputType.key, true)
set(value) {
- prefs.edit { putBoolean(Consts.Prefs.GUESS_INPUT_TYPE, value) }
+ prefs.edit { putBoolean(Prefs.GuessInputType.key, value) }
}
override var commitMode: String
- get() = prefs.getString(Consts.Prefs.COMMIT_MODE, "sysctl") ?: "sysctl"
+ get() = prefs.getString(Prefs.CommitMode.key, "sysctl") ?: "sysctl"
set(value) {
- prefs.edit { putString(Consts.Prefs.COMMIT_MODE, value) }
+ prefs.edit { putString(Prefs.CommitMode.key, value) }
}
override var allowBlankValues: Boolean
- get() = prefs.getBoolean(Consts.Prefs.ALLOW_BLANK, false)
+ get() = prefs.getBoolean(Prefs.ALLOW_BLANK.key, false)
set(value) {
- prefs.edit { putBoolean(Consts.Prefs.ALLOW_BLANK, value) }
+ prefs.edit { putBoolean(Prefs.ALLOW_BLANK.key, value) }
}
override var useBusybox: Boolean
- get() = prefs.getBoolean(Consts.Prefs.USE_BUSYBOX, false)
+ get() = prefs.getBoolean(Prefs.UseBusybox.key, false)
set(value) {
- prefs.edit { putBoolean(Consts.Prefs.USE_BUSYBOX, value) }
+ prefs.edit { putBoolean(Prefs.UseBusybox.key, value) }
}
override var runOnStartUp: Boolean
- get() = prefs.getBoolean(Consts.Prefs.RUN_ON_START_UP, false)
+ get() = prefs.getBoolean(Prefs.RunOnStartup.key, false)
set(value) {
- prefs.edit { putBoolean(Consts.Prefs.RUN_ON_START_UP, value) }
+ prefs.edit { putBoolean(Prefs.RunOnStartup.key, value) }
}
override var startUpDelay: Int
- get() = prefs.getInt(Consts.Prefs.START_UP_DELAY, 0)
+ get() = prefs.getInt(Prefs.StartupDelay.key, 0)
set(value) {
- prefs.edit { putInt(Consts.Prefs.START_UP_DELAY, value) }
+ prefs.edit { putInt(Prefs.StartupDelay.key, value) }
}
override var showTaskerToast: Boolean
- get() = prefs.getBoolean(Consts.Prefs.SHOW_TASKER_TOAST, true)
+ get() = prefs.getBoolean(Prefs.ShowTaskerToast.key, true)
set(value) {
- prefs.edit { putBoolean(Consts.Prefs.SHOW_TASKER_TOAST, value) }
- }
- override var migrationCompleted: Boolean
- get() = prefs.getBoolean(Consts.Prefs.MIGRATION_COMPLETED, false)
- set(value) {
- prefs.edit { putBoolean(Consts.Prefs.MIGRATION_COMPLETED, value) }
+ prefs.edit { putBoolean(Prefs.ShowTaskerToast.key, value) }
}
override var forceDark: Boolean
- get() = prefs.getBoolean(Consts.Prefs.FORCE_DARK_THEME, false)
+ get() = prefs.getBoolean(Prefs.ForceDarkTheme.key, false)
set(value) {
- prefs.edit { putBoolean(Consts.Prefs.FORCE_DARK_THEME, value) }
+ prefs.edit { putBoolean(Prefs.ForceDarkTheme.key, value) }
}
override var dynamicColors: Boolean
- get() = prefs.getBoolean(Consts.Prefs.DYNAMIC_COLORS, false)
+ get() = prefs.getBoolean(Prefs.DynamicColors.key, false)
set(value) {
- prefs.edit { putBoolean(Consts.Prefs.DYNAMIC_COLORS, value) }
+ prefs.edit { putBoolean(Prefs.DynamicColors.key, value) }
}
override var askedForNotificationPermission: Boolean
- get() = prefs.getBoolean(Consts.Prefs.ASKED_NOTIFICATION_PERMISSION, false)
+ get() = prefs.getBoolean(Prefs.AskedNotificationPermission.key, false)
+ set(value) {
+ prefs.edit { putBoolean(Prefs.AskedNotificationPermission.key, value) }
+ }
+ override var useOnlineDocs: Boolean
+ get() = prefs.getBoolean(Prefs.UseOnlineDocs.key, true)
set(value) {
- prefs.edit { putBoolean(Consts.Prefs.ASKED_NOTIFICATION_PERMISSION, value) }
+ prefs.edit { putBoolean(Prefs.UseOnlineDocs.key, value) }
}
+ override var contrastLevel: Int
+ get() = prefs.getInt(Prefs.ContrastLevel.key, 1)
+ set(value) {
+ prefs.edit { putInt(Prefs.ContrastLevel.key, value) }
+ }
+ override val searchHistory: Set
+ get() = prefs.getStringSet(Prefs.SearchHistory.key, emptySet()) ?: emptySet()
+
+ override fun addSearchToHistory(query: String) {
+ val currentHistory = searchHistory.toMutableSet()
+ currentHistory.add(query)
+ prefs.edit { putStringSet(Prefs.SearchHistory.key, currentHistory) }
+ }
+
+ override fun removeSearchFromHistory(query: String) {
+ val currentHistory = searchHistory.toMutableSet()
+ currentHistory.remove(query)
+ prefs.edit { putStringSet(Prefs.SearchHistory.key, currentHistory) }
+ }
}
diff --git a/data/src/main/java/com/androidvip/sysctlgui/data/repository/AppSettingsRepositoryImpl.kt b/data/src/main/java/com/androidvip/sysctlgui/data/repository/AppSettingsRepositoryImpl.kt
new file mode 100644
index 0000000..386e7f8
--- /dev/null
+++ b/data/src/main/java/com/androidvip/sysctlgui/data/repository/AppSettingsRepositoryImpl.kt
@@ -0,0 +1,145 @@
+package com.androidvip.sysctlgui.data.repository
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.os.Build
+import com.androidvip.sysctlgui.data.Prefs
+import com.androidvip.sysctlgui.data.utils.RootUtils
+import com.androidvip.sysctlgui.domain.enums.CommitMode
+import com.androidvip.sysctlgui.domain.enums.SettingItemType
+import com.androidvip.sysctlgui.domain.models.AppSetting
+import com.androidvip.sysctlgui.domain.repository.AppSettingsRepository
+import kotlinx.coroutines.withContext
+import kotlin.coroutines.CoroutineContext
+
+class AppSettingsRepositoryImpl(
+ private val context: Context,
+ private val sharedPreferences: SharedPreferences,
+ private val rootUtils: RootUtils,
+ private val ioContext: CoroutineContext
+) : AppSettingsRepository {
+ override suspend fun getAppSettings(): List> = withContext(ioContext) {
+ val usingDynamicColors = sharedPreferences.getBoolean(Prefs.DynamicColors.key, false)
+ val supportsDynamicColors = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
+
+ listOf(
+ /////////// GENERAL SETTINGS ////////////
+ AppSetting(
+ key = Prefs.ListFoldersFirst.key,
+ value = sharedPreferences.getBoolean(Prefs.ListFoldersFirst.key, true),
+ category = "General",
+ title = "List folders first",
+ description = "List folders first when using the kernel parameter browser option",
+ type = SettingItemType.Switch,
+ ),
+ AppSetting(
+ key = Prefs.GuessInputType.key,
+ value = sharedPreferences.getBoolean(Prefs.GuessInputType.key, true),
+ category = "General",
+ title = "Guess input type",
+ description = "Try to set the best input type for the keyboard based on the value of the parameter",
+ type = SettingItemType.Switch,
+ ),
+ AppSetting(
+ key = Prefs.UseOnlineDocs.key,
+ value = sharedPreferences.getBoolean(Prefs.UseOnlineDocs.key, true),
+ category = "General",
+ title = "Use online docs",
+ description = "Try to use online documentation when displaying parameter descriptions",
+ type = SettingItemType.Switch,
+ ),
+
+ /////////// THEME SETTINGS ////////////
+
+ AppSetting(
+ key = Prefs.ForceDarkTheme.key,
+ value = sharedPreferences.getBoolean(Prefs.ForceDarkTheme.key, false),
+ category = "Theme",
+ title = "Force Dark",
+ description = "Force dark theme when available",
+ type = SettingItemType.Switch,
+ ),
+ AppSetting(
+ key = Prefs.DynamicColors.key,
+ value = usingDynamicColors,
+ enabled = supportsDynamicColors,
+ category = "Theme",
+ title = "Dynamic Colors",
+ description = "Use dynamic colors when available",
+ type = SettingItemType.Switch,
+ ),
+ AppSetting(
+ key = Prefs.ContrastLevel.key,
+ enabled = !usingDynamicColors,
+ value = sharedPreferences.getInt(Prefs.ContrastLevel.key, 1),
+ category = "Theme",
+ title = "Contrast level",
+ description = "Contrast level for the theme colors",
+ type = SettingItemType.Slider,
+ values = listOf(1, 2, 3),
+ ),
+
+ /////////// COMMIT SETTINGS ////////////
+
+ AppSetting(
+ key = Prefs.CommitMode.key,
+ value = sharedPreferences.getString(
+ Prefs.CommitMode.key,
+ CommitMode.SYSCTL.name.lowercase()
+ ) ?: "sysctl",
+ category = "Operations",
+ title = "Commit mode",
+ description = "Command used when applying the parameter value",
+ type = SettingItemType.List,
+ values = listOf(
+ CommitMode.SYSCTL.name.lowercase(),
+ CommitMode.ECHO.name.lowercase(),
+ )
+ ),
+ AppSetting(
+ key = Prefs.UseBusybox.key,
+ value = sharedPreferences.getBoolean(Prefs.UseBusybox.key, false),
+ enabled = rootUtils.isBusyboxAvailable(),
+ category = "Operations",
+ title = "Use busybox",
+ description = "Use busybox to execute commands",
+ type = SettingItemType.Switch,
+ ),
+ AppSetting(
+ key = Prefs.ALLOW_BLANK.key,
+ value = sharedPreferences.getBoolean(Prefs.ALLOW_BLANK.key, false),
+ category = "Operations",
+ title = "Allow blank values",
+ type = SettingItemType.Switch,
+ ),
+
+ /////////// STARTUP SETTINGS ////////////
+
+ AppSetting(
+ key = Prefs.RunOnStartup.key,
+ value = sharedPreferences.getBoolean(Prefs.RunOnStartup.key, false),
+ category = "Startup",
+ title = "Run on startup",
+ description = "Allow the application to apply parameters on startup",
+ type = SettingItemType.Switch,
+ ),
+ AppSetting(
+ key = Prefs.StartupDelay.key,
+ value = sharedPreferences.getInt(Prefs.StartupDelay.key, 0),
+ category = "Startup",
+ title = "Startup delay",
+ description = "Delay in seconds before applying parameters on startup",
+ type = SettingItemType.Slider,
+ values = (0..10).toList(),
+ ),
+ AppSetting(
+ key = "",
+ value = Unit,
+ category = "Startup",
+ title = "Manage parameters",
+ description = "Manage the parameters that will be applied at startup",
+ type = SettingItemType.Text,
+ )
+ )
+ }
+}
diff --git a/data/src/main/java/com/androidvip/sysctlgui/data/repository/DocumentationRepositoryImpl.kt b/data/src/main/java/com/androidvip/sysctlgui/data/repository/DocumentationRepositoryImpl.kt
new file mode 100644
index 0000000..09ae565
--- /dev/null
+++ b/data/src/main/java/com/androidvip/sysctlgui/data/repository/DocumentationRepositoryImpl.kt
@@ -0,0 +1,34 @@
+package com.androidvip.sysctlgui.data.repository
+
+import com.androidvip.sysctlgui.data.source.DocumentationDataSource
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import com.androidvip.sysctlgui.domain.models.ParamDocumentation
+import com.androidvip.sysctlgui.domain.repository.AppPrefs
+import com.androidvip.sysctlgui.domain.repository.DocumentationRepository
+
+/**
+ * Repository for fetching documentation for kernel parameters.
+ *
+ * This repository can fetch documentation from either an online or offline data source,
+ * depending on the user's preference set in [AppPrefs].
+ *
+ * @property offlineDataSource The data source for fetching documentation offline.
+ * @property onlineDataSource The data source for fetching documentation online.
+ * @property appPrefs The application preferences, used to determine whether to use online or
+ * offline documentation.
+ */
+class DocumentationRepositoryImpl(
+ private val offlineDataSource: DocumentationDataSource,
+ private val onlineDataSource: DocumentationDataSource
+) : DocumentationRepository {
+ override suspend fun getDocumentation(
+ param: KernelParam,
+ online: Boolean
+ ): ParamDocumentation? {
+ return if (online) {
+ onlineDataSource.getDocumentation(param)
+ } else {
+ offlineDataSource.getDocumentation(param)
+ }
+ }
+}
diff --git a/data/src/main/java/com/androidvip/sysctlgui/data/repository/ParamsRepositoryImpl.kt b/data/src/main/java/com/androidvip/sysctlgui/data/repository/ParamsRepositoryImpl.kt
index 5400846..768c335 100644
--- a/data/src/main/java/com/androidvip/sysctlgui/data/repository/ParamsRepositoryImpl.kt
+++ b/data/src/main/java/com/androidvip/sysctlgui/data/repository/ParamsRepositoryImpl.kt
@@ -1,207 +1,133 @@
package com.androidvip.sysctlgui.data.repository
-import com.androidvip.sysctlgui.data.datasource.JsonParamDataSource
-import com.androidvip.sysctlgui.data.datasource.RoomParamDataSource
-import com.androidvip.sysctlgui.data.datasource.RuntimeParamDataSource
-import com.androidvip.sysctlgui.domain.exceptions.EmptyFileException
-import com.androidvip.sysctlgui.domain.exceptions.MalformedLineException
-import com.androidvip.sysctlgui.domain.models.DomainKernelParam
+import android.util.Log
+import com.androidvip.sysctlgui.data.utils.RootUtils
+import com.androidvip.sysctlgui.domain.enums.CommitMode
+import com.androidvip.sysctlgui.domain.models.KernelParam
import com.androidvip.sysctlgui.domain.repository.ParamsRepository
-import com.google.gson.Gson
-import com.google.gson.reflect.TypeToken
+import com.androidvip.sysctlgui.utils.isValidSysctlLine
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.single
+import kotlinx.coroutines.flow.toList
import java.io.File
-import java.io.FileDescriptor
-import java.io.FileOutputStream
-import java.io.InputStream
-import java.lang.reflect.Type
-import kotlin.coroutines.CoroutineContext
class ParamsRepositoryImpl(
- private val jsonParamDataSource: JsonParamDataSource,
- private val roomParamDataSource: RoomParamDataSource,
- private val runtimeParamDataSource: RuntimeParamDataSource,
- private val changeListener: ChangeListener?,
- private val ioContext: CoroutineContext = Dispatchers.IO,
- private val workerContext: CoroutineContext = Dispatchers.Default
+ private val rootUtils: RootUtils,
+ private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : ParamsRepository {
-
- override suspend fun getUserParams(): List = withContext(ioContext) {
- return@withContext roomParamDataSource.getData()
- }
-
- override suspend fun getJsonParams(): List = withContext(ioContext) {
- return@withContext jsonParamDataSource.getData()
- }
-
- override suspend fun getRuntimeParams(
- useBusybox: Boolean
- ): List = withContext(workerContext) {
- val localParams = getUserParams()
- val runtimeParams = runtimeParamDataSource.getData(useBusybox)
-
- return@withContext runtimeParams.onEach { runtimeParam ->
- runtimeParam.updateParamWithLocalData(localParams)
- }
- }
-
- override suspend fun getParamsFromFiles(
- files: List
- ): List = withContext(ioContext) {
- val localParams = getUserParams()
- val fileParams = runtimeParamDataSource.getParamsFromFiles(files)
-
- return@withContext fileParams.onEach { runtimeParam ->
- runtimeParam.updateParamWithLocalData(localParams)
- }
- }
-
- override suspend fun applyParam(
- param: DomainKernelParam,
- commitMode: String,
+ override fun getRuntimeParams(
useBusybox: Boolean,
- allowBlank: Boolean
- ) = withContext(workerContext) {
- runtimeParamDataSource.edit(param, commitMode, useBusybox, allowBlank).also {
- changeListener?.onChange()
- }
- }
-
- override suspend fun updateUserParam(
- param: DomainKernelParam,
- allowBlank: Boolean
- ) = withContext(ioContext) {
- val storedParam = getUserParams().find {
- it.name == param.name
- } ?: return@withContext addUserParam(param, allowBlank)
-
- param.id = storedParam.id
- return@withContext roomParamDataSource.edit(param, allowBlank).also {
- changeListener?.onChange()
- }
- }
-
- override suspend fun addUserParam(
- param: DomainKernelParam,
- allowBlank: Boolean
- ) = withContext(ioContext) {
- return@withContext roomParamDataSource.add(param, allowBlank).also {
- changeListener?.onChange()
- }
- }
-
- override suspend fun addUserParams(
- params: List,
- allowBlank: Boolean
- ) = withContext(ioContext) {
- return@withContext roomParamDataSource.addAll(params, allowBlank).also {
- changeListener?.onChange()
- }
- }
-
- override suspend fun removeUserParam(param: DomainKernelParam) = withContext(ioContext) {
- return@withContext roomParamDataSource.remove(param).also {
- changeListener?.onChange()
- }
- }
-
- override suspend fun clearUserParams() = withContext(ioContext) {
- return@withContext roomParamDataSource.clear().also {
- jsonParamDataSource.clear()
- changeListener?.onChange()
- }
- }
-
- override suspend fun performDatabaseMigration() = withContext(ioContext) {
- val jsonParams = getJsonParams()
-
- return@withContext roomParamDataSource.addAll(jsonParams, true).also {
- changeListener?.onChange()
- }
- }
-
- override suspend fun importParamsFromJson(
- stream: InputStream
- ): List = withContext(ioContext) {
- if (stream.available() == 0) throw EmptyFileException()
-
- val rawText = buildString {
- stream.bufferedReader().use { reader ->
- reader.forEachLine { line ->
- append(line)
- }
+ userParams: List
+ ): Flow> = flow {
+ val command = if (useBusybox) BUSYBOX_SYSCTL_GET_ALL_COMMAND else SYSCTL_GET_ALL_COMMAND
+ val paramsList = rootUtils.executeCommandAndStreamOutput(command)
+ .filter { line -> line.isValidSysctlOutput() }
+ .mapNotNull { line ->
+ // Expected output: "grandparent.parent.name = value"
+ val parts = line.split("=", limit = 2)
+ val paramName = parts.first().trim()
+ val paramValue = if (parts.size > 1) parts.last().trim() else ""
+ runCatching {
+ KernelParam.createFromName(
+ name = paramName,
+ value = paramValue,
+ isFavorite = userParams.any { it.name == paramName }
+ )
+ }.getOrNull()
}
- }
- val type: Type = object : TypeToken>() {}.type
- return@withContext Gson().fromJson(rawText, type)
- }
+ .toList()
- override suspend fun importParamsFromConf(
- stream: InputStream
- ): List = withContext(ioContext) {
- fun String.validConfLine() = !startsWith("#") && !startsWith(";") && isNotEmpty()
- val readParams = mutableListOf()
+ emit(paramsList)
+ }.flowOn(ioDispatcher)
- if (stream.available() == 0) throw EmptyFileException()
+ override suspend fun getRuntimeParam(paramName: String, useBusybox: Boolean): KernelParam? {
+ val command = String.format(
+ SYSCTL_GET_PARAM_COMMAND_FORMAT,
+ if (useBusybox) BUSYBOX_PREFIX else "",
+ paramName
+ )
- var cont = 0
- stream.bufferedReader().forEachLine { line ->
- if (line.validConfLine()) runCatching {
- readParams.add(
- DomainKernelParam(
- id = ++cont,
- name = line.split("=").first().trim(),
- value = line.split("=")[1].trim()
- ).apply {
- setPathFromName(this.name)
- }
- )
- }.onFailure {
- throw MalformedLineException()
- }
- }
- return@withContext readParams
- }
+ val paramValue = runCatching {
+ rootUtils.executeCommandAndStreamOutput(command).single()
+ }.getOrNull() ?: return null
- override suspend fun exportParams(
- params: List,
- fileDescriptor: FileDescriptor
- ) = withContext(ioContext) {
- return@withContext FileOutputStream(fileDescriptor).use { stream ->
- stream.write(Gson().toJson(params).toByteArray())
- }
+ return KernelParam.createFromName(
+ name = paramName,
+ value = paramValue
+ )
}
- override suspend fun backupParams(
- params: List,
- fileDescriptor: FileDescriptor
- ) = withContext(ioContext) {
- val rawText = buildString {
- params.forEach { param ->
- appendLine(param.toString())
- }
- }
-
- return@withContext FileOutputStream(fileDescriptor).use { stream ->
- stream.write(rawText.toByteArray())
+ override suspend fun setRuntimeParam(
+ param: KernelParam,
+ commitMode: CommitMode,
+ useBusybox: Boolean
+ ): String {
+ val command = when (commitMode) {
+ CommitMode.SYSCTL -> String.format(
+ SYSCTL_SET_PARAM_COMMAND_FORMAT,
+ if (useBusybox) BUSYBOX_PREFIX else "",
+ param.name,
+ param.value
+ )
+
+ CommitMode.ECHO -> String.format(ECHO_SET_PARAM_COMMAND_FORMAT, param.value, param.path)
}
- }
- private fun DomainKernelParam.updateParamWithLocalData(
- localParams: List
- ): DomainKernelParam {
- return apply {
- favorite = localParams.firstOrNull { roomParam ->
- (roomParam.name == name) && roomParam.favorite
- } != null
- taskerParam = localParams.firstOrNull { roomParam ->
- (roomParam.name == name) && roomParam.taskerParam
- } != null
+ val output = rootUtils.executeCommandAndStreamOutput(command).toList()
+ return output.joinToString("\n")
+ }
+
+ /**
+ * Reads kernel parameters from a list of files.
+ * The parameter name is derived from the file path.
+ *
+ * @param files A list of [File] objects representing the kernel parameter files.
+ * @return A [Flow] emitting a list of [KernelParam] objects.
+ * Returns an empty list if no files are provided or if errors occur during processing.
+ * Emits null for files that could not be processed.
+ */
+ override fun getParamsFromFiles(files: List): Flow> = flow {
+ val params = files.mapNotNull { file ->
+ try {
+ val path = file.absolutePath
+ val value = rootUtils.executeCommandAndStreamOutput(
+ command = String.format(CAT_COMMAND_FORMAT, path)
+ ).toList().joinToString("\n")
+ KernelParam.createFromPath(path, value)
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to process file: ${file.path}", e)
+ null
+ }
}
- }
-
- interface ChangeListener {
- fun onChange()
+ emit(params)
+ }.flowOn(ioDispatcher)
+
+ override fun getParamsFromPath(path: String): Flow> {
+ val files = File(path).listFiles()?.toList() ?: emptyList()
+ return getParamsFromFiles(files)
+ }
+
+ private fun String.isValidSysctlOutput(): Boolean {
+ return isValidSysctlLine() &&
+ !this.contains("denied", ignoreCase = true) &&
+ !this.startsWith("sysctl")
+ }
+
+ companion object {
+ private const val BUSYBOX_PREFIX = "busybox "
+ private const val SYSCTL_GET_ALL_COMMAND = "sysctl -a"
+ private const val BUSYBOX_SYSCTL_GET_ALL_COMMAND = "$BUSYBOX_PREFIX$SYSCTL_GET_ALL_COMMAND"
+ private const val SYSCTL_GET_PARAM_COMMAND_FORMAT = "%ssysctl -n %s" // prefix, name
+ private const val SYSCTL_SET_PARAM_COMMAND_FORMAT =
+ "%ssysctl -w %s=%s" // prefix, name, value
+ private const val ECHO_SET_PARAM_COMMAND_FORMAT = "echo '%s' > %s" // value, path
+ private const val CAT_COMMAND_FORMAT = "cat %s" // path
+ private const val TAG = "ParamsRepositoryImpl"
}
}
diff --git a/data/src/main/java/com/androidvip/sysctlgui/data/repository/PresetRepositoryImpl.kt b/data/src/main/java/com/androidvip/sysctlgui/data/repository/PresetRepositoryImpl.kt
new file mode 100644
index 0000000..4d6a735
--- /dev/null
+++ b/data/src/main/java/com/androidvip/sysctlgui/data/repository/PresetRepositoryImpl.kt
@@ -0,0 +1,70 @@
+package com.androidvip.sysctlgui.data.repository
+
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import com.androidvip.sysctlgui.domain.exceptions.EmptyFileException
+import com.androidvip.sysctlgui.domain.exceptions.MalformedLineException
+import com.androidvip.sysctlgui.domain.repository.PresetRepository
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.json.Json
+import java.io.FileDescriptor
+import java.io.FileOutputStream
+import java.io.InputStream
+import kotlin.coroutines.CoroutineContext
+
+class PresetRepositoryImpl(
+ private val ioCoroutineContext: CoroutineContext = Dispatchers.IO
+) : PresetRepository {
+ override suspend fun readPreset(
+ stream: InputStream
+ ): List = withContext(ioCoroutineContext) {
+ if (stream.available() == 0) throw EmptyFileException()
+
+ return@withContext stream.bufferedReader().use { reader ->
+ reader.lineSequence()
+ .filter { it.validConfLine() }
+ .map { line ->
+ val parts = line.split("=", limit = 2)
+ if (parts.size == 2) {
+ val name = parts[0].trim()
+ val value = parts[1].trim()
+ runCatching {
+ KernelParam.createFromName(name = name, value = value)
+ }.getOrElse {
+ throw MalformedLineException("Invalid format for line: $line", it)
+ }
+ } else {
+ throw MalformedLineException("Line doesn't contain '=' separator: $line")
+ }
+ }.toList()
+ }
+ }
+
+ override suspend fun exportToPreset(params: List, fileDescriptor: FileDescriptor) {
+ val content = params.joinToString(separator = "\n") { param ->
+ "${param.name}=${param.value}"
+ }
+
+ writeContentToFileDescriptor(fileDescriptor, content)
+ }
+
+ override suspend fun backupParams(params: List, fileDescriptor: FileDescriptor) {
+ val content = Json.encodeToString(params)
+ writeContentToFileDescriptor(fileDescriptor, content)
+ }
+
+ private suspend fun writeContentToFileDescriptor(
+ fileDescriptor: FileDescriptor,
+ content: String
+ ) = withContext(ioCoroutineContext) {
+ FileOutputStream(fileDescriptor).use { fileOutputStream ->
+ fileOutputStream.writer(Charsets.UTF_8).buffered().use { writer ->
+ writer.write(content)
+ }
+ }
+ }
+}
+
+private fun String.validConfLine(): Boolean {
+ return !startsWith("#") && !startsWith(";") && isNotEmpty()
+}
diff --git a/data/src/main/java/com/androidvip/sysctlgui/data/repository/UserRepositoryImpl.kt b/data/src/main/java/com/androidvip/sysctlgui/data/repository/UserRepositoryImpl.kt
new file mode 100644
index 0000000..3dbb29b
--- /dev/null
+++ b/data/src/main/java/com/androidvip/sysctlgui/data/repository/UserRepositoryImpl.kt
@@ -0,0 +1,45 @@
+package com.androidvip.sysctlgui.data.repository
+
+import com.androidvip.sysctlgui.data.db.ParamDao
+import com.androidvip.sysctlgui.data.models.KernelParamDTO
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import com.androidvip.sysctlgui.domain.repository.UserRepository
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.withContext
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * Implementation of [UserRepository] that uses a [ParamDao] to store and retrieve user parameters.
+ *
+ * @param paramDao The DAO used to interact with the database.
+ * @param coroutineContext The coroutine context to use for database operations. Defaults to [Dispatchers.IO].
+ */
+class UserRepositoryImpl(
+ private val paramDao: ParamDao,
+ private val coroutineContext: CoroutineContext = Dispatchers.IO
+) : UserRepository {
+ override val userParams: Flow>
+ get() = paramDao.getAllAsFlow()
+
+ override suspend fun getParamByName(name: String) = withContext(coroutineContext) {
+ paramDao.getParamByName(name)
+ }
+
+ override suspend fun upsertUserParam(param: KernelParam) = withContext(coroutineContext) {
+ paramDao.upsert(param as KernelParamDTO)
+ }
+
+ override suspend fun upsertUserParams(params: List) =
+ withContext(coroutineContext) {
+ paramDao.upsertAll(params.map { it as KernelParamDTO })
+ }
+
+ override suspend fun removeUserParam(param: KernelParam) = withContext(coroutineContext) {
+ paramDao.delete(param as KernelParamDTO)
+ }
+
+ override suspend fun clearUserParams() = withContext(coroutineContext) {
+ paramDao.clearTable()
+ }
+}
diff --git a/data/src/main/java/com/androidvip/sysctlgui/data/source/DocumentationDataSource.kt b/data/src/main/java/com/androidvip/sysctlgui/data/source/DocumentationDataSource.kt
new file mode 100644
index 0000000..de5672d
--- /dev/null
+++ b/data/src/main/java/com/androidvip/sysctlgui/data/source/DocumentationDataSource.kt
@@ -0,0 +1,20 @@
+package com.androidvip.sysctlgui.data.source
+
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import com.androidvip.sysctlgui.domain.models.ParamDocumentation
+
+
+/**
+ * Data source interface for fetching documentation for kernel parameters.
+ * This interface defines the contract for any class that provides access
+ * to kernel parameter documentation.
+ */
+fun interface DocumentationDataSource {
+ /**
+ * Retrieves documentation for a given kernel parameter.
+ *
+ * @param param The kernel parameter for which to fetch documentation.
+ * @return The documentation if found, null otherwise.
+ */
+ suspend fun getDocumentation(param: KernelParam): ParamDocumentation?
+}
diff --git a/data/src/main/java/com/androidvip/sysctlgui/data/source/OfflineDocumentationDataSource.kt b/data/src/main/java/com/androidvip/sysctlgui/data/source/OfflineDocumentationDataSource.kt
new file mode 100644
index 0000000..82d88c4
--- /dev/null
+++ b/data/src/main/java/com/androidvip/sysctlgui/data/source/OfflineDocumentationDataSource.kt
@@ -0,0 +1,122 @@
+package com.androidvip.sysctlgui.data.source
+
+import android.annotation.SuppressLint
+import android.content.Context
+import com.androidvip.sysctlgui.data.R
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import com.androidvip.sysctlgui.domain.models.ParamDocumentation
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import java.io.InputStream
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * Fetches documentation from offline sources.
+ *
+ * It first tries to find a string resource matching the parameter name.
+ * If not found, it attempts to extract documentation from raw text files
+ * bundled with the application, categorized by the parameter's path.
+ *
+ * @property context The application context, used to access resources.
+ * @property coroutineContext The coroutine context on which to perform operations.
+ */
+class OfflineDocumentationDataSource(
+ private val context: Context,
+ private val coroutineContext: CoroutineContext = Dispatchers.IO
+) : DocumentationDataSource {
+
+ /**
+ * Retrieves documentation for a given kernel parameter.
+ *
+ * This function attempts to find documentation in the following order:
+ * 1. **String Resource:** Checks for a string resource matching the parameter's name (normalized by replacing hyphens with underscores).
+ * 2. **Raw Text File:** If no string resource is found, it tries to locate documentation within a raw text file based on the parameter's path.
+ * - The path is expected to be in the format `/proc/sys/category/...`.
+ * - The "category" segment determines which raw file to read (e.g., `abi.txt`, `fs.txt`).
+ * - Inside the raw file, it searches for a section matching the parameter's name, delimited by "====" lines.
+ *
+ * @param param The [KernelParam] for which to retrieve documentation.
+ * @return A [String] containing the documentation if found, or `null` otherwise.
+ */
+ @SuppressLint("DiscouragedApi") // Resource name is determined dynamically from name.
+ override suspend fun getDocumentation(
+ param: KernelParam)
+ : ParamDocumentation? = withContext(coroutineContext) {
+ val paramName = param.lastNameSegment
+ val resources = context.resources
+
+ val normalizedResourceName = paramName.replace("-", "_")
+ val resId = resources.getIdentifier(
+ normalizedResourceName,
+ "string",
+ context.packageName
+ )
+ val stringRes = runCatching { context.getString(resId) }.getOrNull()
+
+ // Prefer the documented string resource
+ if (stringRes != null) return@withContext ParamDocumentation(
+ title = param.name,
+ documentationText = stringRes
+ )
+
+ // Assuming path is like /proc/sys/category/further/path
+ val pathSegments = param.path.trim('/').split('/')
+ if (pathSegments.size < MIN_PATH_SEGMENTS_FOR_CATEGORY) return@withContext null
+
+ // Validate fixed parts like "proc" and "sys"
+ if (pathSegments.getOrNull(0) != "proc" || pathSegments.getOrNull(1) != "sys") {
+ // We did our best
+ return@withContext null
+ }
+
+ // Index 2 after splitting by '/' and removing leading '/'
+ val category = pathSegments.getOrNull(2)
+ val rawInputStream: InputStream? = when (category) {
+ "abi" -> resources.openRawResource(R.raw.abi)
+ "fs" -> resources.openRawResource(R.raw.fs)
+ "kernel" -> resources.openRawResource(R.raw.kernel)
+ "net" -> resources.openRawResource(R.raw.net)
+ "vm" -> resources.openRawResource(R.raw.vm)
+ else -> null
+ }
+
+ val documentation = rawInputStream?.use { inputStream ->
+ inputStream.bufferedReader().use { reader ->
+ reader.readText()
+ }
+ }
+ if (documentation.isNullOrEmpty()) return@withContext null
+
+ /*
+ Trying to match:
+
+ ===============
+
+ paramName
+
+ the <==
+ actual <==
+ documentation <==
+
+ ===============
+ */
+ val info: String? = runCatching {
+ documentation
+ .split("=+".toRegex())
+ .last { it.contains("$paramName\n") }
+ .split("$paramName\n")
+ .last()
+ }.getOrNull()
+
+ val documentationText = info.takeIf { it.isNullOrEmpty().not() }
+ if (documentationText == null) return@withContext null
+ return@withContext ParamDocumentation(
+ title = param.name,
+ documentationText = documentationText
+ )
+ }
+
+ companion object {
+ private const val MIN_PATH_SEGMENTS_FOR_CATEGORY = 4
+ }
+}
diff --git a/data/src/main/java/com/androidvip/sysctlgui/data/source/OnlineDocumentationDataSource.kt b/data/src/main/java/com/androidvip/sysctlgui/data/source/OnlineDocumentationDataSource.kt
new file mode 100644
index 0000000..c79b528
--- /dev/null
+++ b/data/src/main/java/com/androidvip/sysctlgui/data/source/OnlineDocumentationDataSource.kt
@@ -0,0 +1,100 @@
+package com.androidvip.sysctlgui.data.source
+
+import android.util.Log
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import com.androidvip.sysctlgui.domain.models.ParamDocumentation
+import io.ktor.client.HttpClient
+import io.ktor.client.request.get
+import io.ktor.client.statement.bodyAsText
+import io.ktor.http.isSuccess
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.jsoup.Jsoup
+import kotlin.coroutines.CoroutineContext
+
+
+class OnlineDocumentationDataSource(
+ private val client: HttpClient,
+ private val coroutineContext: CoroutineContext = Dispatchers.IO
+) : DocumentationDataSource {
+
+ /**
+ * Fetches the documentation for a given kernel parameter.
+ *
+ * This function constructs the documentation URL based on the parameter's name,
+ * retrieves the HTML content from that URL using Ktor and extracts the
+ * relevant documentation text using Jsoup.
+ *
+ * @param param The [KernelParam] for which to fetch documentation.
+ * @return The documentation text as a [ParamDocumentation], or `null` if the
+ * documentation could not be found or an error occurred.
+ */
+ override suspend fun getDocumentation(
+ param: KernelParam
+ ): ParamDocumentation? = withContext(coroutineContext) {
+ val url = getDocumentationUrl(param)
+
+ return@withContext runCatching {
+ val response = client.get(urlString = url)
+
+ if (!response.status.isSuccess()) {
+ Log.w(
+ "OnlineDocRepo",
+ "Failed to fetch docs from $url. Status: ${response.status}"
+ )
+ return@withContext null
+ }
+
+ val html = response.bodyAsText()
+ val document = Jsoup.parse(html)
+ val htmlElementId = param.lastNameSegment.replace('_', '-')
+ val elements = document.select("section#$htmlElementId p")
+
+ if (elements.isEmpty()) {
+ Log.w(
+ "OnlineDocRepo",
+ "No documentation found for ${param.name} with id $htmlElementId on $url"
+ )
+ return@withContext null
+ }
+
+ return@withContext ParamDocumentation(
+ title = param.name,
+ documentationText = elements.text(),
+ documentationHtml = elements.html().optimizedDocumentationHtml(),
+ url = url
+ )
+ }.getOrElse {
+ Log.w("OnlineDocRepo", "Failed to fetch docs from $url", it)
+ return@withContext null
+ }
+ }
+
+ private fun getDocumentationUrl(param: KernelParam): String {
+ val configName = param.groupName
+ return "${DOC_BASE_URL}$configName.html#${param.name}"
+ }
+
+ /**
+ * Optimizes HTML documentation for display.
+ *
+ * This function performs a series of replacements on the input HTML string
+ * to try and improve its rendering in a basic HTML text renderer, such as Android's TextView.
+ * @return The optimized HTML string.
+ */
+ private fun String.optimizedDocumentationHtml(): String {
+ return this.trimIndent()
+ .replace("", "")
+ .replace("", "") // For "code" blocks
+ .replace("", "")
+ .replace("", "") // For code tags
+ .replace("", "
")
+ .replace("
", "") // For spaced bullet points
+ .replace("", "
") // For line breaks in paragraphs
+ .removeSuffix("
") // Remove the last line break
+ }
+
+ companion object {
+ internal const val DOC_BASE_URL = "https://docs.kernel.org/admin-guide/sysctl/"
+ }
+}
diff --git a/data/src/main/java/com/androidvip/sysctlgui/data/utils/AndroidStringProvider.kt b/data/src/main/java/com/androidvip/sysctlgui/data/utils/AndroidStringProvider.kt
new file mode 100644
index 0000000..666fc53
--- /dev/null
+++ b/data/src/main/java/com/androidvip/sysctlgui/data/utils/AndroidStringProvider.kt
@@ -0,0 +1,11 @@
+package com.androidvip.sysctlgui.data.utils
+
+import android.app.Application
+import com.androidvip.sysctlgui.domain.StringProvider
+
+class AndroidStringProvider(private val application: Application) : StringProvider {
+ override fun getString(resId: Int): String = application.getString(resId)
+ override fun getString(resId: Int, vararg formatArgs: Any): String {
+ return application.getString(resId, *formatArgs)
+ }
+}
diff --git a/data/src/main/java/com/androidvip/sysctlgui/data/utils/KernelParamSerializer.kt b/data/src/main/java/com/androidvip/sysctlgui/data/utils/KernelParamSerializer.kt
new file mode 100644
index 0000000..0357c1b
--- /dev/null
+++ b/data/src/main/java/com/androidvip/sysctlgui/data/utils/KernelParamSerializer.kt
@@ -0,0 +1,65 @@
+package com.androidvip.sysctlgui.data.utils
+
+import com.androidvip.sysctlgui.data.models.KernelParamDTO
+import com.androidvip.sysctlgui.utils.Consts
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.descriptors.element
+import kotlinx.serialization.encoding.CompositeDecoder
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.encoding.decodeStructure
+import kotlinx.serialization.encoding.encodeStructure
+
+object KernelParamSerializer : KSerializer {
+ override val descriptor: SerialDescriptor = buildClassSerialDescriptor("KernelParamDTO") {
+ element("id")
+ element("name")
+ element("path")
+ element("value")
+ element("isFavorite")
+ element("isTaskerParam")
+ element("taskerList")
+ }
+
+ override fun serialize(encoder: Encoder, value: KernelParamDTO) {
+ encoder.encodeStructure(descriptor) {
+ encodeIntElement(descriptor, 0, value.id)
+ encodeStringElement(descriptor, 1, value.name)
+ encodeStringElement(descriptor, 2, value.path)
+ encodeStringElement(descriptor, 3, value.value)
+ encodeBooleanElement(descriptor, 4, value.isFavorite)
+ encodeBooleanElement(descriptor, 5, value.isTaskerParam)
+ encodeIntElement(descriptor, 6, value.taskerList)
+ }
+ }
+
+ override fun deserialize(decoder: Decoder): KernelParamDTO {
+ return decoder.decodeStructure(descriptor) {
+ var id = 0
+ var name = ""
+ var path = ""
+ var value = ""
+ var isFavorite = false
+ var isTaskerParam = false
+ var taskerList = Consts.LIST_NUMBER_PRIMARY_TASKER // Default
+
+ while (true) {
+ when (val index = decodeElementIndex(descriptor)) {
+ 0 -> id = decodeIntElement(descriptor, 0)
+ 1 -> name = decodeStringElement(descriptor, 1)
+ 2 -> path = decodeStringElement(descriptor, 2)
+ 3 -> value = decodeStringElement(descriptor, 3)
+ 4 -> isFavorite = decodeBooleanElement(descriptor, 4)
+ 5 -> isTaskerParam = decodeBooleanElement(descriptor, 5)
+ 6 -> taskerList = decodeIntElement(descriptor, 6)
+ CompositeDecoder.Companion.DECODE_DONE -> break
+ else -> throw SerializationException("Unknown index $index")
+ }
+ }
+ KernelParamDTO(id, name, path, value, isFavorite, isTaskerParam, taskerList)
+ }
+ }
+}
\ No newline at end of file
diff --git a/data/src/main/java/com/androidvip/sysctlgui/data/utils/PresetsFileProcessor.kt b/data/src/main/java/com/androidvip/sysctlgui/data/utils/PresetsFileProcessor.kt
new file mode 100644
index 0000000..696b4e1
--- /dev/null
+++ b/data/src/main/java/com/androidvip/sysctlgui/data/utils/PresetsFileProcessor.kt
@@ -0,0 +1,52 @@
+package com.androidvip.sysctlgui.data.utils
+
+import android.content.ContentResolver
+import android.net.Uri
+import android.util.Log
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import com.androidvip.sysctlgui.utils.isValidSysctlLine
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import java.io.IOException
+
+class PresetsFileProcessor(
+ private val contentResolver: ContentResolver,
+ private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
+) {
+ suspend fun getKernelParamsFromUri(
+ uri: Uri
+ ): List = withContext(ioDispatcher) {
+ contentResolver.openInputStream(uri)?.use { inputStream ->
+ val lines = inputStream.bufferedReader().readLines()
+ lines.mapNotNull { line ->
+ if (line.isValidSysctlLine()) {
+ runCatching {
+ KernelParam.Companion.createFromName(
+ name = line.substringBefore('=').trim(),
+ value = line.substringAfter('=').trim(),
+ isFavorite = true
+ )
+ }.getOrNull()
+ } else {
+ Log.w("PresetsFileProcessor", "Invalid line: $line")
+ null
+ }
+ }
+ } ?: throw IOException("Failed to open input stream for URI: $uri")
+ }
+
+ suspend fun backupParamsToUri(
+ uri: Uri,
+ params: List
+ ) = withContext(ioDispatcher) {
+ val fileContent = params.joinToString("\n") { "${it.name}=${it.value}" }
+
+ contentResolver.openOutputStream(uri)?.use { outputStream ->
+ outputStream.bufferedWriter().use { writer ->
+ writer.write(fileContent)
+ writer.flush()
+ }
+ } ?: throw IOException("Failed to open output stream for URI: $uri")
+ }
+}
\ No newline at end of file
diff --git a/data/src/main/java/com/androidvip/sysctlgui/data/utils/RootUtils.kt b/data/src/main/java/com/androidvip/sysctlgui/data/utils/RootUtils.kt
index 901304a..93bcfb1 100644
--- a/data/src/main/java/com/androidvip/sysctlgui/data/utils/RootUtils.kt
+++ b/data/src/main/java/com/androidvip/sysctlgui/data/utils/RootUtils.kt
@@ -1,42 +1,49 @@
package com.androidvip.sysctlgui.data.utils
+import android.util.Log
+import com.androidvip.sysctlgui.domain.exceptions.ShellCommandException
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.withContext
-class RootUtils(private val dispatcher: CoroutineDispatcher = Dispatchers.Default) {
+class RootUtils(private val shellDispatcher: CoroutineDispatcher = Dispatchers.Default) {
+ suspend fun getRootShell(): Shell? = withContext(shellDispatcher) {
+ Shell.getShell().takeIf { it.isRoot }
+ }
+
+ suspend fun isRootAvailable(): Boolean = withContext(shellDispatcher) {
+ Shell.isAppGrantedRoot() == true
+ }
- suspend fun isBusyboxAvailable(): Boolean = withContext(dispatcher) {
+ suspend fun isBusyboxAvailable(): Boolean = withContext(shellDispatcher) {
val results: List = Shell.cmd("which busybox").exec().out
return@withContext ShellUtils.isValidOutput(results) && results.firstOrNull()
?.isNotEmpty() == true
}
- suspend fun executeWithOutput(
- command: String,
- defaultOutput: String = "",
- forEachLine: ((String) -> Unit)? = null
- ): String = withContext(dispatcher) {
- return@withContext runCatching {
- buildString {
- val outputs = Shell.cmd(command).exec().out
- if (!ShellUtils.isValidOutput(outputs)) {
- append(defaultOutput)
- return@buildString
- }
- outputs.forEach { line ->
- if (forEachLine != null) {
- forEachLine(line.orEmpty())
- appendLine(line.orEmpty())
- } else {
- appendLine(line.orEmpty())
- }
- }
- }.trim().removeSuffix("\n")
- }.getOrDefault(defaultOutput)
- }
+ fun executeCommandAndStreamOutput(command: String): Flow = flow {
+ val result = Shell.cmd(command).exec()
+ val outputs = result.out
+
+ if (ShellUtils.isValidOutput(outputs)) {
+ outputs.forEach { line ->
+ emit(line.orEmpty())
+ }
+ } else {
+ if (result.isSuccess.not()) {
+ result.err.forEach { errorLine -> Log.e("RootUtils", errorLine) }
+ throw ShellCommandException(
+ message = "Command execution failed",
+ cause = Exception(result.err.joinToString("\n"))
+ )
+ }
+ }
+ }.flowOn(shellDispatcher)
fun finishProcess() {
runCatching {
diff --git a/app/src/main/res/raw/abi.txt b/data/src/main/res/raw/abi.txt
similarity index 100%
rename from app/src/main/res/raw/abi.txt
rename to data/src/main/res/raw/abi.txt
diff --git a/app/src/main/res/raw/fs.txt b/data/src/main/res/raw/fs.txt
similarity index 100%
rename from app/src/main/res/raw/fs.txt
rename to data/src/main/res/raw/fs.txt
diff --git a/app/src/main/res/raw/kernel.txt b/data/src/main/res/raw/kernel.txt
similarity index 100%
rename from app/src/main/res/raw/kernel.txt
rename to data/src/main/res/raw/kernel.txt
diff --git a/app/src/main/res/raw/net.txt b/data/src/main/res/raw/net.txt
similarity index 100%
rename from app/src/main/res/raw/net.txt
rename to data/src/main/res/raw/net.txt
diff --git a/app/src/main/res/raw/vm.txt b/data/src/main/res/raw/vm.txt
similarity index 100%
rename from app/src/main/res/raw/vm.txt
rename to data/src/main/res/raw/vm.txt
From 8fa41eb50c0e2441dbe8a606646d96a1c86e2642 Mon Sep 17 00:00:00 2001
From: Lennoard
Date: Tue, 12 Aug 2025 20:50:04 -0300
Subject: [PATCH 04/18] refactor: [WIP] updated presentation layer
---
app/build.gradle.kts | 22 +-
app/src/main/AndroidManifest.xml | 20 +-
.../com/androidvip/sysctlgui/SysctlGuiApp.kt | 4 +-
.../core/navigation/TopLevelRoute.kt | 20 +
.../sysctlgui/core/navigation/UiRoute.kt | 27 +
.../data/mapper/DomainParamMapper.kt | 26 -
.../sysctlgui/data/models/KernelParam.kt | 22 -
.../sysctlgui/data/models/SettingsItem.kt | 10 -
.../sysctlgui/di/PresentationModule.kt | 25 +-
.../helpers/OnSettingsItemClickedListener.kt | 7 -
.../sysctlgui/helpers/ParamDiffCallback.kt | 18 -
.../helpers/SettingsItemDiffCallback.kt | 14 -
.../sysctlgui/helpers/UiKernelParamMapper.kt | 17 +
.../androidvip/sysctlgui/models/SearchHint.kt | 15 +
.../sysctlgui/models/UiKernelParam.kt | 58 ++
.../services/tiles/StartAppTileService.kt | 26 +-
.../services/tiles/StartUpTileService.kt | 60 +-
.../ui/base/BaseAppCompatActivity.kt | 25 -
.../sysctlgui/ui/base/BaseSearchFragment.kt | 57 --
.../sysctlgui/ui/base/BaseViewHolder.kt | 10 -
.../sysctlgui/ui/components/ErrorContainer.kt | 96 +++
.../ui/components/SingleChoiceDialog.kt | 77 ++
.../ui/export/ExportOptionsFragment.kt | 152 ----
.../ui/export/ExportOptionsItemAdapter.kt | 40 -
.../ui/export/ExportOptionsViewEffect.kt | 17 -
.../ui/export/ExportOptionsViewModel.kt | 160 ----
.../sysctlgui/ui/main/AppNavHost.kt | 90 ++
.../sysctlgui/ui/main/MainActivity.kt | 81 +-
.../sysctlgui/ui/main/MainNavBar.kt | 107 +++
.../sysctlgui/ui/main/MainScreen.kt | 105 +++
.../sysctlgui/ui/main/MainTopBar.kt | 75 ++
.../sysctlgui/ui/main/MainViewEffect.kt | 9 -
.../sysctlgui/ui/main/MainViewModel.kt | 61 +-
.../sysctlgui/ui/main/MainViewState.kt | 26 +
.../ui/params/DocumentationBottomSheet.kt | 157 ++++
.../sysctlgui/ui/params/EmptyParamsWarning.kt | 76 --
.../ui/params/OnParamItemClickedListener.kt | 8 -
.../params/OnPopUpMenuItemSelectedListener.kt | 13 -
.../ui/params/browse/BrowseParamsViewModel.kt | 161 ----
.../browse/KernelParamBrowseFragment.kt | 277 ------
.../ui/params/browse/ParamBrowseItem.kt | 100 ---
.../ui/params/browse/ParamBrowseScreen.kt | 313 +++++++
.../ui/params/browse/ParamBrowseState.kt | 23 +
.../ui/params/browse/ParamBrowseViewModel.kt | 115 +++
.../params/browse/ParamBrowserViewEffect.kt | 11 -
.../ui/params/browse/ParamBrowserViewEvent.kt | 13 -
.../ui/params/browse/ParamBrowserViewState.kt | 14 -
.../ui/params/browse/ParamFileRow.kt | 159 ++++
.../sysctlgui/ui/params/browse/ParamRow.kt | 106 +++
.../ui/params/edit/ActionToggleButton.kt | 188 ++++
.../ui/params/edit/EditKernelParamActivity.kt | 108 ---
.../ui/params/edit/EditParamScreen.kt | 816 +++++++++++-------
.../ui/params/edit/EditParamViewEffect.kt | 10 -
.../ui/params/edit/EditParamViewEvent.kt | 15 -
.../ui/params/edit/EditParamViewModel.kt | 253 +++---
.../ui/params/edit/EditParamViewState.kt | 26 +-
.../ui/params/list/KernelParamListFragment.kt | 135 ---
.../ui/params/list/ListParamsViewModel.kt | 47 -
.../sysctlgui/ui/params/list/ParamItem.kt | 53 --
.../ui/params/list/ParamViewEffect.kt | 7 -
.../ui/params/list/ParamViewEvent.kt | 9 -
.../ui/params/list/ParamViewState.kt | 9 -
.../params/user/BaseManageParamsActivity.kt | 47 -
.../user/ManageFavoritesParamsActivity.kt | 12 -
.../user/ManageOnStartUpParamsActivity.kt | 12 -
.../ui/params/user/UserParamsScreen.kt | 242 ------
.../ui/params/user/UserParamsViewEvent.kt | 12 -
.../ui/params/user/UserParamsViewModel.kt | 80 --
.../ui/params/user/UserParamsViewState.kt | 8 -
.../ui/presets/ImportPresetScreen.kt | 357 ++++++++
.../sysctlgui/ui/presets/PresetsScreen.kt | 215 +++++
.../sysctlgui/ui/presets/PresetsViewModel.kt | 109 +++
.../sysctlgui/ui/presets/PresetsViewState.kt | 30 +
.../sysctlgui/ui/search/SearchScreen.kt | 462 ++++++++++
.../sysctlgui/ui/search/SearchViewModel.kt | 140 +++
.../sysctlgui/ui/search/SearchViewState.kt | 23 +
.../sysctlgui/ui/settings/SettingsFragment.kt | 120 ---
.../sysctlgui/ui/settings/SettingsScreen.kt | 233 +++++
.../ui/settings/SettingsViewModel.kt | 68 ++
.../ui/settings/components/HeaderComponent.kt | 46 +
.../components/SettingsComponentColumn.kt | 45 +
.../components/SliderSettingComponent.kt | 89 ++
.../components/SwitchSettingComponent.kt | 91 ++
.../components/TextSettingComponent.kt | 111 +++
.../ui/settings/model/SettingsViewEvent.kt | 15 +
.../sysctlgui/ui/start/StartActivity.kt | 44 +-
.../sysctlgui/ui/start/StartErrorActivity.kt | 4 +-
.../ui/tasker/TaskerPluginActivity.kt | 6 +-
.../sysctlgui/ui/user/UserParamsScreen.kt | 262 ++++++
.../sysctlgui/ui/user/UserParamsViewModel.kt | 69 ++
.../sysctlgui/ui/user/UserParamsViewState.kt | 20 +
.../sysctlgui/utils/DataBindingUtils.kt | 10 -
.../sysctlgui/utils/KernelParamUtils.kt | 32 -
.../androidvip/sysctlgui/utils/ThemeExt.kt | 28 -
.../widgets/FavoriteWidgetParamUpdater.kt | 7 +-
.../sysctlgui/widgets/FavoritesWidget.kt | 34 +-
.../widgets/FavoritesWidgetService.kt | 13 +-
.../sysctlgui/work/StartUpWorker.kt | 17 +-
.../androidvip/sysctlgui/work/TaskerWorker.kt | 14 +-
.../drawable-night/ic_launcher_background.xml | 10 -
app/src/main/res/drawable/circle_file.xml | 10 -
app/src/main/res/drawable/circle_folder.xml | 10 -
.../main/res/drawable/fast_scroll_thumb.xml | 13 -
.../main/res/drawable/fast_scroll_track.xml | 5 -
.../main/res/drawable/ic_action_tasker.xml | 10 -
app/src/main/res/drawable/ic_arrow_upward.xml | 12 +
app/src/main/res/drawable/ic_close.xml | 9 -
app/src/main/res/drawable/ic_config.xml | 5 -
app/src/main/res/drawable/ic_delete_sweep.xml | 14 +-
.../main/res/drawable/ic_documentation.xml | 14 +-
app/src/main/res/drawable/ic_edit_outline.xml | 9 -
app/src/main/res/drawable/ic_export.xml | 5 +
app/src/main/res/drawable/ic_favorite.xml | 5 +
.../res/drawable/ic_favorite_outlined.xml | 5 +
app/src/main/res/drawable/ic_file.xml | 9 +
app/src/main/res/drawable/ic_file_outline.xml | 9 -
app/src/main/res/drawable/ic_folder.xml | 11 +
.../main/res/drawable/ic_folder_outline.xml | 9 -
app/src/main/res/drawable/ic_heart_broken.xml | 5 +
app/src/main/res/drawable/ic_history.xml | 5 +
app/src/main/res/drawable/ic_import.xml | 5 +
app/src/main/res/drawable/ic_info_outline.xml | 9 -
.../res/drawable/ic_launcher_background.xml | 2 +-
app/src/main/res/drawable/ic_more_vert.xml | 9 -
app/src/main/res/drawable/ic_name.xml | 5 -
.../main/res/drawable/ic_open_in_browser.xml | 12 +
app/src/main/res/drawable/ic_search.xml | 9 -
.../main/res/drawable/ic_settings_outline.xml | 9 -
app/src/main/res/drawable/ic_tasker.xml | 10 +
.../main/res/drawable/ic_tasker_outlined.xml | 10 +
.../main/res/layout-land/activity_main2.xml | 50 --
app/src/main/res/layout/activity_main2.xml | 43 -
app/src/main/res/layout/activity_splash.xml | 1 -
.../res/layout/activity_tasker_plugin.xml | 10 +-
.../res/layout/fragment_export_options.xml | 43 -
.../main/res/layout/list_item_settings.xml | 74 --
app/src/main/res/layout/settings_activity.xml | 26 -
app/src/main/res/menu/menu_browse_params.xml | 32 -
app/src/main/res/menu/menu_main.xml | 15 -
app/src/main/res/menu/menu_main_search.xml | 23 -
app/src/main/res/menu/menu_search.xml | 11 -
app/src/main/res/menu/nav_main.xml | 24 -
app/src/main/res/menu/popup_manage_params.xml | 5 -
.../main/res/navigation/main_navigation.xml | 50 --
app/src/main/res/values-land/integers.xml | 4 -
app/src/main/res/values-pt-rBR/strings.xml | 2 +-
app/src/main/res/values-tr/strings.xml | 1 -
app/src/main/res/values-v14/dimens.xml | 10 -
app/src/main/res/values/integers.xml | 4 -
app/src/main/res/values/strings.xml | 9 +-
app/src/main/res/xml/preferences.xml | 86 --
buildSrc/src/main/kotlin/AndroidX.kt | 26 -
buildSrc/src/main/kotlin/BuildPlugins.kt | 7 -
buildSrc/src/main/kotlin/Compose.kt | 8 -
buildSrc/src/main/kotlin/Dependencies.kt | 12 -
buildSrc/src/main/kotlin/Google.kt | 4 -
buildSrc/src/main/kotlin/Modules.kt | 7 -
common/design/build.gradle.kts | 6 +-
.../design/BaseBottomSheetFragment.kt | 67 --
.../sysctlgui/design/DesignResources.kt | 5 -
.../sysctlgui/design/ModalBottomSheet.kt | 88 --
.../sysctlgui/design/theme/Color.kt | 290 +++++--
.../sysctlgui/design/theme/Shape.kt | 26 -
.../sysctlgui/design/theme/Theme.kt | 303 +++++--
.../androidvip/sysctlgui/design/theme/Type.kt | 133 +++
.../src/main/res/font/passionone_bold.ttf | Bin 0 -> 22876 bytes
.../src/main/res/font/passionone_regular.ttf | Bin 0 -> 23076 bytes
.../src/main/res/font/sansation_bold.ttf | Bin 0 -> 37012 bytes
.../main/res/font/sansation_bold_italic.ttf | Bin 0 -> 38740 bytes
.../src/main/res/font/sansation_regular.ttf | Bin 0 -> 36764 bytes
.../res/font/sansation_regular_italic.ttf | Bin 0 -> 38596 bytes
.../design/src/main/res/layout/dialog_web.xml | 28 -
.../main/res/layout/modal_bottom_sheet.xml | 79 --
.../preference_widget_switch_compat.xml | 9 -
.../sysctlgui/data/ExampleInstrumentedTest.kt | 24 -
.../sysctlgui/data/di/DataModule.kt | 14 +-
.../sysctlgui/data/models/KernelParamDTO.kt | 15 +-
.../repository/AppSettingsRepositoryImpl.kt | 3 +-
.../data/repository/ParamsRepositoryImpl.kt | 22 +-
.../data/repository/UserRepositoryImpl.kt | 6 +-
.../source/OnlineDocumentationDataSource.kt | 24 +-
.../data/utils/PresetsFileProcessor.kt | 11 +-
.../sysctlgui/data/ExampleUnitTest.kt | 17 -
.../sysctlgui/domain/enums}/Actions.kt | 5 +-
.../sysctlgui/domain/models/KernelParam.kt | 13 +-
gradle/libs.versions.toml | 19 +-
186 files changed, 5748 insertions(+), 4238 deletions(-)
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/core/navigation/TopLevelRoute.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/core/navigation/UiRoute.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/data/mapper/DomainParamMapper.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/data/models/KernelParam.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/data/models/SettingsItem.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/helpers/OnSettingsItemClickedListener.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/helpers/ParamDiffCallback.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/helpers/SettingsItemDiffCallback.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/helpers/UiKernelParamMapper.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/models/SearchHint.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/models/UiKernelParam.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/base/BaseAppCompatActivity.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/base/BaseSearchFragment.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/base/BaseViewHolder.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/components/ErrorContainer.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/components/SingleChoiceDialog.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/export/ExportOptionsFragment.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/export/ExportOptionsItemAdapter.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/export/ExportOptionsViewEffect.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/export/ExportOptionsViewModel.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/AppNavHost.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainNavBar.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainScreen.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainTopBar.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainViewEffect.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainViewState.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/DocumentationBottomSheet.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/EmptyParamsWarning.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/OnParamItemClickedListener.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/OnPopUpMenuItemSelectedListener.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/BrowseParamsViewModel.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/KernelParamBrowseFragment.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowseItem.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowseScreen.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowseState.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowseViewModel.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowserViewEffect.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowserViewEvent.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowserViewState.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamFileRow.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamRow.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/ActionToggleButton.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditKernelParamActivity.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditParamViewEffect.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditParamViewEvent.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/list/KernelParamListFragment.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/list/ListParamsViewModel.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/list/ParamItem.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/list/ParamViewEffect.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/list/ParamViewEvent.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/list/ParamViewState.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/BaseManageParamsActivity.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/ManageFavoritesParamsActivity.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/ManageOnStartUpParamsActivity.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/UserParamsScreen.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/UserParamsViewEvent.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/UserParamsViewModel.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/UserParamsViewState.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/presets/ImportPresetScreen.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/presets/PresetsScreen.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/presets/PresetsViewModel.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/presets/PresetsViewState.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/search/SearchScreen.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/search/SearchViewModel.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/search/SearchViewState.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/SettingsFragment.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/SettingsScreen.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/SettingsViewModel.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/components/HeaderComponent.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/components/SettingsComponentColumn.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/components/SliderSettingComponent.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/components/SwitchSettingComponent.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/components/TextSettingComponent.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/model/SettingsViewEvent.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/user/UserParamsScreen.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/user/UserParamsViewModel.kt
create mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/ui/user/UserParamsViewState.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/utils/DataBindingUtils.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/utils/KernelParamUtils.kt
delete mode 100644 app/src/main/kotlin/com/androidvip/sysctlgui/utils/ThemeExt.kt
delete mode 100644 app/src/main/res/drawable-night/ic_launcher_background.xml
delete mode 100644 app/src/main/res/drawable/circle_file.xml
delete mode 100644 app/src/main/res/drawable/circle_folder.xml
delete mode 100644 app/src/main/res/drawable/fast_scroll_thumb.xml
delete mode 100644 app/src/main/res/drawable/fast_scroll_track.xml
delete mode 100644 app/src/main/res/drawable/ic_action_tasker.xml
create mode 100644 app/src/main/res/drawable/ic_arrow_upward.xml
delete mode 100644 app/src/main/res/drawable/ic_close.xml
delete mode 100644 app/src/main/res/drawable/ic_config.xml
delete mode 100644 app/src/main/res/drawable/ic_edit_outline.xml
create mode 100644 app/src/main/res/drawable/ic_export.xml
create mode 100644 app/src/main/res/drawable/ic_favorite.xml
create mode 100644 app/src/main/res/drawable/ic_favorite_outlined.xml
create mode 100644 app/src/main/res/drawable/ic_file.xml
delete mode 100644 app/src/main/res/drawable/ic_file_outline.xml
create mode 100644 app/src/main/res/drawable/ic_folder.xml
delete mode 100644 app/src/main/res/drawable/ic_folder_outline.xml
create mode 100644 app/src/main/res/drawable/ic_heart_broken.xml
create mode 100644 app/src/main/res/drawable/ic_history.xml
create mode 100644 app/src/main/res/drawable/ic_import.xml
delete mode 100644 app/src/main/res/drawable/ic_info_outline.xml
delete mode 100644 app/src/main/res/drawable/ic_more_vert.xml
delete mode 100644 app/src/main/res/drawable/ic_name.xml
create mode 100644 app/src/main/res/drawable/ic_open_in_browser.xml
delete mode 100644 app/src/main/res/drawable/ic_search.xml
delete mode 100644 app/src/main/res/drawable/ic_settings_outline.xml
create mode 100644 app/src/main/res/drawable/ic_tasker.xml
create mode 100644 app/src/main/res/drawable/ic_tasker_outlined.xml
delete mode 100644 app/src/main/res/layout-land/activity_main2.xml
delete mode 100644 app/src/main/res/layout/activity_main2.xml
delete mode 100644 app/src/main/res/layout/fragment_export_options.xml
delete mode 100644 app/src/main/res/layout/list_item_settings.xml
delete mode 100644 app/src/main/res/layout/settings_activity.xml
delete mode 100644 app/src/main/res/menu/menu_browse_params.xml
delete mode 100644 app/src/main/res/menu/menu_main.xml
delete mode 100644 app/src/main/res/menu/menu_main_search.xml
delete mode 100644 app/src/main/res/menu/menu_search.xml
delete mode 100644 app/src/main/res/menu/nav_main.xml
delete mode 100644 app/src/main/res/menu/popup_manage_params.xml
delete mode 100644 app/src/main/res/navigation/main_navigation.xml
delete mode 100644 app/src/main/res/values-land/integers.xml
delete mode 100644 app/src/main/res/values-v14/dimens.xml
delete mode 100644 app/src/main/res/values/integers.xml
delete mode 100644 app/src/main/res/xml/preferences.xml
delete mode 100644 buildSrc/src/main/kotlin/AndroidX.kt
delete mode 100644 buildSrc/src/main/kotlin/BuildPlugins.kt
delete mode 100644 buildSrc/src/main/kotlin/Compose.kt
delete mode 100644 buildSrc/src/main/kotlin/Dependencies.kt
delete mode 100644 buildSrc/src/main/kotlin/Google.kt
delete mode 100644 buildSrc/src/main/kotlin/Modules.kt
delete mode 100644 common/design/src/main/java/com/androidvip/sysctlgui/design/BaseBottomSheetFragment.kt
delete mode 100644 common/design/src/main/java/com/androidvip/sysctlgui/design/DesignResources.kt
delete mode 100644 common/design/src/main/java/com/androidvip/sysctlgui/design/ModalBottomSheet.kt
delete mode 100644 common/design/src/main/java/com/androidvip/sysctlgui/design/theme/Shape.kt
create mode 100644 common/design/src/main/java/com/androidvip/sysctlgui/design/theme/Type.kt
create mode 100644 common/design/src/main/res/font/passionone_bold.ttf
create mode 100644 common/design/src/main/res/font/passionone_regular.ttf
create mode 100644 common/design/src/main/res/font/sansation_bold.ttf
create mode 100644 common/design/src/main/res/font/sansation_bold_italic.ttf
create mode 100644 common/design/src/main/res/font/sansation_regular.ttf
create mode 100644 common/design/src/main/res/font/sansation_regular_italic.ttf
delete mode 100644 common/design/src/main/res/layout/dialog_web.xml
delete mode 100644 common/design/src/main/res/layout/modal_bottom_sheet.xml
delete mode 100644 common/design/src/main/res/layout/preference_widget_switch_compat.xml
delete mode 100644 data/src/androidTest/java/com/androidvip/sysctlgui/data/ExampleInstrumentedTest.kt
delete mode 100644 data/src/test/java/com/androidvip/sysctlgui/data/ExampleUnitTest.kt
rename {app/src/main/kotlin/com/androidvip/sysctlgui/helpers => domain/src/main/java/com/androidvip/sysctlgui/domain/enums}/Actions.kt (60%)
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index b202615..1643914 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -6,7 +6,6 @@ plugins {
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.ksp)
alias(libs.plugins.jetbrains.kotlin.serialization)
- kotlin("kapt")
id("kotlin-parcelize")
}
@@ -65,7 +64,6 @@ android {
buildFeatures {
viewBinding = true
- dataBinding = true
compose = true
}
@@ -108,12 +106,14 @@ dependencies {
implementation(libs.kotlin.stdlib)
implementation(libs.kotlinx.coroutines.android)
- implementation(libs.androidx.core.ktx)
implementation(libs.androidx.activity.compose)
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.material)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.window)
+ implementation(libs.androidx.work.runtime.ktx)
// Lifecycle
implementation(libs.androidx.lifecycle.runtime.ktx)
@@ -124,23 +124,7 @@ dependencies {
implementation(libs.androidx.lifecycle.viewmodel.savedstate)
//ksp(libs.androidx.lifecycle.compiler)
- implementation(AndroidX.splashScreen)
- implementation(AndroidX.lifecycleLiveData)
- implementation(AndroidX.navigationFragment)
- implementation(AndroidX.navigationUi)
- implementation(AndroidX.preference)
- implementation(AndroidX.room)
- implementation(AndroidX.roomRuntime)
- implementation(AndroidX.workManager)
- ksp(AndroidX.roomCompiler)
-
- implementation(Google.gson)
-
implementation(libs.koin)
implementation(libs.koin.compose)
implementation(libs.bundles.libsu)
-
- implementation(Dependencies.libSuIo)
- implementation(Dependencies.liveEvent)
- implementation(Dependencies.tapTargetView)
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e683bad..dad076a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -22,25 +22,7 @@
android:theme="@style/AppTheme"
android:enableOnBackInvokedCallback="true"
tools:ignore="GoogleAppIndexingWarning">
-
-
-
-
-
+
(
+ val name: String,
+ val route: T,
+ val selectedIcon: ImageVector,
+ val unselectedIcon: ImageVector
+)
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/core/navigation/UiRoute.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/core/navigation/UiRoute.kt
new file mode 100644
index 0000000..b53090c
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/core/navigation/UiRoute.kt
@@ -0,0 +1,27 @@
+package com.androidvip.sysctlgui.core.navigation
+
+import kotlinx.serialization.Serializable
+
+/**
+ * Represents the different routes in the application's UI.
+ * This is used for navigation purposes.
+ */
+@Serializable
+sealed interface UiRoute {
+ @Serializable
+ data object BrowseParams : UiRoute
+ @Serializable
+ data class EditParam(val paramName: String) : UiRoute
+ @Serializable
+ data object Presets : UiRoute
+ @Serializable
+ data object ImportPresets : UiRoute
+ @Serializable
+ data object Favorites : UiRoute
+ @Serializable
+ data object UserParams : UiRoute
+ @Serializable
+ data object Search : UiRoute
+ @Serializable
+ data object Settings : UiRoute
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/data/mapper/DomainParamMapper.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/data/mapper/DomainParamMapper.kt
deleted file mode 100644
index b60af5b..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/data/mapper/DomainParamMapper.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.androidvip.sysctlgui.data.mapper
-
-import com.androidvip.sysctlgui.data.models.KernelParam
-import com.androidvip.sysctlgui.domain.models.DomainKernelParam
-
-object DomainParamMapper : Mapper {
- override fun map(from: DomainKernelParam): KernelParam = KernelParam().apply {
- id = from.id
- name = from.name
- path = from.path
- value = from.value
- favorite = from.favorite
- taskerParam = from.taskerParam
- taskerList = from.taskerList
- }
-
- override fun unmap(from: KernelParam): DomainKernelParam = DomainKernelParam().apply {
- id = from.id
- name = from.name
- path = from.path
- value = from.value
- favorite = from.favorite
- taskerParam = from.taskerParam
- taskerList = from.taskerList
- }
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/data/models/KernelParam.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/data/models/KernelParam.kt
deleted file mode 100644
index 5843783..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/data/models/KernelParam.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.androidvip.sysctlgui.data.models
-
-import android.os.Parcelable
-import com.androidvip.sysctlgui.domain.models.DomainKernelParam
-import com.androidvip.sysctlgui.utils.Consts
-import kotlinx.parcelize.Parcelize
-
-@Parcelize
-data class KernelParam(
- override var id: Int = 0,
- override var name: String = "",
- override var path: String = "",
- override var value: String = "",
- override var favorite: Boolean = false,
- override var taskerParam: Boolean = false,
- override var taskerList: Int = Consts.LIST_NUMBER_PRIMARY_TASKER
-) : DomainKernelParam(), Parcelable {
- override fun toString(): String {
- if (name.isEmpty()) setNameFromPath(path)
- return "$name = $value"
- }
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/data/models/SettingsItem.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/data/models/SettingsItem.kt
deleted file mode 100644
index edba905..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/data/models/SettingsItem.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.androidvip.sysctlgui.data.models
-
-import androidx.annotation.DrawableRes
-import androidx.annotation.StringRes
-
-data class SettingsItem(
- @StringRes val titleRes: Int,
- @StringRes val descriptionRes: Int,
- @DrawableRes val iconRes: Int
-)
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/di/PresentationModule.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/di/PresentationModule.kt
index 0acae81..87aac32 100644
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/di/PresentationModule.kt
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/di/PresentationModule.kt
@@ -1,24 +1,25 @@
package com.androidvip.sysctlgui.di
-import com.androidvip.sysctlgui.ui.export.ExportOptionsViewModel
import com.androidvip.sysctlgui.ui.main.MainViewModel
-import com.androidvip.sysctlgui.ui.params.browse.BrowseParamsViewModel
+import com.androidvip.sysctlgui.ui.params.browse.ParamBrowseViewModel
import com.androidvip.sysctlgui.ui.params.edit.EditParamViewModel
-import com.androidvip.sysctlgui.ui.params.list.ListParamsViewModel
-import com.androidvip.sysctlgui.ui.params.user.UserParamsViewModel
+import com.androidvip.sysctlgui.ui.presets.PresetsViewModel
+import com.androidvip.sysctlgui.ui.search.SearchViewModel
+import com.androidvip.sysctlgui.ui.settings.SettingsViewModel
+import com.androidvip.sysctlgui.ui.user.UserParamsViewModel
import com.androidvip.sysctlgui.widgets.FavoriteWidgetParamUpdater
import org.koin.android.ext.koin.androidContext
-import org.koin.androidx.viewmodel.dsl.viewModel
-import org.koin.androidx.viewmodel.dsl.viewModelOf
+import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.module
-internal val presentationModules = module {
- viewModel { BrowseParamsViewModel(get(), get()) }
- viewModelOf(::ListParamsViewModel)
- viewModelOf(::UserParamsViewModel)
- viewModelOf(::EditParamViewModel)
+internal val presentationModule = module {
viewModelOf(::MainViewModel)
- viewModel { ExportOptionsViewModel(get(), get(), get()) }
+ viewModelOf(::SettingsViewModel)
+ viewModelOf(::ParamBrowseViewModel)
+ viewModelOf(::EditParamViewModel)
+ viewModelOf(::SearchViewModel)
+ viewModelOf(::PresetsViewModel)
+ viewModelOf(::UserParamsViewModel)
single { FavoriteWidgetParamUpdater(androidContext()).getListener() }
}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/helpers/OnSettingsItemClickedListener.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/helpers/OnSettingsItemClickedListener.kt
deleted file mode 100644
index 97e3be3..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/helpers/OnSettingsItemClickedListener.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.androidvip.sysctlgui.helpers
-
-import com.androidvip.sysctlgui.data.models.SettingsItem
-
-interface OnSettingsItemClickedListener {
- fun onSettingsItemClicked(item: SettingsItem, position: Int)
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/helpers/ParamDiffCallback.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/helpers/ParamDiffCallback.kt
deleted file mode 100644
index cec3940..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/helpers/ParamDiffCallback.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.androidvip.sysctlgui.helpers
-
-import androidx.recyclerview.widget.DiffUtil
-import com.androidvip.sysctlgui.data.models.KernelParam
-
-class ParamDiffCallback : DiffUtil.ItemCallback() {
- override fun areItemsTheSame(oldItem: KernelParam, newItem: KernelParam): Boolean {
- return oldItem.name == newItem.name
- }
-
- override fun areContentsTheSame(oldItem: KernelParam, newItem: KernelParam): Boolean {
- return oldItem.name == newItem.name
- && oldItem.path == newItem.path
- && oldItem.value == newItem.value
- && oldItem.favorite == newItem.favorite
- && oldItem.taskerParam == newItem.taskerParam
- }
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/helpers/SettingsItemDiffCallback.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/helpers/SettingsItemDiffCallback.kt
deleted file mode 100644
index baab603..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/helpers/SettingsItemDiffCallback.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.androidvip.sysctlgui.helpers
-
-import androidx.recyclerview.widget.DiffUtil
-import com.androidvip.sysctlgui.data.models.SettingsItem
-
-internal object SettingsItemDiffCallback : DiffUtil.ItemCallback() {
- override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean {
- return oldItem.titleRes == newItem.titleRes
- }
-
- override fun areContentsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean {
- return oldItem == newItem
- }
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/helpers/UiKernelParamMapper.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/helpers/UiKernelParamMapper.kt
new file mode 100644
index 0000000..04dbaa2
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/helpers/UiKernelParamMapper.kt
@@ -0,0 +1,17 @@
+package com.androidvip.sysctlgui.helpers
+
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import com.androidvip.sysctlgui.models.UiKernelParam
+
+object UiKernelParamMapper {
+ fun map(param: KernelParam): UiKernelParam {
+ return UiKernelParam(
+ name = param.name,
+ path = param.path,
+ value = param.value,
+ isFavorite = param.isFavorite,
+ isTaskerParam = param.isTaskerParam,
+ taskerList = param.taskerList
+ )
+ }
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/models/SearchHint.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/models/SearchHint.kt
new file mode 100644
index 0000000..881e305
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/models/SearchHint.kt
@@ -0,0 +1,15 @@
+package com.androidvip.sysctlgui.models
+
+/**
+ * Represents a search hint displayed to the user.
+ *
+ * This holds information about a single search suggestion, including the text of the hint
+ * and whether it originates from the user's search history.
+ *
+ * @property hint The text of the search hint.
+ * @property isFromHistory A boolean flag indicating whether the hint is from the user's search history
+ */
+data class SearchHint(
+ val hint: String,
+ val isFromHistory: Boolean = false
+)
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/models/UiKernelParam.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/models/UiKernelParam.kt
new file mode 100644
index 0000000..379d49d
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/models/UiKernelParam.kt
@@ -0,0 +1,58 @@
+package com.androidvip.sysctlgui.models
+
+import android.os.Build
+import android.os.Parcelable
+import androidx.compose.runtime.Stable
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import com.androidvip.sysctlgui.utils.Consts
+import kotlinx.parcelize.IgnoredOnParcel
+import kotlinx.parcelize.Parcelize
+import java.io.File
+import java.nio.file.Paths
+import kotlin.io.path.isDirectory
+
+/**
+ * Represents a kernel parameter with additional UI-specific properties.
+ */
+@Stable
+@Parcelize
+data class UiKernelParam(
+ override val name: String = "",
+ override val path: String = "",
+ override val value: String = "",
+ override val isFavorite: Boolean = false,
+ override val isTaskerParam: Boolean = false,
+ override val taskerList: Int = Consts.LIST_NUMBER_PRIMARY_TASKER
+) : KernelParam(name, path, value, isFavorite, isTaskerParam, taskerList), Parcelable {
+
+ /**
+ * Lazily determines if the [path] represents a directory.
+ * Uses [Paths] for Android O and above, otherwise falls back to [File].
+ */
+ @IgnoredOnParcel
+ val isDirectory by lazy {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ Paths.get(path).isDirectory()
+ } else {
+ File(path).isDirectory
+ }
+ }
+
+ /**
+ * The last segment of the parameter's name or path.
+ *
+ * If the parameter represents a directory, this will be the last segment of its [path]
+ * after the last `/`. For example: `/proc/sys/vm/` -> `vm`.
+ *
+ * If the parameter represents a file, this will be the last segment of its [name]
+ * after the last `.`. For example: `vm.swappiness` -> `swappiness`.
+ *
+ * If there is no `.` in the name, the full [name] is returned.
+ */
+ override val lastNameSegment: String
+ get() = if (isDirectory) {
+ path.substringAfterLast('/')
+ } else {
+ name.substringAfterLast('.', name)
+ }
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/services/tiles/StartAppTileService.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/services/tiles/StartAppTileService.kt
index 1c9bb6b..0cbebac 100644
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/services/tiles/StartAppTileService.kt
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/services/tiles/StartAppTileService.kt
@@ -1,23 +1,39 @@
package com.androidvip.sysctlgui.services.tiles
+import android.annotation.SuppressLint
+import android.app.PendingIntent
import android.content.Intent
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.annotation.RequiresApi
+import androidx.core.service.quicksettings.PendingIntentActivityWrapper
+import androidx.core.service.quicksettings.TileServiceCompat
import com.androidvip.sysctlgui.ui.start.StartActivity
-@RequiresApi(Build.VERSION_CODES.N)
+
class StartAppTileService : TileService() {
+
+ @SuppressLint("StartActivityAndCollapseDeprecated")
override fun onClick() {
super.onClick()
qsTile.apply {
state = Tile.STATE_INACTIVE
updateTile()
}
- startActivityAndCollapse(
- Intent(this, StartActivity::class.java)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
+
+ val intent = Intent(this, StartActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
+ }
+
+ val wrapper = PendingIntentActivityWrapper(
+ this,
+ 0,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT,
+ false
)
+
+ TileServiceCompat.startActivityAndCollapse(this, wrapper)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/services/tiles/StartUpTileService.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/services/tiles/StartUpTileService.kt
index b73db99..84aee78 100644
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/services/tiles/StartUpTileService.kt
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/services/tiles/StartUpTileService.kt
@@ -1,55 +1,69 @@
package com.androidvip.sysctlgui.services.tiles
-import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import android.widget.Toast
-import androidx.annotation.RequiresApi
import com.androidvip.sysctlgui.R
+import com.androidvip.sysctlgui.data.utils.RootUtils
import com.androidvip.sysctlgui.domain.repository.AppPrefs
import com.androidvip.sysctlgui.helpers.StartUpServiceToggle
-import com.topjohnwu.superuser.Shell
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
-@RequiresApi(Build.VERSION_CODES.N)
class StartUpTileService : TileService(), KoinComponent {
private val prefs: AppPrefs by inject()
+ private val rootUtils: RootUtils by inject()
+ private val serviceJob = Job()
+ private val serviceScope = CoroutineScope(Dispatchers.Main.immediate + serviceJob)
+
override fun onStartListening() {
super.onStartListening()
- qsTile.apply {
- if (Shell.rootAccess()) {
- state = if (isStartUpEnabled()) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
- } else {
- state = Tile.STATE_UNAVAILABLE
- label = resources.getString(R.string.tile_toggle_start_up_no_root_access_label)
+ serviceScope.launch {
+ qsTile.apply {
+ if (rootUtils.isRootAvailable()) {
+ state = if (isStartUpEnabled()) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
+ } else {
+ state = Tile.STATE_UNAVAILABLE
+ label = resources.getString(R.string.tile_toggle_start_up_no_root_access_label)
+ }
+ updateTile()
}
- updateTile()
}
}
override fun onClick() {
super.onClick()
- if (!Shell.rootAccess()) {
- Toast.makeText(
- this,
- resources.getString(R.string.tile_toggle_start_up_no_root_access_toast),
- Toast.LENGTH_LONG
- ).show()
- return
- }
+ serviceScope.launch {
+ if (!rootUtils.isRootAvailable()) {
+ Toast.makeText(
+ this@StartUpTileService,
+ resources.getString(R.string.tile_toggle_start_up_no_root_access_toast),
+ Toast.LENGTH_LONG
+ ).show()
+ return@launch
+ }
- toggleService(isStartUpEnabled().not())
+ toggleService(isStartUpEnabled().not())
- qsTile.apply {
- state = if (isStartUpEnabled()) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
- updateTile()
+ qsTile.apply {
+ state = if (isStartUpEnabled()) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
+ updateTile()
+ }
}
}
+ override fun onDestroy() {
+ super.onDestroy()
+ serviceJob.cancel()
+ }
+
private fun isStartUpEnabled() = prefs.runOnStartUp
private fun toggleService(enabled: Boolean) {
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/base/BaseAppCompatActivity.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/base/BaseAppCompatActivity.kt
deleted file mode 100644
index 12896ea..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/base/BaseAppCompatActivity.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.androidvip.sysctlgui.ui.base
-
-import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.view.WindowCompat
-import com.androidvip.sysctlgui.design.DesignStyles
-import com.androidvip.sysctlgui.domain.repository.AppPrefs
-import org.koin.android.ext.android.inject
-
-/**
- * Base activity that uses AppCompat for theming
- * TODO: Temporary until 100% compose
- */
-abstract class BaseAppCompatActivity : AppCompatActivity() {
- protected val prefs by inject()
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- WindowCompat.setDecorFitsSystemWindows(window, false)
-
- if (prefs.forceDark) {
- setTheme(DesignStyles.AppTheme_ForceDark)
- }
- }
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/base/BaseSearchFragment.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/base/BaseSearchFragment.kt
deleted file mode 100644
index be07193..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/base/BaseSearchFragment.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-package com.androidvip.sysctlgui.ui.base
-
-import android.os.Bundle
-import android.view.Menu
-import android.view.MenuInflater
-import android.widget.SearchView
-import androidx.fragment.app.Fragment
-import com.androidvip.sysctlgui.R
-
-abstract class BaseSearchFragment : Fragment() {
- protected var searchExpression: String = ""
- private var searchView: SearchView? = null
-
- abstract fun onQueryTextChanged()
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setHasOptionsMenu(true)
- }
-
- override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
- super.onCreateOptionsMenu(menu, inflater)
-
- inflater.inflate(R.menu.menu_search, menu)
- setUpSearchView(menu)
- }
-
- protected fun setUpSearchView(menu: Menu?) {
- searchView = (menu?.findItem(R.id.action_search)?.actionView as? SearchView)?.apply {
- setOnQueryTextListener(
- object :
- androidx.appcompat.widget.SearchView.OnQueryTextListener,
- SearchView.OnQueryTextListener {
- override fun onQueryTextSubmit(query: String?): Boolean {
- return true
- }
-
- override fun onQueryTextChange(newText: String?): Boolean {
- searchExpression = newText.orEmpty().replace(".", "")
-
- this@BaseSearchFragment.onQueryTextChanged()
- return true
- }
- }
- )
-
- // expand and show keyboard
- isIconifiedByDefault = false
- onActionViewExpanded()
- }
- }
-
- protected fun resetSearchExpression() {
- searchExpression = ""
- searchView?.setQuery("", false)
- }
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/base/BaseViewHolder.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/base/BaseViewHolder.kt
deleted file mode 100644
index 8aab4cb..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/base/BaseViewHolder.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.androidvip.sysctlgui.ui.base
-
-import androidx.databinding.ViewDataBinding
-import androidx.recyclerview.widget.RecyclerView
-
-abstract class BaseViewHolder(
- binding: ViewDataBinding
-) : RecyclerView.ViewHolder(binding.root) {
- abstract fun bind(item: T, position: Int)
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/components/ErrorContainer.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/components/ErrorContainer.kt
new file mode 100644
index 0000000..d1f6940
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/components/ErrorContainer.kt
@@ -0,0 +1,96 @@
+package com.androidvip.sysctlgui.ui.components
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Close
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+private const val ERROR_CONTAINER_ANIMATION_DURATION = 4000
+
+@Composable
+internal fun ErrorContainer(message: String, onAnimationEnd: () -> Unit) {
+ var animationStarted by remember { mutableStateOf(false) }
+ val progressTarget = if (animationStarted) 0f else 1f
+
+ val progress by animateFloatAsState(
+ targetValue = progressTarget,
+ animationSpec = tween(
+ durationMillis = ERROR_CONTAINER_ANIMATION_DURATION,
+ easing = LinearEasing
+ ),
+ finishedListener = { value ->
+ if (value == 0f) {
+ onAnimationEnd()
+ }
+ }
+ )
+
+ LaunchedEffect(Unit) { animationStarted = true }
+
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.errorContainer,
+ contentColor = MaterialTheme.colorScheme.onErrorContainer
+ )
+ ) {
+ Box {
+ LinearProgressIndicator(
+ progress = { progress },
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .fillMaxWidth(),
+ color = MaterialTheme.colorScheme.onErrorContainer,
+ trackColor = MaterialTheme.colorScheme.errorContainer
+ )
+
+ Row(
+ modifier = Modifier.padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Icon(
+ modifier = Modifier.size(40.dp),
+ imageVector = Icons.Rounded.Close,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onErrorContainer
+ )
+ Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
+ Text(
+ text = "Error",
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onErrorContainer
+ )
+ Text(
+ text = message,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onErrorContainer
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/components/SingleChoiceDialog.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/components/SingleChoiceDialog.kt
new file mode 100644
index 0000000..45ddb18
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/components/SingleChoiceDialog.kt
@@ -0,0 +1,77 @@
+package com.androidvip.sysctlgui.ui.components
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.RadioButton
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+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.unit.dp
+import android.R as AndroidResources
+
+@Composable
+fun SingleChoiceDialog(
+ showDialog: Boolean,
+ title: String,
+ options: List,
+ initialSelectedOptionIndex: Int,
+ onDismissRequest: () -> Unit,
+ onOptionSelected: (Int) -> Unit
+) {
+ if (showDialog) {
+ var selectedOptionIndex by remember { mutableIntStateOf(initialSelectedOptionIndex) }
+
+ AlertDialog(
+ onDismissRequest = onDismissRequest,
+ title = { Text(text = title) },
+ text = {
+ Column {
+ options.forEachIndexed { index, option ->
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { selectedOptionIndex = index }
+ .padding(vertical = 4.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ RadioButton(
+ selected = (index == selectedOptionIndex),
+ onClick = { selectedOptionIndex = index }
+ )
+ Text(
+ text = option,
+ modifier = Modifier.padding(start = 4.dp)
+ )
+ }
+ }
+ }
+ },
+ confirmButton = {
+ TextButton(
+ onClick = {
+ onOptionSelected(selectedOptionIndex)
+ onDismissRequest()
+ }
+ ) {
+ Text(stringResource(AndroidResources.string.ok))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismissRequest) {
+ Text(stringResource(AndroidResources.string.cancel))
+ }
+ }
+ )
+ }
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/export/ExportOptionsFragment.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/export/ExportOptionsFragment.kt
deleted file mode 100644
index 4f2a3ba..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/export/ExportOptionsFragment.kt
+++ /dev/null
@@ -1,152 +0,0 @@
-package com.androidvip.sysctlgui.ui.export
-
-import android.app.Activity
-import android.content.Intent
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.annotation.StringRes
-import androidx.fragment.app.Fragment
-import com.androidvip.sysctlgui.R
-import com.androidvip.sysctlgui.data.models.SettingsItem
-import com.androidvip.sysctlgui.databinding.FragmentExportOptionsBinding
-import com.androidvip.sysctlgui.design.ModalBottomSheet
-import com.androidvip.sysctlgui.helpers.OnSettingsItemClickedListener
-import com.androidvip.sysctlgui.toast
-import org.koin.androidx.viewmodel.ext.android.viewModel
-
-class ExportOptionsFragment : Fragment(), OnSettingsItemClickedListener {
- private var _binding: FragmentExportOptionsBinding? = null
- private val binding get() = _binding!!
- private val viewModel: ExportOptionsViewModel by viewModel()
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- _binding = FragmentExportOptionsBinding.inflate(inflater, container, false)
- return binding.root
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- val adapter = ExportOptionsItemAdapter(this)
- binding.recyclerView.adapter = adapter
- adapter.submitList(viewModel.getBackOptionItems())
-
- observeUi()
- }
-
- override fun onDestroyView() {
- super.onDestroyView()
- _binding = null
- }
-
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- if (resultCode != Activity.RESULT_OK) return toast(R.string.error)
-
- when (requestCode) {
- RC_IMPORT_USER_PARAMS,
- RC_RESTORE_PARAMS -> {
- val uri = data?.data ?: return toast(R.string.import_error)
- val extension = uri.lastPathSegment.orEmpty()
- val stream = requireContext().contentResolver.openInputStream(uri)
- ?: return toast(R.string.import_error)
- viewModel.importParams(stream, extension)
- }
-
- RC_EXPORT_USER_PARAMS -> {
- val uri = data?.data ?: return toast(R.string.export_error)
- viewModel.exportParams(uri, requireContext(), false)
- }
-
- RC_BACKUP_PARAMS -> {
- val uri = data?.data ?: return toast(R.string.export_error)
- viewModel.exportParams(uri, requireContext(), true)
- }
- }
- super.onActivityResult(requestCode, resultCode, data)
- }
-
- override fun onSettingsItemClicked(item: SettingsItem, position: Int) {
- when (position) {
- 0 -> viewModel.doWhenImportUserParamsPressed()
- 1 -> viewModel.doWhenExportUserParamsPressed()
- 2 -> viewModel.doWhenBackupPressed()
- 3 -> viewModel.doWhenRestorePressed()
- }
- }
-
- private fun observeUi() {
- viewModel.viewEffect.observe(viewLifecycleOwner) {
- when (it) {
- is ExportOptionsViewEffect.ImportUserParams -> requestImportFile(RC_IMPORT_USER_PARAMS)
- is ExportOptionsViewEffect.ExportUserParams -> requestExportFile(RC_EXPORT_USER_PARAMS)
- is ExportOptionsViewEffect.RestoreRuntimeParams -> requestImportFile(RC_RESTORE_PARAMS)
- is ExportOptionsViewEffect.BackupRuntimeParams -> requestExportFile(RC_BACKUP_PARAMS)
- is ExportOptionsViewEffect.ShowImportError -> showErrorModal(it.messageRes)
- is ExportOptionsViewEffect.ShowImportSuccess -> showSuccessModal(
- getString(R.string.import_success_message, it.paramCount)
- )
- is ExportOptionsViewEffect.ShowExportError -> showErrorModal(it.messageRes)
- is ExportOptionsViewEffect.ShowExportSuccess -> showSuccessModal(
- getString(R.string.export_success_message)
- )
- }
- }
-
- viewModel.viewState.observe(viewLifecycleOwner) {
- binding.progress.visibility = if (it.isLoading) View.VISIBLE else View.GONE
- binding.loadingText.visibility = if (it.isLoading) View.VISIBLE else View.GONE
- binding.recyclerView.visibility = if (it.isLoading) View.GONE else View.VISIBLE
- }
- }
-
- private fun requestImportFile(requestCode: Int) {
- val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
- addCategory(Intent.CATEGORY_OPENABLE)
- type = "*/*"
- }
- startActivityForResult(intent, requestCode)
- }
-
- private fun requestExportFile(requestCode: Int) {
- val extension = if (requestCode == RC_BACKUP_PARAMS) "conf" else "json"
- val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
- addCategory(Intent.CATEGORY_OPENABLE)
- type = "*/*"
- putExtra(Intent.EXTRA_TITLE, "params.$extension")
- }
- startActivityForResult(intent, requestCode)
- }
-
- private fun showErrorModal(@StringRes messageRes: Int) {
- ModalBottomSheet.newInstance(
- getString(R.string.error),
- getString(messageRes),
- getString(android.R.string.ok)
- ).also {
- if (isAdded) it.show(childFragmentManager, "sheet")
- }
- }
-
- private fun showSuccessModal(message: String) {
- ModalBottomSheet.newInstance(
- getString(R.string.done),
- message,
- getString(android.R.string.ok)
- ).also {
- if (isAdded) it.show(childFragmentManager, "sheet")
- }
- }
-
- companion object {
- private const val RC_IMPORT_USER_PARAMS: Int = 1
- private const val RC_EXPORT_USER_PARAMS: Int = 2
- private const val RC_BACKUP_PARAMS: Int = 3
- private const val RC_RESTORE_PARAMS: Int = 4
- }
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/export/ExportOptionsItemAdapter.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/export/ExportOptionsItemAdapter.kt
deleted file mode 100644
index b031975..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/export/ExportOptionsItemAdapter.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.androidvip.sysctlgui.ui.export
-
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import androidx.recyclerview.widget.ListAdapter
-import com.androidvip.sysctlgui.data.models.SettingsItem
-import com.androidvip.sysctlgui.databinding.ListItemSettingsBinding
-import com.androidvip.sysctlgui.helpers.OnSettingsItemClickedListener
-import com.androidvip.sysctlgui.helpers.SettingsItemDiffCallback
-import com.androidvip.sysctlgui.ui.base.BaseViewHolder
-
-class ExportOptionsItemAdapter(
- private val itemClickedListener: OnSettingsItemClickedListener
-) : ListAdapter>(SettingsItemDiffCallback) {
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeItemViewHolder {
- val inflater = LayoutInflater.from(parent.context)
- val binding = ListItemSettingsBinding.inflate(
- inflater, parent, false
- )
- return HomeItemViewHolder(binding)
- }
-
- override fun onBindViewHolder(holder: BaseViewHolder<*>, position: Int) {
- if (holder is HomeItemViewHolder) {
- holder.bind(getItem(position), position)
- }
- }
-
- inner class HomeItemViewHolder(
- private val binding: ListItemSettingsBinding
- ) : BaseViewHolder(binding) {
- override fun bind(item: SettingsItem, position: Int) {
- binding.item = item
- binding.position = position
- binding.onSettingsItemClickedListener = itemClickedListener
- binding.executePendingBindings()
- }
- }
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/export/ExportOptionsViewEffect.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/export/ExportOptionsViewEffect.kt
deleted file mode 100644
index 8767785..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/export/ExportOptionsViewEffect.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.androidvip.sysctlgui.ui.export
-
-import androidx.annotation.StringRes
-
-sealed interface ExportOptionsViewEffect {
- object ImportUserParams : ExportOptionsViewEffect
- object ExportUserParams : ExportOptionsViewEffect
-
- object BackupRuntimeParams : ExportOptionsViewEffect
- object RestoreRuntimeParams : ExportOptionsViewEffect
-
- class ShowImportError(@StringRes val messageRes: Int) : ExportOptionsViewEffect
- class ShowImportSuccess(val paramCount: Int) : ExportOptionsViewEffect
-
- class ShowExportError(@StringRes val messageRes: Int) : ExportOptionsViewEffect
- object ShowExportSuccess : ExportOptionsViewEffect
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/export/ExportOptionsViewModel.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/export/ExportOptionsViewModel.kt
deleted file mode 100644
index 9caec46..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/export/ExportOptionsViewModel.kt
+++ /dev/null
@@ -1,160 +0,0 @@
-package com.androidvip.sysctlgui.ui.export
-
-import android.content.Context
-import android.net.Uri
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import com.androidvip.sysctlgui.R
-import com.androidvip.sysctlgui.data.models.SettingsItem
-import com.androidvip.sysctlgui.domain.exceptions.EmptyFileException
-import com.androidvip.sysctlgui.domain.exceptions.InvalidFileExtensionException
-import com.androidvip.sysctlgui.domain.exceptions.MalformedLineException
-import com.androidvip.sysctlgui.domain.exceptions.NoParameterFoundException
-import com.androidvip.sysctlgui.domain.exceptions.NoValidParamException
-import com.androidvip.sysctlgui.utils.ViewState
-import com.androidvip.sysctlgui.domain.usecase.BackupParamsUseCase
-import com.androidvip.sysctlgui.domain.usecase.ExportParamsUseCase
-import com.androidvip.sysctlgui.domain.usecase.ImportParamsUseCase
-import com.google.gson.JsonParseException
-import com.google.gson.JsonSyntaxException
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import java.io.IOException
-import java.io.InputStream
-
-class ExportOptionsViewModel(
- private val importParamsUseCase: ImportParamsUseCase,
- private val exportParamsUseCase: ExportParamsUseCase,
- private val backupParamsUseCase: BackupParamsUseCase,
- private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
-) : ViewModel() {
- private val _viewEffect = MutableLiveData()
- internal val viewEffect: LiveData = _viewEffect
-
- private val _viewState = MutableLiveData>()
- val viewState: LiveData> = _viewState
-
- fun getBackOptionItems(): List = listOf(
- SettingsItem(
- R.string.import_parameters,
- R.string.read_from_file_sum,
- R.drawable.ic_import_params
- ),
- SettingsItem(
- R.string.export_parameters,
- R.string.export_parameters_sum,
- R.drawable.ic_export_params
- ),
- SettingsItem(
- R.string.backup_parameters,
- R.string.backup_parameters_sum,
- R.drawable.ic_backup_params
- ),
- SettingsItem(
- R.string.restore_parameters,
- R.string.restore_parameters_sum,
- R.drawable.ic_restore_params
- )
- )
-
- fun doWhenImportUserParamsPressed() = _viewEffect.postValue(
- ExportOptionsViewEffect.ImportUserParams
- )
-
- fun doWhenExportUserParamsPressed() = _viewEffect.postValue(
- ExportOptionsViewEffect.ExportUserParams
- )
-
- fun doWhenBackupPressed() = _viewEffect.postValue(ExportOptionsViewEffect.BackupRuntimeParams)
-
- fun doWhenRestorePressed() = _viewEffect.postValue(ExportOptionsViewEffect.RestoreRuntimeParams)
-
- fun importParams(stream: InputStream, fileExtension: String) = viewModelScope.launch {
- _viewState.postValue(currentViewState.copyState(isLoading = true))
-
- val postError: (Int) -> Unit = {
- _viewEffect.postValue(ExportOptionsViewEffect.ShowImportError(it))
- }
- val result = runCatching { importParamsUseCase(stream, fileExtension) }
- when (result.exceptionOrNull()) {
- is JsonParseException,
- is JsonSyntaxException -> postError(R.string.import_error_invalid_json)
-
- is InvalidFileExtensionException -> postError(R.string.import_error_invalid_file_type)
-
- is EmptyFileException -> postError(R.string.import_error_empty_file)
-
- is MalformedLineException -> postError(R.string.import_error_malformed_line)
-
- is NoValidParamException -> postError(R.string.no_parameters_found)
-
- null -> {
- val successfulParams = result.getOrNull().orEmpty()
- _viewEffect.postValue(
- ExportOptionsViewEffect.ShowImportSuccess(successfulParams.size)
- )
- }
- else -> postError(R.string.import_error)
- }
-
- _viewState.postValue(currentViewState.copyState(isLoading = false))
- }
-
- fun exportParams(target: Uri, context: Context, backup: Boolean) = viewModelScope.launch {
- _viewState.postValue(currentViewState.copyState(isLoading = true))
-
- val postError: (Int) -> Unit = {
- _viewEffect.postValue(ExportOptionsViewEffect.ShowExportError(it))
- }
- val result = if (backup) {
- backUpParamsWithFileDescriptor(target, context)
- } else {
- exportParamsWithFileDescriptor(target, context)
- }
-
- result.exceptionOrNull()?.printStackTrace()
-
- when (result.exceptionOrNull()) {
- is IOException -> postError(R.string.export_error_io)
-
- is NoParameterFoundException -> postError(R.string.export_error_no_param)
-
- is EmptyFileException -> postError(R.string.import_error_empty_file)
-
- null -> _viewEffect.postValue(ExportOptionsViewEffect.ShowExportSuccess)
-
- else -> postError(R.string.export_error)
- }
-
- _viewState.postValue(currentViewState.copyState(isLoading = false))
- }
-
- private suspend fun exportParamsWithFileDescriptor(
- target: Uri,
- context: Context
- ): Result = withContext(ioDispatcher) {
- return@withContext runCatching {
- context.contentResolver.openFileDescriptor(target, "w").use {
- exportParamsUseCase(it!!.fileDescriptor)
- }
- }
- }
-
- private suspend fun backUpParamsWithFileDescriptor(
- target: Uri,
- context: Context
- ): Result = withContext(ioDispatcher) {
- return@withContext runCatching {
- context.contentResolver.openFileDescriptor(target, "w").use {
- backupParamsUseCase(it!!.fileDescriptor)
- }
- }
- }
-
- private val currentViewState: ViewState
- get() = viewState.value ?: ViewState()
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/AppNavHost.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/AppNavHost.kt
new file mode 100644
index 0000000..b01c59b
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/AppNavHost.kt
@@ -0,0 +1,90 @@
+package com.androidvip.sysctlgui.ui.main
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalView
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import com.androidvip.sysctlgui.core.navigation.UiRoute
+import com.androidvip.sysctlgui.ui.user.UserParamsScreen
+import com.androidvip.sysctlgui.ui.params.browse.ParamBrowseScreen
+import com.androidvip.sysctlgui.ui.params.browse.ParamBrowseScreenContentPreview
+import com.androidvip.sysctlgui.ui.params.edit.EditParamScreen
+import com.androidvip.sysctlgui.ui.presets.ImportPresetScreen
+import com.androidvip.sysctlgui.ui.presets.PresetsScreen
+import com.androidvip.sysctlgui.ui.search.SearchScreen
+import com.androidvip.sysctlgui.ui.settings.SettingsScreen
+
+@Composable
+internal fun AppNavHost(innerPadding: PaddingValues, navController: NavHostController) {
+ NavHost(
+ modifier = Modifier.padding(innerPadding),
+ navController = navController,
+ startDestination = UiRoute.BrowseParams
+ ) {
+ composable {
+ if (LocalView.current.isInEditMode) {
+ ParamBrowseScreenContentPreview()
+ } else {
+ ParamBrowseScreen(
+ onParamSelected = {
+ navController.navigate(UiRoute.EditParam(paramName = it.name))
+ }
+ )
+ }
+ }
+
+ composable {
+ EditParamScreen(onNavigateBack = { navController.popBackStack() })
+ }
+
+ composable {
+ PresetsScreen(
+ onNavigateBack = { navController.popBackStack() },
+ onNavigateToImport = { navController.navigate(UiRoute.ImportPresets) }
+ )
+ }
+
+ composable {
+ ImportPresetScreen(onNavigateBack = { navController.popBackStack() })
+ }
+
+ composable {
+ UserParamsScreen(
+ filterPredicate = { it.isFavorite },
+ onParamSelected = {
+ navController.navigate(UiRoute.EditParam(paramName = it.name))
+ }
+ )
+ }
+
+ composable {
+ SearchScreen(
+ onParamSelected = {
+ navController.navigate(UiRoute.EditParam(paramName = it.name))
+ },
+ onNavigateBack = { navController.popBackStack() }
+ )
+ }
+
+ composable {
+ SettingsScreen(
+ onNavigateToUserParams = {
+ navController.navigate(UiRoute.UserParams)
+ }
+ )
+ }
+
+ composable {
+ UserParamsScreen(
+ filterPredicate = { true },
+ onParamSelected = {
+ navController.navigate(UiRoute.EditParam(paramName = it.name))
+ }
+ )
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainActivity.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainActivity.kt
index 840b03d..d00fece 100644
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainActivity.kt
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainActivity.kt
@@ -1,78 +1,48 @@
package com.androidvip.sysctlgui.ui.main
import android.app.NotificationManager
-import android.content.Context
import android.os.Build
import android.os.Bundle
import android.os.Handler
-import android.view.MenuItem
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.isSystemInDarkTheme
import androidx.core.os.postDelayed
-import androidx.core.view.WindowCompat
-import androidx.navigation.fragment.NavHostFragment
-import androidx.navigation.ui.AppBarConfiguration
-import androidx.navigation.ui.setupWithNavController
-import com.androidvip.sysctlgui.R
-import com.androidvip.sysctlgui.databinding.ActivityMain2Binding
-import com.androidvip.sysctlgui.helpers.Actions
-import com.androidvip.sysctlgui.ui.base.BaseAppCompatActivity
-
-class MainActivity : BaseAppCompatActivity() {
- private lateinit var binding: ActivityMain2Binding
+import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
+import com.androidvip.sysctlgui.domain.repository.AppPrefs
+import org.koin.android.ext.android.inject
+class MainActivity : ComponentActivity() {
+ private val prefs: AppPrefs by inject()
private val notificationPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { _ -> }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- WindowCompat.setDecorFitsSystemWindows(window, false)
-
- binding = ActivityMain2Binding.inflate(layoutInflater)
- setContentView(binding.root)
- setSupportActionBar(binding.toolbar)
+ enableEdgeToEdge()
+
+ setContent {
+ SysctlGuiTheme(
+ darkTheme = prefs.forceDark || isSystemInDarkTheme(),
+ contrastLevel = prefs.contrastLevel,
+ dynamicColor = prefs.dynamicColors
+ ) {
+ MainScreen()
+ }
+ }
Handler(mainLooper).postDelayed(1000) {
checkNotificationPermission()
}
- setUpNavigation()
navigateFromIntent()
}
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- when (item.itemId) {
- android.R.id.home -> finish()
-
- R.id.action_exit -> {
- moveTaskToBack(true)
- finish()
- }
- }
-
- return false // Let fragments have a chance to consume it
- }
-
- override fun onSupportNavigateUp(): Boolean {
- return navHost.navController.navigateUp() || super.onSupportNavigateUp()
- }
-
- private fun setUpNavigation() = with(binding) {
- val navController = navHost.navController
- val defaultIds = setOf(
- R.id.navigationBrowse,
- R.id.navigationList,
- R.id.navigationExport,
- R.id.navigationSettings
- )
- val appBarConfiguration = AppBarConfiguration(defaultIds)
-
- toolbar.setupWithNavController(navController, appBarConfiguration)
- navView?.setupWithNavController(navController)
- navRail?.setupWithNavController(navController)
- }
-
private fun navigateFromIntent() {
- val fragmentName = intent.getStringExtra(EXTRA_DESTINATION) ?: return
+ // TODO: handle intent
+ /*val fragmentName = intent.getStringExtra(EXTRA_DESTINATION) ?: return
when (fragmentName) {
Actions.BrowseParams.name -> R.id.navigationBrowse
Actions.ListParams.name -> R.id.navigationList
@@ -81,11 +51,11 @@ class MainActivity : BaseAppCompatActivity() {
else -> null
}?.let { id ->
navHost.navController.navigate(id)
- }
+ }*/
}
private fun checkNotificationPermission() {
- val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
if (prefs.askedForNotificationPermission || !prefs.runOnStartUp) return
if (manager.areNotificationsEnabled()) return
@@ -94,9 +64,6 @@ class MainActivity : BaseAppCompatActivity() {
prefs.askedForNotificationPermission = true
}
- private val navHost: NavHostFragment
- get() = supportFragmentManager.findFragmentById(R.id.navHostFragment) as NavHostFragment
-
companion object {
internal const val EXTRA_DESTINATION = "destination"
}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainNavBar.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainNavBar.kt
new file mode 100644
index 0000000..8e42edb
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainNavBar.kt
@@ -0,0 +1,107 @@
+package com.androidvip.sysctlgui.ui.main
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material.icons.outlined.Build
+import androidx.compose.material.icons.outlined.FavoriteBorder
+import androidx.compose.material.icons.outlined.Home
+import androidx.compose.material.icons.outlined.Settings
+import androidx.compose.material.icons.rounded.Build
+import androidx.compose.material.icons.rounded.Settings
+import androidx.compose.material3.Icon
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.PreviewDynamicColors
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.navigation.NavDestination.Companion.hasRoute
+import androidx.navigation.NavDestination.Companion.hierarchy
+import androidx.navigation.NavGraph.Companion.findStartDestination
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.currentBackStackEntryAsState
+import androidx.navigation.compose.rememberNavController
+import com.androidvip.sysctlgui.R
+import com.androidvip.sysctlgui.core.navigation.TopLevelRoute
+import com.androidvip.sysctlgui.core.navigation.UiRoute
+import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
+
+@Composable
+internal fun MainNavBar(navController: NavHostController = rememberNavController()) {
+ val browseParamsTitle = stringResource(R.string.browse)
+ val presetsTitle = stringResource(R.string.presets)
+ val favoritesTitle = stringResource(R.string.favorites)
+ val settingsTitle = stringResource(R.string.settings)
+ val topLevelRoutes = remember {
+ listOf(
+ TopLevelRoute(
+ name = browseParamsTitle,
+ route = UiRoute.BrowseParams,
+ selectedIcon = Icons.Filled.Home,
+ unselectedIcon = Icons.Outlined.Home
+ ),
+ TopLevelRoute(
+ name = presetsTitle,
+ route = UiRoute.Presets,
+ selectedIcon = Icons.Rounded.Build,
+ unselectedIcon = Icons.Outlined.Build
+ ),
+ TopLevelRoute(
+ name = favoritesTitle,
+ route = UiRoute.Favorites,
+ selectedIcon = Icons.Filled.Favorite,
+ unselectedIcon = Icons.Outlined.FavoriteBorder
+ ),
+ TopLevelRoute(
+ name = settingsTitle,
+ route = UiRoute.Settings,
+ selectedIcon = Icons.Rounded.Settings,
+ unselectedIcon = Icons.Outlined.Settings
+ )
+ )
+ }
+
+ NavigationBar {
+ val navBackStackEntry by navController.currentBackStackEntryAsState()
+ val currentDestination = navBackStackEntry?.destination
+
+ topLevelRoutes.forEach { route ->
+ val selected = currentDestination
+ ?.hierarchy
+ ?.any { it.hasRoute(route.route::class) } == true
+
+ NavigationBarItem(
+ icon = {
+ Icon(
+ imageVector = if (selected) route.selectedIcon else route.unselectedIcon,
+ contentDescription = route.name,
+ )
+ },
+ label = { Text(route.name) },
+ selected = selected,
+ onClick = {
+ navController.navigate(route.route) {
+ popUpTo(navController.graph.findStartDestination().id) {
+ saveState = true
+ }
+ launchSingleTop = true
+ restoreState = true
+ }
+ }
+ )
+ }
+ }
+}
+
+@Composable
+@PreviewLightDark
+@PreviewDynamicColors
+private fun MainNavbarPreview() {
+ SysctlGuiTheme(dynamicColor = true) {
+ MainNavBar()
+ }
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainScreen.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainScreen.kt
new file mode 100644
index 0000000..079de14
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainScreen.kt
@@ -0,0 +1,105 @@
+package com.androidvip.sysctlgui.ui.main
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.tooling.preview.PreviewDynamicColors
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.rememberNavController
+import com.androidvip.sysctlgui.core.navigation.UiRoute
+import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
+import org.koin.androidx.compose.koinViewModel
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MainScreen(viewModel: MainViewModel = koinViewModel()) {
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
+ val navController = rememberNavController()
+ val snackbarHostState = remember { SnackbarHostState() }
+
+ LaunchedEffect(Unit) {
+ viewModel.effect.collect { effect ->
+ if (effect is MainViewEffect.ShowSnackbar) {
+ val result = snackbarHostState.showSnackbar(
+ message = effect.message,
+ actionLabel = effect.actionLabel
+ )
+ viewModel.onEvent(MainViewEvent.OnSnackbarResult(result))
+ }
+ }
+ }
+
+ MainScreenContent(state, navController, snackbarHostState)
+}
+
+@Composable
+private fun MainScreenContent(
+ state: MainViewState,
+ navController: NavHostController,
+ snackbarHostState: SnackbarHostState
+) {
+ Scaffold(
+ topBar = {
+ AnimatedVisibility(
+ visible = state.showTopBar,
+ enter = expandVertically() + slideInVertically() + fadeIn(),
+ exit = shrinkVertically() + slideOutVertically() + fadeOut(),
+ label = "TopBar"
+ ) {
+ MainTopBar(
+ title = state.topBarTitle,
+ showSearch = state.showSearchAction,
+ showBack = state.showBackButton,
+ onSearchPressed = { navController.navigate(UiRoute.Search) },
+ onBackPressed = { navController.popBackStack() }
+ )
+ }
+ },
+ bottomBar = {
+ AnimatedVisibility(
+ visible = state.showNavBar,
+ enter = expandVertically() + slideInVertically { it / 2 } + fadeIn(),
+ exit = shrinkVertically() + slideOutVertically { it / 2 } + fadeOut(),
+ label = "BottomBar"
+ ) {
+ MainNavBar(navController = navController)
+ }
+ },
+ snackbarHost = {
+ SnackbarHost(snackbarHostState)
+ },
+ content = { innerPadding ->
+ AppNavHost(
+ innerPadding = innerPadding,
+ navController = navController
+ )
+ }
+ )
+}
+
+@Composable
+@PreviewLightDark
+@PreviewDynamicColors
+private fun MainScreenPreview() {
+ SysctlGuiTheme(dynamicColor = true) {
+ MainScreenContent(
+ state = MainViewState(),
+ navController = rememberNavController(),
+ snackbarHostState = remember { SnackbarHostState() }
+ )
+ }
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainTopBar.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainTopBar.kt
new file mode 100644
index 0000000..4b214ce
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainTopBar.kt
@@ -0,0 +1,75 @@
+package com.androidvip.sysctlgui.ui.main
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.expandHorizontally
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkHorizontally
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.ArrowBack
+import androidx.compose.material.icons.rounded.Search
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import com.androidvip.sysctlgui.R
+import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MainTopBar(
+ title: String = stringResource(R.string.app_name),
+ showBack: Boolean = false,
+ showSearch: Boolean = false,
+ onSearchPressed: () -> Unit,
+ onBackPressed: () -> Unit
+) {
+ TopAppBar(
+ navigationIcon = {
+ AnimatedVisibility(visible = showBack) {
+ IconButton(onClick = onBackPressed) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
+ contentDescription = "Back"
+ )
+ }
+ }
+ },
+ title = {
+ Text(title, maxLines = 1, overflow = TextOverflow.Ellipsis)
+ },
+ actions = {
+ AnimatedVisibility(
+ visible = showSearch,
+ enter = expandHorizontally() + fadeIn(),
+ exit = shrinkHorizontally() + fadeOut()
+ ) {
+ IconButton(onClick = onSearchPressed) {
+ Icon(
+ imageVector = Icons.Rounded.Search,
+ contentDescription = stringResource(R.string.search)
+ )
+ }
+ }
+ }
+ )
+}
+
+@Composable
+@PreviewLightDark
+private fun MainTopBarPreview() {
+ SysctlGuiTheme {
+ MainTopBar(
+ title = "SysctlGUI",
+ showBack = false,
+ showSearch = true,
+ onSearchPressed = {},
+ onBackPressed = {}
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainViewEffect.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainViewEffect.kt
deleted file mode 100644
index e7b9ab6..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainViewEffect.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.androidvip.sysctlgui.ui.main
-
-sealed interface MainViewEffect {
- object NavigateToKernelList : MainViewEffect
- object NavigateToKernelBrowser : MainViewEffect
- object ExportParams : MainViewEffect
- object NavigateToFavorites : MainViewEffect
- object NavigateToSettings : MainViewEffect
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainViewModel.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainViewModel.kt
index 6f5c92a..cccd608 100644
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainViewModel.kt
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainViewModel.kt
@@ -1,41 +1,26 @@
package com.androidvip.sysctlgui.ui.main
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
-import com.androidvip.sysctlgui.R
-import com.androidvip.sysctlgui.data.models.SettingsItem
-
-class MainViewModel : ViewModel() {
- private val _viewEffect = MutableLiveData()
- val viewEffect: LiveData = _viewEffect
-
- fun getHomeItems(): List = listOf(
- SettingsItem(R.string.show_variables, R.string.show_variables_sum, R.drawable.ic_edit_outline),
- SettingsItem(
- R.string.browse_variables,
- R.string.browse_variables_sum,
- R.drawable.ic_folder_outline
- ),
- SettingsItem(
- R.string.export_options,
- R.string.export_options_sum,
- R.drawable.ic_file_import_outline
- ),
- SettingsItem(
- R.string.show_favorites,
- R.string.show_favorites_sum,
- R.drawable.ic_favorite_unselected
- )
- )
-
- fun doWhenListPressed() = _viewEffect.postValue(MainViewEffect.NavigateToKernelList)
-
- fun doWhenBrowsePressed() = _viewEffect.postValue(MainViewEffect.NavigateToKernelBrowser)
-
- fun doWhenImportPressed() = _viewEffect.postValue(MainViewEffect.ExportParams)
-
- fun doWhenFavoritesPressed() = _viewEffect.postValue(MainViewEffect.NavigateToFavorites)
-
- fun doWhenSettingsPressed() = _viewEffect.postValue(MainViewEffect.NavigateToSettings)
+import androidx.compose.material3.SnackbarResult
+import com.androidvip.sysctlgui.utils.BaseViewModel
+
+class MainViewModel : BaseViewModel() {
+
+ override fun createInitialState() = MainViewState()
+
+ override fun onEvent(event: MainViewEvent) {
+ when (event) {
+ is MainViewEvent.OnSateChangeRequested -> {
+ setState { event.newState }
+ }
+ is MainViewEvent.ShowSnackbarRequested -> {
+ setEffect { MainViewEffect.ShowSnackbar(event.message, event.actionLabel) }
+ }
+ is MainViewEvent.OnSnackbarResult -> {
+ val snackbarResult = event.result
+ if (snackbarResult == SnackbarResult.ActionPerformed) {
+ setEffect { MainViewEffect.ActUponSckbarActionPerformed }
+ }
+ }
+ }
+ }
}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainViewState.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainViewState.kt
new file mode 100644
index 0000000..da18a59
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainViewState.kt
@@ -0,0 +1,26 @@
+package com.androidvip.sysctlgui.ui.main
+
+import androidx.compose.material3.SnackbarResult
+
+data class MainViewState(
+ val topBarTitle: String = "SysctlGUI",
+ val showTopBar: Boolean = true,
+ val showNavBar: Boolean = true,
+ val showBackButton: Boolean = false,
+ val showSearchAction: Boolean = true
+)
+
+sealed interface MainViewEffect {
+ data class ShowSnackbar(val message: String, val actionLabel: String? = null) : MainViewEffect
+ data object ActUponSckbarActionPerformed : MainViewEffect
+}
+
+sealed interface MainViewEvent {
+ data class OnSateChangeRequested(val newState: MainViewState) : MainViewEvent
+ data class ShowSnackbarRequested(
+ val message: String,
+ val actionLabel: String? = null
+ ) : MainViewEvent
+
+ data class OnSnackbarResult(val result: SnackbarResult) : MainViewEvent
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/DocumentationBottomSheet.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/DocumentationBottomSheet.kt
new file mode 100644
index 0000000..8ba0aab
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/DocumentationBottomSheet.kt
@@ -0,0 +1,157 @@
+package com.androidvip.sysctlgui.ui.params
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.SheetState
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextLinkStyles
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.fromHtml
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.compose.ui.unit.dp
+import androidx.core.text.HtmlCompat
+import com.androidvip.sysctlgui.R
+import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
+import com.androidvip.sysctlgui.domain.models.ParamDocumentation
+import com.androidvip.sysctlgui.utils.browse
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import org.intellij.lang.annotations.Language
+import kotlin.text.append
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+internal fun DocumentationBottomSheet(
+ documentation: ParamDocumentation,
+ sheetState: SheetState
+) {
+ val coroutineScope = rememberCoroutineScope()
+ ModalBottomSheet(
+ onDismissRequest = { coroutineScope.launch { sheetState.hide() } },
+ sheetState = sheetState,
+ ) {
+ DocumentationBottomSheetContent(
+ documentation = documentation,
+ sheetState = sheetState
+ )
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun DocumentationBottomSheetContent(
+ documentation: ParamDocumentation,
+ sheetState: SheetState,
+ coroutineScope: CoroutineScope = rememberCoroutineScope(),
+) {
+ Column(modifier = Modifier.padding(24.dp)) {
+ val context = LocalContext.current
+ Text(
+ text = documentation.title,
+ style = MaterialTheme.typography.titleLarge,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+
+ val documentationText = if (!documentation.documentationHtml.isNullOrEmpty()) {
+ AnnotatedString.fromHtml(
+ htmlString = documentation.documentationHtml.orEmpty(),
+ linkStyles = TextLinkStyles(
+ style = MaterialTheme.typography.bodyMedium.toSpanStyle().copy(
+ color = MaterialTheme.colorScheme.primary,
+ textDecoration = TextDecoration.Underline,
+ fontWeight = FontWeight.Medium
+ ),
+ pressedStyle = MaterialTheme.typography.bodyMedium.toSpanStyle().copy(
+ color = MaterialTheme.colorScheme.tertiary,
+ textDecoration = TextDecoration.Underline,
+ fontWeight = FontWeight.Medium
+ )
+ )
+ )
+ } else {
+ AnnotatedString(documentation.documentationText)
+ }
+ Text(
+ text = documentationText,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurface,
+ modifier = Modifier.padding(top = 16.dp)
+ )
+
+ if (documentation.url != null) {
+ TextButton(
+ onClick = {
+ context.browse(documentation.url.orEmpty())
+ coroutineScope.launch { sheetState.hide() }
+ },
+ modifier = Modifier
+ .align(alignment = Alignment.End)
+ .padding(top = 16.dp)
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_open_in_browser),
+ contentDescription = null,
+ modifier = Modifier.padding(end = 8.dp)
+ )
+ Text(text = "Read more")
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+@PreviewLightDark
+private fun DocumentationBottomSheetPreview() {
+
+ @Language("HTML")
+ val htmlDocs = """
+
+ When BPF JIT compiler is enabled, then compiled images are unknown
+ addresses to the kernel, meaning they neither show up in traces nor
+ in /proc/kallsyms. This enables export of these addresses, which can
+ be used for debugging/tracing. If bpf_jit_harden is enabled, this
+ feature is disabled.
+
+ Values :
+
+ - 0 - disable JIT kallsyms export (default value)
+ - 1 - enable JIT kallsyms export for privileged users only
+
+ """.trimIndent()
+
+ val documentation = ParamDocumentation(
+ title = "/proc/sys/fs",
+ url = "https://docs.kernel.org/admin-guide/sysctl/fs.html",
+ documentationText = """
+ The files in this directory can be used to tune and monitor miscellaneous and general
+ things in the operation of the Linux kernel. It is advisable to read both
+ documentation and source before actually making adjustments.
+ """.trimIndent(),
+ documentationHtml = htmlDocs
+ )
+
+ SysctlGuiTheme(dynamicColor = true) {
+ val state = rememberModalBottomSheetState()
+
+ Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
+ DocumentationBottomSheetContent(documentation = documentation, state)
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/EmptyParamsWarning.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/EmptyParamsWarning.kt
deleted file mode 100644
index b63bf04..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/EmptyParamsWarning.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-package com.androidvip.sysctlgui.ui.params
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.Warning
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import com.androidvip.sysctlgui.R
-
-@Composable
-fun EmptyParamsWarning() {
- Box(modifier = Modifier.fillMaxSize()) {
- Card(
- modifier = Modifier
- .fillMaxWidth()
- .padding(24.dp),
- colors = CardDefaults.cardColors(
- containerColor = MaterialTheme.colorScheme.errorContainer
- )
- ) {
- Row(
- modifier = Modifier.padding(24.dp),
- horizontalArrangement = Arrangement.spacedBy(
- 16.dp,
- Alignment.CenterHorizontally
- )
- ) {
- Icon(
- imageVector = Icons.Outlined.Warning,
- contentDescription = stringResource(android.R.string.dialog_alert_title),
- tint = MaterialTheme.colorScheme.onErrorContainer
- )
- Column {
- Text(
- text = stringResource(id = R.string.error),
- style = MaterialTheme.typography.bodyLarge.copy(
- fontWeight = FontWeight.Medium
- ),
- color = MaterialTheme.colorScheme.onErrorContainer
- )
- Text(
- text = stringResource(id = R.string.no_parameters_found),
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onErrorContainer
- )
- }
- }
- }
- }
-}
-
-@Composable
-@Preview
-private fun EmptyParamsWarningPreview() {
- Box(modifier = Modifier.background(Color.White)) {
- EmptyParamsWarning()
- }
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/OnParamItemClickedListener.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/OnParamItemClickedListener.kt
deleted file mode 100644
index caa8b76..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/OnParamItemClickedListener.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.androidvip.sysctlgui.ui.params
-
-import android.view.View
-import com.androidvip.sysctlgui.data.models.KernelParam
-
-fun interface OnParamItemClickedListener {
- fun onParamItemClicked(param: KernelParam, itemLayout: View)
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/OnPopUpMenuItemSelectedListener.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/OnPopUpMenuItemSelectedListener.kt
deleted file mode 100644
index 34aa6e5..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/OnPopUpMenuItemSelectedListener.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.androidvip.sysctlgui.ui.params
-
-import androidx.annotation.IdRes
-import androidx.constraintlayout.widget.ConstraintLayout
-import com.androidvip.sysctlgui.data.models.KernelParam
-
-interface OnPopUpMenuItemSelectedListener {
- fun onPopUpMenuItemSelected(
- kernelParam: KernelParam,
- @IdRes itemId: Int,
- removableLayout: ConstraintLayout
- )
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/BrowseParamsViewModel.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/BrowseParamsViewModel.kt
deleted file mode 100644
index d92bb99..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/BrowseParamsViewModel.kt
+++ /dev/null
@@ -1,161 +0,0 @@
-package com.androidvip.sysctlgui.ui.params.browse
-
-import androidx.lifecycle.viewModelScope
-import com.androidvip.sysctlgui.R
-import com.androidvip.sysctlgui.data.mapper.DomainParamMapper
-import com.androidvip.sysctlgui.domain.repository.AppPrefs
-import com.androidvip.sysctlgui.domain.usecase.GetParamsFromFilesUseCase
-import com.androidvip.sysctlgui.utils.BaseViewModel
-import com.androidvip.sysctlgui.utils.Consts
-import com.topjohnwu.superuser.io.SuFile
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import java.io.File
-
-class BrowseParamsViewModel(
- private val getParamsFromFilesUseCase: GetParamsFromFilesUseCase,
- appPrefs: AppPrefs,
- private val dispatcher: CoroutineDispatcher = Dispatchers.IO
-) : BaseViewModel() {
- private var listFoldersFirst = true
- private var searchExpression = ""
-
- init {
- listFoldersFirst = appPrefs.listFoldersFirst
- }
-
- override fun createInitialState(): ParamBrowserViewState = ParamBrowserViewState()
-
- override fun onEvent(event: ParamBrowserViewEvent) {
- when (event) {
- ParamBrowserViewEvent.RefreshRequested -> setPath(currentState.currentPath)
- is ParamBrowserViewEvent.DirectoryChanged -> onDirectoryChanged(event.dir)
- is ParamBrowserViewEvent.SearchExpressionChanged -> onSearchExpressionChanged(event.data)
- is ParamBrowserViewEvent.ParamClicked -> setEffect {
- ParamBrowserViewEffect.NavigateToParamDetails(DomainParamMapper.map(event.param))
- }
-
- ParamBrowserViewEvent.DocumentationMenuClicked -> setEffect {
- ParamBrowserViewEffect.OpenDocumentationUrl(currentState.docUrl)
- }
-
- ParamBrowserViewEvent.FavoritesMenuClicked -> setEffect {
- ParamBrowserViewEffect.NavigateToFavorite
- }
- }
- }
-
- private fun setPath(path: String) {
- viewModelScope.launch {
- loadBrowsableParamFiles(path)
- }
- }
-
- private fun onDirectoryChanged(newDir: File) {
- val newPath = newDir.absolutePath
- if (newPath.isEmpty() || !newPath.startsWith(Consts.PROC_SYS)) {
- setEffect { ParamBrowserViewEffect.ShowToast(R.string.invalid_path) }
- return
- }
-
- setPath(newPath)
-
- when {
- newPath.startsWith("/proc/sys/abi") -> setState {
- copy(
- docUrl = "https://www.kernel.org/doc/Documentation/sysctl/abi.txt",
- showDocumentationMenu = true
- )
- }
-
- newPath.startsWith("/proc/sys/fs") -> setState {
- copy(
- docUrl = "https://www.kernel.org/doc/Documentation/sysctl/fs.txt",
- showDocumentationMenu = true
- )
- }
-
- newPath.startsWith("/proc/sys/kernel") -> setState {
- copy(
- docUrl = "https://www.kernel.org/doc/Documentation/sysctl/kernel.txt",
- showDocumentationMenu = true
- )
- }
-
- newPath.startsWith("/proc/sys/net") -> setState {
- copy(
- docUrl = "https://www.kernel.org/doc/Documentation/sysctl/net.txt",
- showDocumentationMenu = true
- )
- }
-
- newPath.startsWith("/proc/sys/vm") -> setState {
- copy(
- docUrl = "https://www.kernel.org/doc/Documentation/sysctl/vm.txt",
- showDocumentationMenu = true
- )
- }
-
- else -> setState { copy(showDocumentationMenu = false) }
- }
- }
-
- private suspend fun getCurrentPathFiles(path: String) = withContext(dispatcher) {
- runCatching {
- val baseFile = File(path)
- val file = if (baseFile.canRead()) baseFile else SuFile.open(path)
- file.listFiles()?.toList()
- }.getOrDefault(emptyList())
- }
-
- private suspend fun loadBrowsableParamFiles(path: String) {
- setState { copy(isLoading = true) }
- val files = getCurrentPathFiles(path).maybeDirectorySorted()
- val params = getParamsFromFilesUseCase(files).map {
- DomainParamMapper.map(it)
- }
-
- setState {
- copy(
- currentPath = path,
- isLoading = false,
- data = params.filter { param -> byName(param.name, searchExpression) },
- totalData = params
- )
- }
- }
-
- private suspend fun List?.maybeDirectorySorted() = withContext(dispatcher) {
- return@withContext this@maybeDirectorySorted?.run {
- if (listFoldersFirst) {
- sortedByDescending { it.isDirectory }
- } else {
- this
- }
- }?.toList().orEmpty()
- }
-
- private fun onSearchExpressionChanged(expression: String) {
- searchExpression = expression
-
- setState {
- copy(data = this.totalData.filter { kernelParam ->
- byName(
- kernelParam.name,
- searchExpression
- )
- })
- }
- }
-
- private fun byName(current: String, expected: String): Boolean {
- if (expected.isEmpty()) {
- return true
- }
- return current.lowercase()
- .replace(".", "")
- .contains(expected.lowercase())
- }
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/KernelParamBrowseFragment.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/KernelParamBrowseFragment.kt
deleted file mode 100644
index 02f6406..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/KernelParamBrowseFragment.kt
+++ /dev/null
@@ -1,277 +0,0 @@
-package com.androidvip.sysctlgui.ui.params.browse
-
-import android.annotation.SuppressLint
-import android.app.Dialog
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.Menu
-import android.view.MenuInflater
-import android.view.MenuItem
-import android.view.View
-import android.view.ViewGroup
-import android.view.Window
-import android.webkit.WebChromeClient
-import android.webkit.WebSettings
-import android.webkit.WebView
-import android.webkit.WebViewClient
-import android.widget.ProgressBar
-import androidx.activity.OnBackPressedCallback
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.itemsIndexed
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.pullrefresh.PullRefreshIndicator
-import androidx.compose.material.pullrefresh.pullRefresh
-import androidx.compose.material.pullrefresh.rememberPullRefreshState
-import androidx.compose.material3.HorizontalDivider
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.unit.dp
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.navigation.fragment.findNavController
-import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
-import com.androidvip.sysctlgui.R
-import com.androidvip.sysctlgui.data.models.KernelParam
-import com.androidvip.sysctlgui.design.DesignIds
-import com.androidvip.sysctlgui.design.DesignLayouts
-import com.androidvip.sysctlgui.getColorRoles
-import com.androidvip.sysctlgui.goAway
-import com.androidvip.sysctlgui.show
-import com.androidvip.sysctlgui.toast
-import com.androidvip.sysctlgui.ui.base.BaseSearchFragment
-import com.androidvip.sysctlgui.ui.params.EmptyParamsWarning
-import com.androidvip.sysctlgui.ui.params.OnParamItemClickedListener
-import com.androidvip.sysctlgui.ui.params.edit.EditKernelParamActivity
-import com.androidvip.sysctlgui.utils.ComposeTheme
-import com.androidvip.sysctlgui.utils.Consts
-import kotlinx.coroutines.launch
-import org.koin.android.ext.android.inject
-import java.io.File
-
-class KernelParamBrowseFragment : BaseSearchFragment(), OnParamItemClickedListener {
- private var actionBarMenu: Menu? = null
- private val viewModel: BrowseParamsViewModel by inject()
- private val currentPath: String get() = viewModel.currentState.currentPath
- private val canGoBack: Boolean get() = currentPath != Consts.PROC_SYS
-
- private val onBackPressedCallback = object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- if (canGoBack) {
- onDirectoryChanged(File(currentPath).parentFile ?: File(Consts.PROC_SYS))
- }
- }
- }
-
- @OptIn(ExperimentalMaterialApi::class)
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- return ComposeView(requireContext()).apply {
- setContent {
- ComposeTheme {
- val state by viewModel.uiState.collectAsStateWithLifecycle()
- val refreshing = state.isLoading
- val refreshState = rememberPullRefreshState(
- refreshing = refreshing,
- onRefresh = { refresh() }
- )
-
- actionBarMenu
- ?.findItem(R.id.action_documentation)
- ?.isVisible = state.showDocumentationMenu
-
- Box(Modifier.pullRefresh(refreshState)) {
- if (state.showEmptyState) {
- EmptyParamsWarning()
- } else {
- KernelParamsExplorer(state.data)
- }
-
- PullRefreshIndicator(
- modifier = Modifier.align(Alignment.TopCenter),
- refreshing = refreshing,
- state = refreshState,
- backgroundColor = MaterialTheme.colorScheme.tertiaryContainer,
- contentColor = MaterialTheme.colorScheme.onTertiaryContainer
- )
- }
-
- SideEffect { onBackPressedCallback.isEnabled = canGoBack }
- }
- }
- }
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- lifecycleScope.launch {
- viewModel.onEvent(ParamBrowserViewEvent.DirectoryChanged(File(Consts.PROC_SYS)))
- viewModel.effect.collect(::handleViewEffect)
- }
-
- requireActivity().onBackPressedDispatcher.addCallback(onBackPressedCallback)
- }
-
- override fun onStart() {
- super.onStart()
- refresh()
- }
-
- override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
- inflater.inflate(R.menu.menu_browse_params, menu)
- actionBarMenu = menu
-
- setUpSearchView(menu)
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- when (item.itemId) {
- R.id.action_documentation -> {
- viewModel.onEvent(ParamBrowserViewEvent.DocumentationMenuClicked)
- }
- R.id.action_favorites -> {
- viewModel.onEvent(ParamBrowserViewEvent.FavoritesMenuClicked)
- }
- else -> return false
- }
-
- return true
- }
-
- override fun onQueryTextChanged() {
- viewModel.onEvent(ParamBrowserViewEvent.SearchExpressionChanged(searchExpression))
- }
-
- override fun onParamItemClicked(param: KernelParam, itemLayout: View) {
- viewModel.onEvent(ParamBrowserViewEvent.ParamClicked(param))
- }
-
- private fun onDirectoryChanged(newDir: File) {
- viewModel.onEvent(ParamBrowserViewEvent.DirectoryChanged(newDir))
- resetSearchExpression()
- }
-
- private fun handleViewEffect(viewEffect: ParamBrowserViewEffect) {
- when (viewEffect) {
- is ParamBrowserViewEffect.NavigateToParamDetails -> {
- navigateToParamDetails(viewEffect.param)
- }
- is ParamBrowserViewEffect.NavigateToFavorite -> {
- findNavController().navigate(R.id.navigateFavoritesParams)
- }
- is ParamBrowserViewEffect.OpenDocumentationUrl -> openDocumentationUrl(viewEffect.url)
- is ParamBrowserViewEffect.ShowToast -> toast(viewEffect.stringRes)
- }
- }
-
- private fun navigateToParamDetails(param: KernelParam) {
- startActivity(EditKernelParamActivity.getIntent(requireContext(), param))
- }
-
- private fun refresh() {
- viewModel.onEvent(ParamBrowserViewEvent.RefreshRequested)
- }
-
- @SuppressLint("SetJavaScriptEnabled")
- private fun openDocumentationUrl(url: String) {
- if (!isAdded) return
-
- val dialog = Dialog(requireContext()).apply {
- requestWindowFeature(Window.FEATURE_NO_TITLE)
- setContentView(DesignLayouts.dialog_web)
- setCancelable(true)
- }
-
- val progressBar: ProgressBar = dialog.findViewById(DesignIds.webDialogProgress)
- val swipeLayout: SwipeRefreshLayout = dialog.findViewById(DesignIds.webDialogSwipeLayout)
-
- val webView = dialog.findViewById(DesignIds.webDialogWebView).apply {
- val colorRoles = getColorRoles()
- settings.apply {
- javaScriptEnabled = true
- cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK
- }
-
- loadUrl(url)
-
- webViewClient = object : WebViewClient() {
- override fun onPageFinished(view: WebView, url: String) {
- super.onPageFinished(view, url)
- swipeLayout.isRefreshing = false
-
- val containerColorInt = colorRoles.accentContainer
- val colorInt = colorRoles.onAccentContainer
-
- val containerColorHex = "#%06X".format(0xFFFFFF and containerColorInt)
- val colorHex = "#%06X".format(0xFFFFFF and colorInt)
- // Change webView background and text color to match the app theme
- view.loadUrl(
- """
- |javascript:(
- |function() {
- |document.querySelector('body').style.color='$colorHex';
- |document.querySelector('body').style.background='$containerColorHex';
- |}
- |)()
- """.trimMargin()
- )
- }
- }
-
- webChromeClient = object : WebChromeClient() {
- override fun onProgressChanged(view: WebView, progress: Int) {
- progressBar.progress = progress
- if (progress == 100) {
- progressBar.goAway()
- swipeLayout.isRefreshing = false
- } else {
- progressBar.show()
- }
- }
- }
- }
-
- swipeLayout.apply {
- val roles = getColorRoles()
- setColorSchemeColors(roles.accent)
- setProgressBackgroundColorSchemeColor(roles.accentContainer)
-
- setOnRefreshListener { webView.reload() }
- }
-
- dialog.show()
- }
-
- @Composable
- private fun KernelParamsExplorer(params: List) {
- LazyColumn {
- itemsIndexed(params) { index, param ->
- ParamBrowseItem(
- onParamClick = {
- viewModel.onEvent(ParamBrowserViewEvent.ParamClicked(param))
- },
- onDirectoryChanged = {
- viewModel.onEvent(ParamBrowserViewEvent.DirectoryChanged(it))
- },
- param = param,
- paramFile = File(param.path)
- )
- if (index < params.lastIndex) {
- HorizontalDivider(
- thickness = 1.dp,
- color = MaterialTheme.colorScheme.outlineVariant
- )
- }
- }
- }
- }
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowseItem.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowseItem.kt
deleted file mode 100644
index 13e5349..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowseItem.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-package com.androidvip.sysctlgui.ui.params.browse
-
-import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import com.androidvip.sysctlgui.R
-import com.androidvip.sysctlgui.data.models.KernelParam
-import com.androidvip.sysctlgui.design.theme.md_theme_light_background
-import java.io.File
-
-@Composable
-fun ParamBrowseItem(
- onParamClick: (KernelParam) -> Unit,
- onDirectoryChanged: (File) -> Unit,
- param: KernelParam,
- paramFile: File
-) {
- val isDir = paramFile.isDirectory
- val outlineColor = MaterialTheme.colorScheme.outlineVariant
- val surfaceColor = MaterialTheme.colorScheme.surfaceVariant
- val tintColor = if (isDir) {
- MaterialTheme.colorScheme.onSurfaceVariant
- } else {
- MaterialTheme.colorScheme.onSurface
- }
-
- Box(
- modifier = Modifier
- .clickable {
- if (isDir) onDirectoryChanged(paramFile) else onParamClick(param)
- }
- ) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(16.dp)
- ) {
- Box(contentAlignment = Alignment.Center) {
- Canvas(modifier = Modifier.size(42.dp), onDraw = {
- drawCircle(color = if (isDir) surfaceColor else outlineColor)
- })
-
- val iconResource = if (isDir) {
- R.drawable.ic_folder_outline
- } else {
- R.drawable.ic_file_outline
- }
- Icon(
- painter = painterResource(id = iconResource),
- tint = tintColor,
- contentDescription = ""
- )
- }
- Text(
- text = param.shortName,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- color = MaterialTheme.colorScheme.onBackground,
- style = if (isDir) {
- MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Medium)
- } else {
- MaterialTheme.typography.bodyMedium
- }
- )
- }
- }
-}
-
-@Preview
-@Composable
-fun ParamItemPreview() {
- val param = KernelParam(name = "test", value = "success")
- Box(modifier = Modifier.background(md_theme_light_background)) {
- ParamBrowseItem(
- onParamClick = {},
- onDirectoryChanged = {},
- param = param,
- paramFile = File("/")
- )
- }
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowseScreen.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowseScreen.kt
new file mode 100644
index 0000000..5ff042e
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowseScreen.kt
@@ -0,0 +1,313 @@
+package com.androidvip.sysctlgui.ui.params.browse
+
+import android.widget.Toast
+import androidx.activity.compose.BackHandler
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.foundation.background
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.androidvip.sysctlgui.R
+import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import com.androidvip.sysctlgui.domain.models.ParamDocumentation
+import com.androidvip.sysctlgui.models.UiKernelParam
+import com.androidvip.sysctlgui.ui.main.MainViewEvent
+import com.androidvip.sysctlgui.ui.main.MainViewModel
+import com.androidvip.sysctlgui.ui.main.MainViewState
+import com.androidvip.sysctlgui.ui.params.DocumentationBottomSheet
+import com.androidvip.sysctlgui.utils.browse
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import org.koin.compose.viewmodel.koinViewModel
+import java.io.File
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ParamBrowseScreen(
+ mainViewModel: MainViewModel = koinViewModel(),
+ viewModel: ParamBrowseViewModel = koinViewModel(),
+ onParamSelected: (KernelParam) -> Unit
+) {
+ var documentation by remember { mutableStateOf(null) }
+ val documentationSheetState = rememberModalBottomSheetState()
+ val context = LocalContext.current
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
+
+ LaunchedEffect(state) {
+ mainViewModel.onEvent(
+ MainViewEvent.OnSateChangeRequested(
+ MainViewState(
+ showTopBar = true,
+ showNavBar = true,
+ showBackButton = state.backEnabled,
+ showSearchAction = true
+ )
+ )
+ )
+ }
+
+ LaunchedEffect(viewModel.effect) {
+ viewModel.effect.collect { effect ->
+ when (effect) {
+ is ParamBrowseViewEffect.EditKernelParam -> onParamSelected(effect.param)
+ is ParamBrowseViewEffect.OpenBrowser -> context.browse(effect.url)
+ is ParamBrowseViewEffect.ShowError -> Toast.makeText(
+ context,
+ effect.errorMessage,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+
+ ParamBrowseScreenContent(
+ params = state.params,
+ currentPath = state.currentPath,
+ documentation = state.documentation,
+ onParamClicked = {
+ viewModel.onEvent(ParamBrowseViewEvent.ParamClicked(it))
+ },
+ onDocumentationClicked = {
+ viewModel.onEvent(ParamBrowseViewEvent.DocumentationClicked(it))
+ },
+ backEnabled = state.backEnabled,
+ onBackPressed = {
+ viewModel.onEvent(ParamBrowseViewEvent.BackRequested)
+ }
+ )
+
+ documentation?.let {
+ DocumentationBottomSheet(
+ documentation = it,
+ sheetState = documentationSheetState
+ )
+ }
+}
+
+@Composable
+private fun ParamBrowseScreenContent(
+ params: List,
+ currentPath: String,
+ documentation: ParamDocumentation?,
+ onParamClicked: (UiKernelParam) -> Unit,
+ onDocumentationClicked: (ParamDocumentation) -> Unit,
+ backEnabled: Boolean = false,
+ onBackPressed: () -> Unit,
+) {
+ val listState = rememberLazyListState()
+ var headerVisible by remember { mutableStateOf(backEnabled) }
+
+ BackHandler(enabled = backEnabled, onBack = onBackPressed)
+
+ LaunchedEffect(listState) {
+ var previousOffset = listState.firstVisibleItemScrollOffset
+ var previousIndex = listState.firstVisibleItemIndex
+
+ snapshotFlow { listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
+ .map { (currentIndex, currentOffset) ->
+ when {
+ currentIndex > previousIndex -> false
+ currentIndex < previousIndex -> true
+ currentOffset > previousOffset -> false
+ currentOffset < previousOffset -> true
+ else -> null // No change or unable to determine (keep current state)
+ }.also {
+ previousIndex = currentIndex
+ previousOffset = currentOffset
+ }
+ }
+ .filter { it != null }
+ .distinctUntilChanged()
+ .collect { scrolledUp ->
+ headerVisible = scrolledUp ?: headerVisible
+ }
+ }
+
+ val isAtTop by remember {
+ derivedStateOf {
+ listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0
+ }
+ }
+
+ val finalHeaderVisible = (headerVisible || isAtTop) && backEnabled
+
+ Box(modifier = Modifier.fillMaxSize()) {
+ val spacerHeight by animateDpAsState(if (finalHeaderVisible) 56.dp else 0.dp)
+ LazyColumn(
+ state = listState,
+ modifier = Modifier.fillMaxSize()
+ ) {
+ item { Spacer(modifier = Modifier.height(spacerHeight)) }
+
+ items(
+ count = params.size,
+ key = { index -> params[index].name }
+ ) { index ->
+ ParamFileRow(
+ modifier = Modifier.animateItem(),
+ param = params[index],
+ onParamClicked = onParamClicked,
+ )
+ }
+
+ if (documentation != null) {
+ item { Spacer(modifier = Modifier.height(56.dp)) }
+ }
+ }
+
+ AnimatedVisibility(
+ visible = finalHeaderVisible,
+ enter = slideInVertically(initialOffsetY = { -it }) + fadeIn(),
+ exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut(),
+ modifier = Modifier.align(Alignment.TopCenter)
+ ) {
+ InfoItem(
+ text = currentPath,
+ icon = painterResource(R.drawable.ic_arrow_upward),
+ onClicked = onBackPressed
+ )
+ }
+
+ AnimatedVisibility(
+ visible = finalHeaderVisible && documentation != null,
+ enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),
+ exit = slideOutVertically(targetOffsetY = { it }) + fadeOut(),
+ modifier = Modifier.align(Alignment.BottomCenter)
+ ) {
+ InfoItem(
+ text = "Read documentation for \"${documentation?.title}\"",
+ textStyle = MaterialTheme.typography.titleSmall.copy(
+ textDecoration = TextDecoration.Underline,
+ color = MaterialTheme.colorScheme.primary
+ ),
+ icon = painterResource(R.drawable.ic_documentation),
+ onClicked = { onDocumentationClicked(documentation!!) }
+ )
+ }
+ }
+}
+
+@Composable
+private fun InfoItem(
+ modifier: Modifier = Modifier,
+ text: String,
+ textStyle: TextStyle = MaterialTheme.typography.titleSmall.copy(
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ ),
+ icon: Painter,
+ onClicked: () -> Unit,
+) {
+ val context = LocalContext.current
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .combinedClickable(
+ role = Role.Button,
+ onClick = onClicked,
+ onLongClick = { Toast.makeText(context, text, Toast.LENGTH_SHORT).show() },
+ )
+ .background(MaterialTheme.colorScheme.surfaceContainer)
+ .padding(horizontal = 16.dp, vertical = 12.dp)
+ .semantics(mergeDescendants = true) { this.contentDescription = text },
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Icon(
+ painter = icon,
+ contentDescription = null,
+ tint = textStyle.color
+ )
+ Text(
+ text = text,
+ style = textStyle,
+ maxLines = 2,
+ overflow = TextOverflow.MiddleEllipsis
+ )
+ }
+}
+
+@Composable
+@PreviewLightDark
+internal fun ParamBrowseScreenContentPreview() {
+ fun mapFilesToParams(files: Array?): List {
+ return files?.map { file ->
+ UiKernelParam(
+ name = file.name,
+ path = file.path,
+ value = "",
+ isFavorite = (0..5).random() % 2 == 0
+ )
+ } ?: emptyList()
+ }
+
+ val root = File("/")
+ var currentPath by remember { mutableStateOf(root.path) }
+ var params by remember(currentPath) {
+ mutableStateOf(mapFilesToParams(File(currentPath).listFiles()))
+ }
+
+ SysctlGuiTheme(dynamicColor = true) {
+ Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
+ ParamBrowseScreenContent(
+ params = params,
+ currentPath = currentPath,
+ documentation = ParamDocumentation(
+ title = currentPath,
+ documentationText = "Documentation for $currentPath",
+ url = null
+ ),
+ onParamClicked = {
+ if (it.isDirectory) {
+ currentPath = it.path
+ params = mapFilesToParams(File(it.path).listFiles())
+ }
+ },
+ onDocumentationClicked = {},
+ backEnabled = currentPath != root.path,
+ onBackPressed = { currentPath = File(currentPath).parent ?: root.path },
+ )
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowseState.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowseState.kt
new file mode 100644
index 0000000..a141586
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowseState.kt
@@ -0,0 +1,23 @@
+package com.androidvip.sysctlgui.ui.params.browse
+
+import com.androidvip.sysctlgui.domain.models.ParamDocumentation
+import com.androidvip.sysctlgui.models.UiKernelParam
+
+data class ParamBrowseState(
+ val params: List = emptyList(),
+ val currentPath: String = "",
+ val backEnabled: Boolean = false,
+ val documentation: ParamDocumentation? = null
+)
+
+sealed interface ParamBrowseViewEffect {
+ data class OpenBrowser(val url: String) : ParamBrowseViewEffect
+ data class EditKernelParam(val param: UiKernelParam) : ParamBrowseViewEffect
+ data class ShowError(val errorMessage: String) : ParamBrowseViewEffect
+}
+
+sealed interface ParamBrowseViewEvent {
+ data class ParamClicked(val param: UiKernelParam) : ParamBrowseViewEvent
+ data class DocumentationClicked(val docs: ParamDocumentation) : ParamBrowseViewEvent
+ object BackRequested : ParamBrowseViewEvent
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowseViewModel.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowseViewModel.kt
new file mode 100644
index 0000000..f3b49d9
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowseViewModel.kt
@@ -0,0 +1,115 @@
+package com.androidvip.sysctlgui.ui.params.browse
+
+import androidx.lifecycle.viewModelScope
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import com.androidvip.sysctlgui.domain.repository.AppPrefs
+import com.androidvip.sysctlgui.domain.usecase.GetParamDocumentationUseCase
+import com.androidvip.sysctlgui.domain.usecase.GetParamsFromFilesUseCase
+import com.androidvip.sysctlgui.helpers.UiKernelParamMapper
+import com.androidvip.sysctlgui.models.UiKernelParam
+import com.androidvip.sysctlgui.utils.BaseViewModel
+import com.androidvip.sysctlgui.utils.Consts
+import com.topjohnwu.superuser.nio.FileSystemManager
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.io.File
+
+class ParamBrowseViewModel(
+ private val getParamsFromFiles: GetParamsFromFilesUseCase,
+ private val getParamDocumentation: GetParamDocumentationUseCase,
+ private val appPrefs: AppPrefs,
+ private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
+) : BaseViewModel() {
+ override fun createInitialState() = ParamBrowseState()
+
+ init {
+ viewModelScope.launch {
+ val startingDirectory = File(Consts.PROC_SYS)
+ val params = getParams(startingDirectory)
+
+ setState {
+ copy(
+ params = params,
+ currentPath = startingDirectory.absolutePath
+ )
+ }
+ }
+ }
+
+ override fun onEvent(event: ParamBrowseViewEvent) {
+ when (event) {
+ is ParamBrowseViewEvent.DocumentationClicked -> setEffect {
+ ParamBrowseViewEffect.OpenBrowser(event.docs.url.orEmpty())
+ }
+
+ ParamBrowseViewEvent.BackRequested -> onBackRequested()
+ is ParamBrowseViewEvent.ParamClicked -> onParamClicked(event.param)
+ }
+ }
+
+ private fun fetchChildParams(parentParam: UiKernelParam) {
+ viewModelScope.launch {
+ runCatching {
+ val newParamsDeferred = async { getParams(File(parentParam.path)) }
+ val directoryDocumentationDeferred = async { getParamDocumentation(parentParam) }
+
+ val newParams = newParamsDeferred.await()
+ val directoryDocumentation = directoryDocumentationDeferred.await()
+
+ setState {
+ copy(
+ params = newParams,
+ currentPath = parentParam.path,
+ backEnabled = parentParam.path != Consts.PROC_SYS,
+ documentation = directoryDocumentation
+ )
+ }
+ }.onFailure {
+ setEffect { ParamBrowseViewEffect.ShowError(it.message ?: "Unknown error") }
+ }
+ }
+ }
+
+ private fun fetchChildParams(parentPath: String) {
+ val param = KernelParam.createFromPath(parentPath, "")
+ fetchChildParams(UiKernelParamMapper.map(param))
+ }
+
+ private fun onParamClicked(param: UiKernelParam) {
+ if (param.isDirectory) {
+ fetchChildParams(param)
+ } else {
+ setEffect {
+ ParamBrowseViewEffect.EditKernelParam(param)
+ }
+ }
+ }
+
+ private fun onBackRequested() {
+ val currentPath = currentState.currentPath
+ if (currentPath == Consts.PROC_SYS) return
+ val parentFile = File(currentPath).parentFile ?: return
+
+ fetchChildParams(parentFile.absolutePath)
+ }
+
+ private suspend fun getParams(file: File): List = withContext(ioDispatcher) {
+ val fileList = if (file.canRead()) {
+ file.listFiles()?.toList() ?: emptyList()
+ } else {
+ val rootAwareFile = FileSystemManager.getLocal().getFile(file.absolutePath)
+ rootAwareFile.listFiles()?.toList() ?: emptyList()
+ }
+
+ val params = getParamsFromFiles(fileList).map(UiKernelParamMapper::map)
+
+ if (appPrefs.listFoldersFirst) {
+ params.sortedByDescending { it.isDirectory }
+ } else {
+ params
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowserViewEffect.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowserViewEffect.kt
deleted file mode 100644
index b258e95..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowserViewEffect.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.androidvip.sysctlgui.ui.params.browse
-
-import androidx.annotation.StringRes
-import com.androidvip.sysctlgui.data.models.KernelParam
-
-sealed class ParamBrowserViewEffect {
- object NavigateToFavorite : ParamBrowserViewEffect()
- class NavigateToParamDetails(val param: KernelParam) : ParamBrowserViewEffect()
- class OpenDocumentationUrl(val url: String) : ParamBrowserViewEffect()
- class ShowToast(@StringRes val stringRes: Int) : ParamBrowserViewEffect()
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowserViewEvent.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowserViewEvent.kt
deleted file mode 100644
index ae5ea92..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowserViewEvent.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.androidvip.sysctlgui.ui.params.browse
-
-import com.androidvip.sysctlgui.domain.models.DomainKernelParam
-import java.io.File
-
-sealed interface ParamBrowserViewEvent {
- object RefreshRequested : ParamBrowserViewEvent
- class SearchExpressionChanged(val data: String) : ParamBrowserViewEvent
- class ParamClicked(val param: DomainKernelParam) : ParamBrowserViewEvent
- class DirectoryChanged(val dir: File) : ParamBrowserViewEvent
- object DocumentationMenuClicked : ParamBrowserViewEvent
- object FavoritesMenuClicked : ParamBrowserViewEvent
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowserViewState.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowserViewState.kt
deleted file mode 100644
index be18a2c..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowserViewState.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.androidvip.sysctlgui.ui.params.browse
-
-import com.androidvip.sysctlgui.data.models.KernelParam
-import com.androidvip.sysctlgui.utils.Consts
-
-data class ParamBrowserViewState(
- var data: List = listOf(),
- var totalData: List = listOf(),
- var isLoading: Boolean = true,
- var showEmptyState: Boolean = false,
- var currentPath: String = Consts.PROC_SYS,
- var showDocumentationMenu: Boolean = false,
- var docUrl: String = "https://www.kernel.org/doc/Documentation"
-)
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamFileRow.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamFileRow.kt
new file mode 100644
index 0000000..43f299c
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamFileRow.kt
@@ -0,0 +1,159 @@
+package com.androidvip.sysctlgui.ui.params.browse
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
+import androidx.compose.material.icons.rounded.Favorite
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.PreviewDynamicColors
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.compose.ui.unit.dp
+import com.androidvip.sysctlgui.R
+import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
+import com.androidvip.sysctlgui.models.UiKernelParam
+
+@Composable
+fun ParamFileRow(
+ modifier: Modifier = Modifier,
+ param: UiKernelParam,
+ onParamClicked: (UiKernelParam) -> Unit,
+ showFavoriteIcon: Boolean = true,
+) {
+ Box(modifier = Modifier.clickable { onParamClicked(param) }) {
+ val rowDescription = if (param.isDirectory) {
+ "Directory: ${param.name}"
+ } else {
+ "Parameter: ${param.name}"
+ }
+ Row(
+ modifier = modifier
+ .semantics(mergeDescendants = true) { contentDescription = rowDescription }
+ .padding(horizontal = 16.dp, vertical = 12.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ ParamIcon(param = param)
+
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = param.lastNameSegment,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onBackground,
+ fontWeight = if (param.isDirectory) FontWeight.Bold else FontWeight.Normal
+ )
+
+ if (param.value.isNotBlank() && !param.isDirectory) {
+ Text(
+ text = param.value,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+
+ TrailingIcon(param = param, showFavoriteIcon = showFavoriteIcon)
+ }
+ }
+}
+
+
+@Composable
+private fun ParamIcon(param: UiKernelParam) {
+ val primaryContainerColor = MaterialTheme.colorScheme.primaryContainer
+ val secondaryContainerColor = MaterialTheme.colorScheme.secondaryContainer
+ val containerColor by remember(param.isDirectory) {
+ derivedStateOf {
+ if (param.isDirectory) primaryContainerColor else secondaryContainerColor
+ }
+ }
+
+ val onPrimaryContainerColor = MaterialTheme.colorScheme.onPrimaryContainer
+ val onSecondaryContainerColor = MaterialTheme.colorScheme.onSecondaryContainer
+ val iconColor by remember(param.isDirectory) {
+ derivedStateOf {
+ if (param.isDirectory) onPrimaryContainerColor else onSecondaryContainerColor
+ }
+ }
+
+ val iconId = if (param.isDirectory) R.drawable.ic_folder else R.drawable.ic_file
+
+ Box(
+ modifier = Modifier
+ .size(42.dp)
+ .clip(CircleShape)
+ .background(containerColor),
+ contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ painter = painterResource(iconId),
+ contentDescription = "Parameter icon",
+ modifier = Modifier.size(24.dp),
+ tint = iconColor
+ )
+ }
+}
+
+@Composable
+private fun TrailingIcon(param: UiKernelParam, showFavoriteIcon: Boolean) {
+ if (param.isDirectory) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
+ contentDescription = "Navigate do directory",
+ modifier = Modifier.size(24.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ } else if (param.isFavorite && showFavoriteIcon) {
+ Icon(
+ imageVector = Icons.Rounded.Favorite,
+ contentDescription = "Favorite",
+ tint = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.size(18.dp)
+ )
+ }
+}
+
+@Composable
+@PreviewLightDark
+@PreviewDynamicColors
+private fun ParamFileRowPreview() {
+ val param = UiKernelParam(
+ name = "vm.swappiness",
+ path = "/proc/sys/vm/swappiness",
+ value = "0"
+ )
+
+ SysctlGuiTheme(dynamicColor = true) {
+ Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
+ ParamFileRow(param = param.copy(path = "C://"), onParamClicked = {})
+ ParamFileRow(param = param.copy(path = "/home"), onParamClicked = {})
+ ParamFileRow(param = param, onParamClicked = {})
+ ParamFileRow(param = param.copy(isFavorite = true), onParamClicked = {})
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamRow.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamRow.kt
new file mode 100644
index 0000000..da3c96b
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamRow.kt
@@ -0,0 +1,106 @@
+package com.androidvip.sysctlgui.ui.params.browse
+
+import androidx.compose.foundation.background
+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.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Favorite
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.compose.ui.unit.dp
+import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
+import com.androidvip.sysctlgui.models.UiKernelParam
+
+@Composable
+fun ParamRow(
+ modifier: Modifier = Modifier,
+ param: UiKernelParam,
+ onParamClicked: (UiKernelParam) -> Unit,
+ showFullName: Boolean = false
+) {
+ val rowDescription = "Parameter: ${param.name}"
+ val rowState = if (param.isFavorite) "Marked as favorite" else ""
+
+ Row(
+ modifier = modifier
+ .heightIn(min = 64.dp)
+ .clickable { onParamClicked(param) },
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column(
+ modifier = Modifier
+ .semantics(mergeDescendants = showFullName) {
+ this.contentDescription = rowDescription
+ this.stateDescription = rowState
+ }
+ .padding(16.dp)
+ .weight(1f),
+ verticalArrangement = Arrangement.spacedBy(2.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = if (showFullName) param.name else param.lastNameSegment,
+ modifier = Modifier.fillMaxWidth(),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onBackground,
+ fontWeight = FontWeight.Medium
+ )
+ if (param.value.isNotBlank()) {
+ Text(
+ text = param.value,
+ modifier = Modifier.fillMaxWidth(),
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ }
+ }
+
+ if (param.isFavorite) {
+ Icon(
+ imageVector = Icons.Rounded.Favorite,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.secondary,
+ modifier = Modifier
+ .padding(end = 16.dp)
+ .size(18.dp)
+ )
+ }
+ }
+}
+
+@Composable
+@PreviewLightDark
+private fun ParamRowPreview() {
+ val param = UiKernelParam(
+ name = "vm.swappiness",
+ path = "/proc/sys/vm/swappiness",
+ value = "0"
+ )
+
+ SysctlGuiTheme(contrastLevel = 1) {
+ Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
+ ParamRow(param = param, onParamClicked = {}, showFullName = true)
+ ParamRow(param = param.copy(isFavorite = true), onParamClicked = {},)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/ActionToggleButton.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/ActionToggleButton.kt
new file mode 100644
index 0000000..3bf22ef
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/ActionToggleButton.kt
@@ -0,0 +1,188 @@
+package com.androidvip.sysctlgui.ui.params.edit
+
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.scaleIn
+import androidx.compose.animation.scaleOut
+import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.FloatingActionButtonDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+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.graphics.painter.Painter
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.PreviewDynamicColors
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.compose.ui.unit.dp
+import com.androidvip.sysctlgui.R
+import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
+
+@Composable
+internal fun ActionToggleButton(
+ modifier: Modifier = Modifier,
+ isActive: Boolean,
+ iconOnActive: Painter,
+ iconOnInactive: Painter,
+ contentDescription: String? = null,
+ onToggle: (Boolean) -> Unit,
+) {
+ val containerColor by animateColorAsState(
+ targetValue = if (isActive) {
+ MaterialTheme.colorScheme.secondary
+ } else {
+ MaterialTheme.colorScheme.background
+ },
+ label = "FabContainerColor"
+ )
+
+ val defaultElevation by animateDpAsState(
+ targetValue = if (isActive) 4.dp else 2.dp,
+ label = "FabElevation"
+ )
+
+ FloatingActionButton(
+ modifier = modifier,
+ onClick = { onToggle(!isActive) },
+ containerColor = containerColor,
+ elevation = FloatingActionButtonDefaults.elevation(
+ defaultElevation = defaultElevation,
+ pressedElevation = defaultElevation * 2
+ ),
+ shape = CircleShape,
+ ) {
+ AnimatedContent(
+ targetState = isActive,
+ label = "ActionToggleButtonAnimation",
+ transitionSpec = {
+ val enterTransition = scaleIn(
+ animationSpec = spring(
+ dampingRatio = Spring.DampingRatioMediumBouncy,
+ stiffness = Spring.StiffnessLow
+ ),
+ initialScale = 1.25f
+ ) + fadeIn(animationSpec = tween(durationMillis = 200))
+
+ val exitTransition = scaleOut(
+ animationSpec = tween(durationMillis = 150),
+ targetScale = 1.25f
+ ) + fadeOut(animationSpec = tween(durationMillis = 100))
+
+ enterTransition togetherWith exitTransition
+ }
+ ) { isCurrentlyActive ->
+ val iconTint = if (isCurrentlyActive) {
+ MaterialTheme.colorScheme.onSecondary
+ } else {
+ MaterialTheme.colorScheme.onSurfaceVariant
+ }
+
+ Icon(
+ painter = if (isCurrentlyActive) iconOnActive else iconOnInactive,
+ contentDescription = "Toggle $contentDescription",
+ tint = iconTint
+ )
+ }
+ }
+}
+
+@Composable
+internal fun FavoriteButton(
+ modifier: Modifier = Modifier,
+ isFavorite: Boolean,
+ onFavoriteClick: (Boolean) -> Unit,
+) {
+ ActionToggleButton(
+ modifier = modifier,
+ isActive = isFavorite,
+ iconOnActive = painterResource(R.drawable.ic_favorite),
+ iconOnInactive = painterResource(R.drawable.ic_favorite_outlined),
+ contentDescription = "favorite",
+ onToggle = onFavoriteClick
+ )
+}
+@Composable
+internal fun TaskerButton(
+ modifier: Modifier = Modifier,
+ isTaskerParam: Boolean,
+ onToggle: (Boolean) -> Unit,
+) {
+ ActionToggleButton(
+ modifier = modifier,
+ isActive = isTaskerParam,
+ iconOnActive = painterResource(R.drawable.ic_tasker),
+ iconOnInactive = painterResource(R.drawable.ic_tasker_outlined),
+ contentDescription = "tasker param",
+ onToggle = onToggle
+ )
+}
+
+@PreviewLightDark
+@Composable
+private fun FavoriteButtonStatesPreview() {
+ SysctlGuiTheme {
+ Surface(color = MaterialTheme.colorScheme.background) {
+ Row(
+ modifier = Modifier.padding(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ FavoriteButton(
+ isFavorite = false,
+ onFavoriteClick = {}
+ )
+ FavoriteButton(
+ isFavorite = true,
+ onFavoriteClick = {}
+ )
+ TaskerButton(
+ isTaskerParam = false,
+ onToggle = {}
+ )
+ TaskerButton(
+ isTaskerParam = true,
+ onToggle = {}
+ )
+ }
+ }
+ }
+}
+
+@Composable
+@PreviewDynamicColors
+private fun FavoriteButtonInteractivePreview() {
+ SysctlGuiTheme(dynamicColor = true) {
+ Surface {
+ var isFavorite by remember { mutableStateOf(false) }
+
+ Row(
+ modifier = Modifier.padding(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ FavoriteButton(
+ isFavorite = isFavorite,
+ onFavoriteClick = { isFavorite = !isFavorite },
+ )
+ TaskerButton(
+ isTaskerParam = isFavorite,
+ onToggle = { isFavorite = !isFavorite }
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditKernelParamActivity.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditKernelParamActivity.kt
deleted file mode 100644
index 459e4e1..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditKernelParamActivity.kt
+++ /dev/null
@@ -1,108 +0,0 @@
-package com.androidvip.sysctlgui.ui.params.edit
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.annotation.StringRes
-import androidx.appcompat.app.AlertDialog
-import androidx.lifecycle.lifecycleScope
-import com.androidvip.sysctlgui.R
-import com.androidvip.sysctlgui.data.models.KernelParam
-import com.androidvip.sysctlgui.toast
-import com.androidvip.sysctlgui.utils.ComposeTheme
-import kotlinx.coroutines.launch
-import org.koin.androidx.viewmodel.ext.android.viewModel
-
-class EditKernelParamActivity : ComponentActivity() {
- private val viewModel by viewModel()
- private val isEditingSavedParam: Boolean
- get() = intent.getBooleanExtra(EXTRA_EDIT_SAVED_PARAM, false)
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- setContent {
- ComposeTheme {
- EditParamScreen(viewModel = viewModel)
- }
- }
-
- lifecycleScope.launch {
- viewModel.effect.collect(::handleViewEffect)
- }
-
- handleIntent(intent)
- }
-
- private fun handleIntent(intent: Intent) {
- val param = intent.getParcelableExtra(EXTRA_PARAM) as? KernelParam
- if (param != null) {
- viewModel.onEvent(EditParamViewEvent.ReceivedParam(param, this))
- } else {
- finishWithInvalidParamError()
- }
- }
-
- private fun finishWithInvalidParamError() {
- toast(R.string.unexpected_error)
- finish()
- }
-
- private fun handleViewEffect(effect: EditParamViewEffect) {
- when (effect) {
- EditParamViewEffect.NavigateBack -> onBackPressedDispatcher.onBackPressed()
- EditParamViewEffect.ShowTaskerListSelection -> {
- selectTaskerListAsDialog { listId ->
- viewModel.onEvent(EditParamViewEvent.TaskerListSelected(listId))
- }
- }
-
- is EditParamViewEffect.ShowApplyError -> doAfterParamNotApplied(effect.messageRes)
- is EditParamViewEffect.ShowApplySuccess -> doAfterParamApplied()
- }
- }
-
- private fun selectTaskerListAsDialog(block: (Int) -> Unit) {
- AlertDialog.Builder(this)
- .setTitle(R.string.select_tasker_list)
- .setNegativeButton(android.R.string.cancel) { _, _ -> }
- .setSingleChoiceItems(R.array.tasker_lists, -1) { dialog, which ->
- block(which)
- dialog.dismiss()
- }.also {
- if (!isFinishing) {
- it.show()
- }
- }
- }
-
- private fun doAfterParamApplied() {
- if (isEditingSavedParam) {
- setResult(Activity.RESULT_OK)
- toast(R.string.done)
- finish()
- }
- }
-
- private fun doAfterParamNotApplied(@StringRes messageRes: Int) {
- toast(messageRes)
- if (isEditingSavedParam) {
- setResult(Activity.RESULT_CANCELED)
- finish()
- }
- }
-
- companion object {
- const val EXTRA_EDIT_SAVED_PARAM = "edit_saved_param"
- const val EXTRA_PARAM = "param"
-
- fun getIntent(context: Context, param: KernelParam): Intent {
- return Intent(context, EditKernelParamActivity::class.java).apply {
- putExtra(EXTRA_PARAM, param)
- }
- }
- }
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditParamScreen.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditParamScreen.kt
index 30b6e34..c9ee615 100644
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditParamScreen.kt
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditParamScreen.kt
@@ -1,446 +1,592 @@
package com.androidvip.sysctlgui.ui.params.edit
-import androidx.annotation.DrawableRes
+import android.content.ClipData
+import android.widget.Toast
+import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.SizeTransform
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.background
+import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.widthIn
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.ArrowBack
-import androidx.compose.material.icons.outlined.Check
-import androidx.compose.material.icons.outlined.FavoriteBorder
-import androidx.compose.material.icons.outlined.Refresh
-import androidx.compose.material.icons.outlined.Warning
+import androidx.compose.material.icons.rounded.Done
+import androidx.compose.material.icons.rounded.Edit
+import androidx.compose.material.icons.rounded.Warning
+import androidx.compose.material3.AssistChip
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExtendedFloatingActionButton
-import androidx.compose.material3.FloatingActionButtonDefaults
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
-import androidx.compose.material3.LargeFloatingActionButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.SmallFloatingActionButton
-import androidx.compose.material3.SnackbarDuration
-import androidx.compose.material3.SnackbarHost
-import androidx.compose.material3.SnackbarHostState
-import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
+import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.platform.ClipEntry
+import androidx.compose.ui.platform.LocalClipboard
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.fromHtml
import androidx.compose.ui.text.input.KeyboardType
-import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.PreviewDynamicColors
+import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
+import androidx.core.view.HapticFeedbackConstantsCompat
+import androidx.core.view.ViewCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.androidvip.sysctlgui.R
-import com.androidvip.sysctlgui.data.models.KernelParam
+import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
+import com.androidvip.sysctlgui.domain.enums.CommitMode
+import com.androidvip.sysctlgui.domain.models.ParamDocumentation
+import com.androidvip.sysctlgui.models.UiKernelParam
+import com.androidvip.sysctlgui.ui.components.ErrorContainer
+import com.androidvip.sysctlgui.ui.components.SingleChoiceDialog
+import com.androidvip.sysctlgui.ui.main.MainViewEffect
+import com.androidvip.sysctlgui.ui.main.MainViewEvent
+import com.androidvip.sysctlgui.ui.main.MainViewModel
+import com.androidvip.sysctlgui.ui.main.MainViewState
+import com.androidvip.sysctlgui.utils.Consts
+import com.androidvip.sysctlgui.utils.browse
+import com.androidvip.sysctlgui.utils.performHapticFeedbackForToggle
+import kotlinx.coroutines.launch
+import org.intellij.lang.annotations.Language
+import org.koin.androidx.compose.koinViewModel
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun EditParamScreen(viewModel: EditParamViewModel) {
- val state by viewModel.uiState.collectAsStateWithLifecycle()
- val snackbarHostState = remember { SnackbarHostState() }
- val listState = rememberLazyListState()
- val expandedFabState = remember {
- derivedStateOf {
- listState.firstVisibleItemIndex == 0
- }
+fun EditParamScreen(
+ viewModel: EditParamViewModel = koinViewModel(),
+ mainViewModel: MainViewModel = koinViewModel(),
+ onNavigateBack: () -> Unit
+) {
+ val context = LocalContext.current
+ val state = viewModel.uiState.collectAsStateWithLifecycle()
+ val taskerListOptions = listOf("Primary", "Secondary")
+ var showSelectTaskerListDialog by rememberSaveable { mutableStateOf(true) }
+ var selectedOptionIndex by rememberSaveable {
+ mutableIntStateOf(Consts.LIST_NUMBER_PRIMARY_TASKER)
}
+ var errorMessage by rememberSaveable { mutableStateOf("") }
+ var showError by rememberSaveable { mutableStateOf(false) }
- Scaffold(
- topBar = {
- TopAppBar(
- title = { Text(text = stringResource(id = R.string.edit_params)) },
- navigationIcon = {
- IconButton(onClick = { viewModel.onEvent(EditParamViewEvent.BackPressed) }) {
- Icon(
- imageVector = Icons.Outlined.ArrowBack,
- contentDescription = stringResource(id = R.string.restore_param),
- tint = MaterialTheme.colorScheme.onPrimaryContainer
- )
- }
- }
- )
- },
- snackbarHost = { SnackbarHost(snackbarHostState) },
- floatingActionButton = {
- FloatingActionButtonColumn(
- onReset = { viewModel.onEvent(EditParamViewEvent.ResetPressed) },
- onApply = { viewModel.onEvent(EditParamViewEvent.ApplyPressed) },
- hasApplied = state.hasApplied,
- expanded = expandedFabState.value
- )
- }
- ) { contentPadding ->
- LazyColumn(
- modifier = Modifier
- .fillMaxSize()
- .padding(contentPadding),
- state = listState
- ) {
- item { ParamTexts(param = state.param) }
- item {
- ParamValues(
- param = state.param,
- appliedValue = state.restoreValue,
- keyboardType = state.keyboardType,
- singleLine = state.singleLine,
- onValueChange = {
- viewModel.onEvent(EditParamViewEvent.ParamValueInputChanged(it))
- }
- )
- }
- item {
- ParamActions(
- onFavoriteClicked = {
- viewModel.onEvent(EditParamViewEvent.FavoritePressed(state.param.favorite))
- },
- onTaskerClicked = { viewModel.onEvent(EditParamViewEvent.TaskerPressed) },
- param = state.param,
- taskerAvailable = state.taskerAvailable
+ LaunchedEffect(Unit) {
+ mainViewModel.onEvent(
+ MainViewEvent.OnSateChangeRequested(
+ MainViewState(
+ topBarTitle = "Edit kernel parameter",
+ showTopBar = true,
+ showNavBar = false,
+ showBackButton = true,
+ showSearchAction = false
)
+ )
+ )
+
+ mainViewModel.effect.collect { effect ->
+ if (effect is MainViewEffect.ActUponSckbarActionPerformed) {
+ viewModel.onEvent(EditParamViewEvent.UndoRequested)
}
- item { ParamDocs(info = state.paramInfo) }
}
}
- val successMessage = stringResource(id = R.string.done)
- val undoMessage = stringResource(id = R.string.undo)
- LaunchedEffect(key1 = Unit) {
+ LaunchedEffect(viewModel.effect) {
viewModel.effect.collect { effect ->
when (effect) {
+ EditParamViewEffect.GoBack -> onNavigateBack()
+
+ is EditParamViewEffect.ShowError -> {
+ errorMessage = effect.message
+ showError = true
+ }
+
+ is EditParamViewEffect.OpenBrowser -> context.browse(effect.url)
+
is EditParamViewEffect.ShowApplySuccess -> {
- val result = snackbarHostState.showSnackbar(
- message = successMessage,
- actionLabel = undoMessage,
- duration = SnackbarDuration.Short
+ mainViewModel.onEvent(
+ MainViewEvent.ShowSnackbarRequested(
+ message = "Value applied successfully",
+ actionLabel = "Undo"
+ )
)
-
- if (result == SnackbarResult.ActionPerformed) {
- viewModel.onEvent(EditParamViewEvent.ResetPressed)
- }
}
- else -> Unit
}
}
}
+
+ EditParamContent(
+ state = state.value,
+ showError = showError,
+ errorMessage = errorMessage,
+ onValueApply = {
+ viewModel.onEvent(EditParamViewEvent.ApplyPressed(it))
+ },
+ onTaskerClicked = {
+ viewModel.onEvent(EditParamViewEvent.TaskerTogglePressed(it, selectedOptionIndex))
+ },
+ onDocsReadMorePressed = {
+ viewModel.onEvent(EditParamViewEvent.DocumentationReadMoreClicked)
+ },
+ onFavoriteToggle = {
+ viewModel.onEvent(EditParamViewEvent.FavoriteTogglePressed(it))
+ },
+ onErrorAnimationEnd = { showError = false },
+ taskerListNameResolver = { listId -> taskerListOptions.getOrNull(listId).orEmpty() }
+ )
+
+ SingleChoiceDialog(
+ showDialog = showSelectTaskerListDialog,
+ title = "Choose a Tasker list",
+ options = taskerListOptions,
+ initialSelectedOptionIndex = selectedOptionIndex,
+ onDismissRequest = { showSelectTaskerListDialog = false },
+ onOptionSelected = {
+ selectedOptionIndex = it
+ viewModel.onEvent(EditParamViewEvent.TaskerTogglePressed(true, it))
+ }
+ )
}
@Composable
-private fun FloatingActionButtonColumn(
- onReset: () -> Unit,
- onApply: () -> Unit,
- hasApplied: Boolean,
- expanded: Boolean
+private fun EditParamContent(
+ state: EditParamViewState,
+ showError: Boolean,
+ errorMessage: String,
+ onDocsReadMorePressed: () -> Unit,
+ onValueApply: (String) -> Unit,
+ onFavoriteToggle: (Boolean) -> Unit,
+ onTaskerClicked: (Boolean) -> Unit,
+ onErrorAnimationEnd: () -> Unit,
+ taskerListNameResolver: (Int) -> String = { "List #$it" },
) {
+ val param = state.kernelParam
+ val view = LocalView.current
+ val context = LocalContext.current
+ val coroutineScope = rememberCoroutineScope()
+ val clipboardManager = LocalClipboard.current
+ val scrollState = rememberScrollState()
+
+ val copyParamContentToClipboard = {
+ val clipData = ClipData.newPlainText(
+ "Kernel Parameter",
+ "${param.lastNameSegment}=${param.value} (${param.path})"
+ )
+ val clipEntry = ClipEntry(clipData)
+ coroutineScope.launch {
+ clipboardManager.setClipEntry(clipEntry)
+ }
+ Toast.makeText(context, "Copied to clipboard", Toast.LENGTH_SHORT).show()
+ }
+
Column(
- verticalArrangement = Arrangement.spacedBy(12.dp),
- horizontalAlignment = Alignment.End,
- modifier = Modifier.padding(bottom = 8.dp)
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(scrollState)
+ .background(MaterialTheme.colorScheme.surfaceContainer)
) {
- AnimatedVisibility(hasApplied) {
- SmallFloatingActionButton(
- onClick = onReset,
- containerColor = MaterialTheme.colorScheme.tertiaryContainer,
- contentColor = MaterialTheme.colorScheme.onTertiaryContainer
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(MaterialTheme.colorScheme.background)
+ ) {
+ Text(
+ text = param.lastNameSegment,
+ style = MaterialTheme.typography.displayLarge,
+ modifier = Modifier
+ .combinedClickable(
+ enabled = true,
+ onClick = {
+ Toast.makeText(context, "Long press to copy", Toast.LENGTH_SHORT).show()
+ },
+ onLongClick = copyParamContentToClipboard
+ )
+ .padding(start = 16.dp, end = 16.dp, top = 64.dp),
+ maxLines = 3,
+ color = MaterialTheme.colorScheme.onBackground,
+ overflow = TextOverflow.Ellipsis
+ )
+
+ Row(
+ modifier = Modifier.padding(
+ horizontal = 16.dp,
+ vertical = if (param.isTaskerParam) 0.dp else 24.dp
+ ),
+ verticalAlignment = Alignment.CenterVertically
) {
- Icon(
- imageVector = Icons.Outlined.Refresh,
- contentDescription = stringResource(id = R.string.restore_param),
- tint = MaterialTheme.colorScheme.onTertiaryContainer
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = param.name,
+ style = MaterialTheme.typography.titleMedium,
+ modifier = Modifier.padding(vertical = 8.dp),
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+
+ Text(
+ text = param.path,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+
+ Spacer(modifier = Modifier.width(8.dp))
+
+ if (state.taskerAvailable) {
+ TaskerButton(
+ isTaskerParam = param.isTaskerParam,
+ onToggle = { newState ->
+ performHapticFeedbackForToggle(newState, view)
+ onTaskerClicked(newState)
+ },
+ modifier = Modifier.scale(0.85f)
+ )
+ }
+
+ FavoriteButton(
+ isFavorite = param.isFavorite,
+ onFavoriteClick = { newState ->
+ performHapticFeedbackForToggle(newState, view)
+ onFavoriteToggle(newState)
+ },
+ modifier = Modifier.scale(0.85f)
)
}
- }
- ExtendedFloatingActionButton(
- text = {
- Text(
- text = stringResource(id = R.string.apply_param),
- color = MaterialTheme.colorScheme.onSecondaryContainer,
- fontWeight = FontWeight.Medium
- )
- },
- icon = {
- Icon(
- imageVector = Icons.Outlined.Check,
- contentDescription = stringResource(id = R.string.apply_param),
- tint = MaterialTheme.colorScheme.onSecondaryContainer
+ if (param.isTaskerParam && state.taskerAvailable) {
+ val listName = taskerListNameResolver(param.taskerList)
+ AssistChip(
+ onClick = { onTaskerClicked(true) },
+ modifier = Modifier.padding(16.dp),
+ label = { Text(text = "Tasker list: $listName") },
+ leadingIcon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_tasker),
+ contentDescription = "Tasker list",
+ tint = MaterialTheme.colorScheme.tertiary
+ )
+ }
)
- },
- onClick = onApply,
- expanded = expanded,
- containerColor = MaterialTheme.colorScheme.secondaryContainer
+ }
+ }
+
+ ParamValueContent(
+ modifier = Modifier.padding(16.dp),
+ param = param,
+ keyboardType = state.keyboardType,
+ onValueApply = onValueApply
+ )
+
+ AnimatedVisibility(
+ visible = showError && errorMessage.isNotEmpty(),
+ modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
+ ) {
+ ErrorContainer(message = errorMessage, onAnimationEnd = onErrorAnimationEnd)
+ }
+
+ ParamDocs(
+ modifier = Modifier.padding(16.dp),
+ documentation = state.documentation,
+ onReadMorePressed = onDocsReadMorePressed
)
}
}
@Composable
-private fun ParamTexts(param: KernelParam) {
- Card(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp, vertical = 8.dp)
+private fun ParamValueContent(
+ modifier: Modifier = Modifier,
+ param: UiKernelParam,
+ keyboardType: KeyboardType,
+ onValueApply: (String) -> Unit
+) {
+ var isEditing by remember { mutableStateOf(false) }
+ var editedValue by remember(param.value) { mutableStateOf(param.value) }
+ val view = LocalView.current
+
+ HorizontalDivider()
+
+ Row(
+ modifier = modifier,
+ verticalAlignment = Alignment.CenterVertically
) {
- Column(modifier = Modifier.padding(16.dp)) {
+ Column(modifier = Modifier.weight(1f)) {
Text(
- text = stringResource(id = R.string.param),
- style = MaterialTheme.typography.headlineMedium,
- color = MaterialTheme.colorScheme.onSurface
+ text = "Parameter value",
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onBackground
)
- ParamRow(iconRes = R.drawable.ic_config, text = param.configName)
- ParamRow(iconRes = R.drawable.ic_name, text = param.shortName)
- ParamRow(iconRes = R.drawable.ic_folder_outline, text = param.path)
+ EditableParamValue(
+ isEditing = isEditing,
+ paramValue = param.value,
+ editedValue = editedValue,
+ keyboardType = keyboardType,
+ onEditorValueChange = { editedValue = it },
+ modifier = Modifier
+ .padding(top = 8.dp)
+ .fillMaxWidth()
+ )
+ }
+
+ IconButton(
+ onClick = {
+ if (isEditing) {
+ onValueApply(editedValue)
+ ViewCompat.performHapticFeedback(view, HapticFeedbackConstantsCompat.CONFIRM)
+ }
+ isEditing = !isEditing
+ }
+ ) {
+ AnimatedContent(
+ targetState = isEditing,
+ label = "EditButtonAnimation",
+ ) { editingActive ->
+ if (editingActive) {
+ Icon(
+ imageVector = Icons.Rounded.Done,
+ contentDescription = "Apply",
+ tint = MaterialTheme.colorScheme.primary
+ )
+ } else {
+ Icon(
+ imageVector = Icons.Rounded.Edit,
+ contentDescription = "Edit",
+ tint = MaterialTheme.colorScheme.primary
+ )
+ }
+ }
}
}
}
@Composable
-private fun ParamRow(@DrawableRes iconRes: Int, text: String) {
- Row(
- modifier = Modifier.padding(top = 16.dp, start = 16.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(24.dp)
- ) {
- Icon(
- painter = painterResource(id = iconRes),
- contentDescription = "",
- tint = MaterialTheme.colorScheme.onSurface,
- modifier = Modifier.size(24.dp)
- )
- Text(
- text = text,
- style = MaterialTheme.typography.titleMedium,
- color = MaterialTheme.colorScheme.onSurface
- )
+fun EditableParamValue(
+ isEditing: Boolean,
+ paramValue: String,
+ editedValue: String,
+ keyboardType: KeyboardType = KeyboardType.Text,
+ onEditorValueChange: (String) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Box(modifier = modifier) {
+ AnimatedContent(
+ targetState = isEditing,
+ label = "EditableValueAnimation",
+ transitionSpec = {
+ if (targetState) {
+ slideInVertically { it } + fadeIn() togetherWith
+ slideOutVertically { -it } + fadeOut()
+ } else {
+ slideInVertically { -it } + fadeIn() togetherWith
+ slideOutVertically { it } + fadeOut()
+ }.using(
+ SizeTransform(clip = true)
+ )
+ }
+ ) { editingActive ->
+ if (editingActive) {
+ OutlinedTextField(
+ value = editedValue,
+ onValueChange = onEditorValueChange,
+ label = { Text("New value") },
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(keyboardType = keyboardType),
+ modifier = Modifier.fillMaxWidth()
+ )
+ } else {
+ Text(
+ text = paramValue,
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
}
}
@Composable
-private fun ParamValues(
- param: KernelParam,
- onValueChange: (String) -> Unit,
- appliedValue: String,
- keyboardType: KeyboardType,
- singleLine: Boolean
+private fun ParamDocs(
+ modifier: Modifier = Modifier,
+ documentation: ParamDocumentation?,
+ onReadMorePressed: () -> Unit,
) {
- var typedValue by rememberSaveable { mutableStateOf(param.value) }
-
- Column(modifier = Modifier.padding(16.dp)) {
- Text(
- text = stringResource(id = R.string.value),
- style = MaterialTheme.typography.headlineMedium,
- color = MaterialTheme.colorScheme.onBackground
- )
+ HorizontalDivider()
+ Column(modifier = modifier) {
Text(
- modifier = Modifier.padding(top = 16.dp),
- text = stringResource(id = R.string.current_value),
+ text = "Documentation",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onBackground
)
- OutlinedTextField(
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 8.dp),
- textStyle = MaterialTheme.typography.bodyLarge.copy(
- color = MaterialTheme.colorScheme.onBackground
- ),
- keyboardOptions = KeyboardOptions(
- keyboardType = keyboardType,
- imeAction = ImeAction.Done
- ),
- maxLines = 3,
- singleLine = singleLine,
- value = typedValue,
- onValueChange = { typedValue = it; onValueChange(it) }
- )
- Text(
- modifier = Modifier.padding(top = 16.dp),
- text = stringResource(id = R.string.last_applied_value),
- style = MaterialTheme.typography.titleMedium,
- color = MaterialTheme.colorScheme.onBackground
- )
- SelectionContainer {
- Text(
- text = appliedValue,
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onBackground
- )
- }
- }
-}
+ if (documentation != null) {
+ val documentationText = if (!documentation.documentationHtml.isNullOrEmpty()) {
+ AnnotatedString.fromHtml(
+ htmlString = documentation.documentationHtml.orEmpty(),
+ linkStyles = TextLinkStyles(
+ style = MaterialTheme.typography.bodyMedium.toSpanStyle().copy(
+ color = MaterialTheme.colorScheme.primary,
+ textDecoration = TextDecoration.Underline,
+ fontWeight = FontWeight.Medium
+ ),
+ pressedStyle = MaterialTheme.typography.bodyMedium.toSpanStyle().copy(
+ color = MaterialTheme.colorScheme.tertiary,
+ textDecoration = TextDecoration.Underline,
+ fontWeight = FontWeight.Medium
+ )
+ )
+ )
+ } else {
+ AnnotatedString(documentation.documentationText)
+ }
-@Composable
-private fun ParamActions(
- onFavoriteClicked: () -> Unit,
- onTaskerClicked: () -> Unit,
- param: KernelParam,
- taskerAvailable: Boolean
-) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(24.dp, Alignment.CenterHorizontally)
- ) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- LargeFloatingActionButton(
- modifier = Modifier.size(74.dp),
- onClick = onFavoriteClicked,
- containerColor = if (param.favorite) {
- MaterialTheme.colorScheme.errorContainer
- } else {
- MaterialTheme.colorScheme.outlineVariant
- },
- contentColor = if (param.favorite) {
- MaterialTheme.colorScheme.onErrorContainer
- } else {
- MaterialTheme.colorScheme.outline
- },
- elevation = FloatingActionButtonDefaults.elevation(defaultElevation = 0.dp)
- ) {
- Icon(
- imageVector = Icons.Outlined.FavoriteBorder,
- contentDescription = stringResource(id = R.string.set_favorite)
+ SelectionContainer {
+ Text(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp),
+ text = documentationText,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
- Text(
+ TextButton(
+ onClick = onReadMorePressed,
modifier = Modifier
- .widthIn(max = 82.dp)
- .padding(top = 2.dp),
- text = if (param.favorite) {
- stringResource(id = R.string.remove_from_favorites)
- } else {
- stringResource(id = R.string.set_favorite)
- },
- textAlign = TextAlign.Center,
- style = MaterialTheme.typography.bodyMedium,
- maxLines = 2,
- overflow = TextOverflow.Ellipsis,
- color = MaterialTheme.colorScheme.onBackground
- )
- }
-
- if (taskerAvailable) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally
+ .padding(vertical = 8.dp)
+ .align(Alignment.End)
) {
- LargeFloatingActionButton(
- modifier = Modifier.size(74.dp),
- onClick = onTaskerClicked,
- containerColor = if (param.taskerParam) {
- MaterialTheme.colorScheme.primaryContainer
- } else {
- MaterialTheme.colorScheme.outlineVariant
- },
- contentColor = if (param.taskerParam) {
- MaterialTheme.colorScheme.onPrimaryContainer
- } else {
- MaterialTheme.colorScheme.outline
- },
- elevation = FloatingActionButtonDefaults.elevation(defaultElevation = 0.dp)
+ Text(text = "Read more")
+ }
+ } else {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(24.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.errorContainer
+ )
+ ) {
+ Row(
+ modifier = Modifier.padding(24.dp),
+ horizontalArrangement = Arrangement.spacedBy(
+ 16.dp,
+ Alignment.CenterHorizontally
+ )
) {
Icon(
- painter = painterResource(id = R.drawable.ic_action_tasker),
- contentDescription = stringResource(id = R.string.set_favorite),
- tint = MaterialTheme.colorScheme.onPrimaryContainer
+ imageVector = Icons.Rounded.Warning,
+ contentDescription = stringResource(android.R.string.dialog_alert_title),
+ tint = MaterialTheme.colorScheme.onErrorContainer
+ )
+ Text(
+ text = "No documentation available",
+ style = MaterialTheme.typography.bodyLarge.copy(),
+ fontWeight = FontWeight.Medium,
+ color = MaterialTheme.colorScheme.onErrorContainer
)
}
- Text(
- modifier = Modifier
- .widthIn(max = 82.dp)
- .padding(top = 2.dp),
- text = if (param.taskerParam) {
- stringResource(id = R.string.remove_from_tasker_list)
- } else {
- stringResource(id = R.string.add_to_tasker_list)
- },
- textAlign = TextAlign.Center,
- style = MaterialTheme.typography.bodyMedium,
- maxLines = 2,
- overflow = TextOverflow.Ellipsis,
- color = MaterialTheme.colorScheme.onBackground
- )
}
}
}
}
@Composable
-private fun ParamDocs(info: String?) {
- Text(
- modifier = Modifier.padding(top = 16.dp, bottom = 0.dp, start = 16.dp, end = 16.dp),
- text = stringResource(id = R.string.information),
- style = MaterialTheme.typography.titleMedium,
- color = MaterialTheme.colorScheme.onBackground
- )
+@PreviewLightDark
+@PreviewDynamicColors
+private fun EditParamContentPreview() {
- if (info != null) {
- SelectionContainer {
- Text(
- modifier = Modifier.padding(top = 4.dp, bottom = 16.dp, start = 16.dp, end = 16.dp),
- text = info,
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onBackground
+ @Language("html")
+ val htmlDocs = """
+ Correctable memory errors are very common on servers.
+ Soft-offline is kernel’s solution for memory pages having
+ (excessive) corrected memory errors.
+
+ For different types_of page, soft-offline has different behaviors / costs.
+
+ - For a raw error page,
soft-offline migrates the in-use page’s content to a new raw page.
+ - For a page that is part of a transparent hugepage,
soft-offline splits the transparent hugepage into raw pages, then migrates only the raw error page. As a result, user is transparently backed by 1 less hugepage, impacting memory access performance.
+ - For a page that is part of a HugeTLB hugepage,
soft-offline first migrates the entire HugeTLB hugepage, during which a free hugepage will be consumed as migration target. Then the original hugepage is dissolved into raw pages without compensation, reducing the capacity of the HugeTLB pool by 1.
+ - It is user’s call to choose between reliability (staying away from fragile physical memory) vs performance / capacity implications in transparent and HugeTLB cases.
+
+ """.trimIndent()
+ .replace(
+ "",
+ ""
+ )
+ .replace("", "")
+
+ var showError by remember { mutableStateOf(true) }
+
+ SysctlGuiTheme(dynamicColor = true) {
+ Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
+
+ val state = EditParamViewState(
+ kernelParam = UiKernelParam(
+ name = "vm.enable_soft_offline",
+ path = "/proc/sys/vm/enable_soft_offline",
+ value = "1",
+ taskerList = 1,
+ isTaskerParam = false,
+ isFavorite = false
+ ),
+ taskerAvailable = true,
+ keyboardType = KeyboardType.Number,
+ documentation = ParamDocumentation(
+ title = "vm.enable_soft_offline",
+ documentationText = "",
+ documentationHtml = htmlDocs,
+ url = "url"
+ ),
)
- }
- } else {
- Card(
- modifier = Modifier
- .fillMaxWidth()
- .padding(24.dp),
- colors = CardDefaults.cardColors(
- containerColor = MaterialTheme.colorScheme.errorContainer
+ EditParamContent(
+ state = state,
+ showError = showError,
+ errorMessage = "Sysctl command for 'wm.swappiness' executed, " +
+ "but output did not confirm the change. Output: 'Access denied'. " +
+ "Try using '${CommitMode.ECHO}' mode.",
+ onValueApply = {},
+ onTaskerClicked = {},
+ onDocsReadMorePressed = {},
+ onFavoriteToggle = {},
+ onErrorAnimationEnd = { showError = false }
)
- ) {
- Row(
- modifier = Modifier.padding(24.dp),
- horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally)
- ) {
- Icon(
- imageVector = Icons.Outlined.Warning,
- contentDescription = stringResource(android.R.string.dialog_alert_title),
- tint = MaterialTheme.colorScheme.onErrorContainer
- )
- Text(
- text = stringResource(id = R.string.no_info_available),
- style = MaterialTheme.typography.bodyLarge.copy(
- fontWeight = FontWeight.Medium
- ),
- color = MaterialTheme.colorScheme.onErrorContainer
- )
- }
}
}
}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditParamViewEffect.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditParamViewEffect.kt
deleted file mode 100644
index 9e1c327..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditParamViewEffect.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.androidvip.sysctlgui.ui.params.edit
-
-import androidx.annotation.StringRes
-
-sealed interface EditParamViewEffect {
- class ShowApplyError(@StringRes val messageRes: Int) : EditParamViewEffect
- object ShowApplySuccess : EditParamViewEffect
- object NavigateBack : EditParamViewEffect
- object ShowTaskerListSelection : EditParamViewEffect
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditParamViewEvent.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditParamViewEvent.kt
deleted file mode 100644
index b289d50..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditParamViewEvent.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.androidvip.sysctlgui.ui.params.edit
-
-import android.content.Context
-import com.androidvip.sysctlgui.data.models.KernelParam
-
-sealed interface EditParamViewEvent {
- class ReceivedParam(val param: KernelParam, val context: Context) : EditParamViewEvent
- object BackPressed : EditParamViewEvent
- class FavoritePressed(val favorite: Boolean) : EditParamViewEvent
- object TaskerPressed : EditParamViewEvent
- object ApplyPressed : EditParamViewEvent
- object ResetPressed : EditParamViewEvent
- class TaskerListSelected(val listId: Int) : EditParamViewEvent
- class ParamValueInputChanged(val newValue: String) : EditParamViewEvent
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditParamViewModel.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditParamViewModel.kt
index 44e5713..d8cb6ea 100644
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditParamViewModel.kt
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditParamViewModel.kt
@@ -1,202 +1,141 @@
package com.androidvip.sysctlgui.ui.params.edit
-import android.annotation.SuppressLint
-import android.content.Context
-import android.content.pm.PackageManager
-import android.os.Build
+import android.util.Log
import androidx.compose.ui.text.input.KeyboardType
+import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
-import com.androidvip.sysctlgui.R
-import com.androidvip.sysctlgui.data.models.KernelParam
-import com.androidvip.sysctlgui.domain.exceptions.ApplyValueException
-import com.androidvip.sysctlgui.domain.exceptions.CommitModeException
import com.androidvip.sysctlgui.domain.repository.AppPrefs
-import com.androidvip.sysctlgui.domain.usecase.ApplyParamsUseCase
-import com.androidvip.sysctlgui.domain.usecase.UpdateUserParamUseCase
-import com.androidvip.sysctlgui.readLines
+import com.androidvip.sysctlgui.domain.usecase.ApplyParamUseCase
+import com.androidvip.sysctlgui.domain.usecase.GetParamDocumentationUseCase
+import com.androidvip.sysctlgui.domain.usecase.GetRuntimeParamUseCase
+import com.androidvip.sysctlgui.domain.usecase.GetUserParamByNameUseCase
+import com.androidvip.sysctlgui.domain.usecase.IsTaskerInstalledUseCase
+import com.androidvip.sysctlgui.domain.usecase.UpsertUserParamUseCase
+import com.androidvip.sysctlgui.helpers.UiKernelParamMapper
+import com.androidvip.sysctlgui.models.UiKernelParam
import com.androidvip.sysctlgui.utils.BaseViewModel
-import java.io.InputStream
import kotlinx.coroutines.launch
class EditParamViewModel(
- private val prefs: AppPrefs,
- private val applyParams: ApplyParamsUseCase,
- private val updateUserParam: UpdateUserParamUseCase
+ savedStateHandle: SavedStateHandle,
+ private val applyParam: ApplyParamUseCase,
+ private val getDocumentation: GetParamDocumentationUseCase,
+ private val upsertUserParam: UpsertUserParamUseCase,
+ private val getRuntimeParam: GetRuntimeParamUseCase,
+ private val getUserParam: GetUserParamByNameUseCase,
+ private val isTaskerInstalled: IsTaskerInstalledUseCase,
+ private val appPrefs: AppPrefs
) : BaseViewModel() {
- override fun createInitialState(): EditParamViewState = EditParamViewState()
+ private val paramName: String? = savedStateHandle.get(PARAM_NAME_KEY)
+ private var previousKernelParamValue: String? = null
- override fun onEvent(event: EditParamViewEvent) {
- when (event) {
- EditParamViewEvent.ApplyPressed -> {
- applyParam(currentState.param.copy(value = currentState.typedValue))
- }
- EditParamViewEvent.BackPressed -> {
- setEffect { EditParamViewEffect.NavigateBack }
- }
- is EditParamViewEvent.FavoritePressed -> {
- updateParam(currentState.param.copy(favorite = !currentState.param.favorite))
- }
- is EditParamViewEvent.TaskerListSelected -> {
- updateParam(currentState.param.copy(taskerList = event.listId, taskerParam = true))
- }
- is EditParamViewEvent.ParamValueInputChanged -> {
- setState { copy(typedValue = event.newValue) }
- }
- is EditParamViewEvent.ReceivedParam -> {
- setInitialState(event.param, event.context)
- }
- EditParamViewEvent.ResetPressed -> {
- applyParam(currentState.param.copy(value = currentState.restoreValue))
- }
- EditParamViewEvent.TaskerPressed -> {
- setEffect { EditParamViewEffect.ShowTaskerListSelection }
+ init {
+ viewModelScope.launch {
+ if (paramName.isNullOrEmpty()) return@launch setEffect { EditParamViewEffect.GoBack }
+
+ val param = runCatching { getUserParam(paramName) }.getOrNull()
+ ?: getRuntimeParam(paramName)
+ ?: return@launch setEffect { EditParamViewEffect.GoBack }
+
+ val documentation = getDocumentation(param)
+
+ setState {
+ copy(
+ kernelParam = UiKernelParamMapper.map(param),
+ taskerAvailable = isTaskerInstalled(),
+ keyboardType = guessKeyboardType(param.value),
+ documentation = documentation,
+ )
}
}
}
- private fun setInitialState(param: KernelParam, context: Context) {
- val keyboardType = getKeyboardTypeForValue(param.value)
- val singleLine = keyboardType != KeyboardType.Text ||
- param.value.length <= PARAM_LENGTH_INPUT_THRESHOLD
+ override fun createInitialState() = EditParamViewState()
- setState {
- copy(
- param = param,
- restoreValue = param.value,
- typedValue = param.value,
- paramInfo = findParamInfo(param, context),
- taskerAvailable = isTaskerInstalled(context),
- keyboardType = keyboardType,
- singleLine = singleLine
- )
+ override fun onEvent(event: EditParamViewEvent) {
+ when (event) {
+ is EditParamViewEvent.ApplyPressed -> applyKernelParam(event.newValue)
+ is EditParamViewEvent.UndoRequested -> {
+ previousKernelParamValue?.let { applyKernelParam(it) }
+ }
+ is EditParamViewEvent.DocumentationReadMoreClicked -> onDocumentationReadMoreClicked()
+ is EditParamViewEvent.FavoriteTogglePressed -> onFavoriteTogglePressed(event.newState)
+ is EditParamViewEvent.TaskerTogglePressed -> onTaskerTogglePressed(event.newState, event.listId)
}
}
- private fun applyParam(param: KernelParam) {
+ private fun applyKernelParam(newValue: String) {
+ val oldParam = currentState.kernelParam
viewModelScope.launch {
+ val newParam = oldParam.copy(value = newValue)
runCatching {
- applyParams(param)
- updateUserParam(param)
- }.onFailure {
- val messageRes = when (it) {
- is ApplyValueException -> R.string.apply_value_error
- is CommitModeException -> R.string.commit_value_error
- else -> R.string.error
- }
- setEffect { EditParamViewEffect.ShowApplyError(messageRes) }
+ applyParam(newParam)
+ upsertUserParam(newParam)
}.onSuccess {
- setEffect { EditParamViewEffect.ShowApplySuccess }
- setState {
- copy(param = param, hasApplied = param.value != currentState.restoreValue)
+ setState { copy(kernelParam = newParam) }
+ setEffect { EditParamViewEffect.ShowApplySuccess(oldParam.value) }
+ previousKernelParamValue = oldParam.value
+ }.onFailure {
+ Log.e("EditParamViewModel", "Failed to apply param", it)
+ setEffect {
+ EditParamViewEffect.ShowError(it.message.orEmpty())
}
}
}
}
- private fun updateParam(param: KernelParam) {
+ private fun onFavoriteTogglePressed(newState: Boolean) {
viewModelScope.launch {
+ val newParam = currentState.kernelParam.copy(isFavorite = newState)
runCatching {
- updateUserParam(param)
- }.onFailure {
- setEffect { EditParamViewEffect.ShowApplyError(R.string.error) }
+ upsertUserParam(newParam)
}.onSuccess {
- setState { copy(param = param) }
+ setState { copy(kernelParam = newParam) }
+ }.onFailure {
+ Log.e("EditParamViewModel", "Failed to update favorite status", it)
+ setEffect {
+ EditParamViewEffect.ShowError("Failed to update favorite status")
+ }
}
}
}
- private fun getKeyboardTypeForValue(paramValue: String): KeyboardType {
- if (!prefs.guessInputType) return KeyboardType.Text
-
- val intValue = paramValue.toIntOrNull()
- if (intValue != null) return KeyboardType.Number
-
- val decimalValue = paramValue.toDoubleOrNull()
- if (decimalValue != null) return KeyboardType.Decimal
-
- return KeyboardType.Text
- }
-
- @SuppressLint("DiscouragedApi")
- private fun findParamInfo(param: KernelParam, context: Context): String? = with(context) {
- val paramName = param.shortName
- val resId = resources.getIdentifier(
- paramName.replace("-", "_"),
- "string",
- packageName
- )
- val stringRes = runCatching { getString(resId) }.getOrNull()
-
- // Prefer the documented string resource
- if (stringRes != null) return stringRes
-
- if (!param.path.startsWith("/")) return null
-
- val subdirs = param.path.split("/")
- if (subdirs.isEmpty() || subdirs.size < SUBDIR_THRESHOLD) return null
-
- // Finding param info within subdir whole documentation string
-
- val rawInputStream: InputStream? = when (subdirs[3]) { // /proc/sys/[?]
- "abi" -> resources.openRawResource(R.raw.abi)
- "fs" -> resources.openRawResource(R.raw.fs)
- "kernel" -> resources.openRawResource(R.raw.kernel)
- "net" -> resources.openRawResource(R.raw.net)
- "vm" -> resources.openRawResource(R.raw.vm)
- else -> null
- }
-
- val documentation = buildString {
- rawInputStream.readLines {
- append(it)
- append("\n")
+ private fun onTaskerTogglePressed(newState: Boolean, listId: Int) {
+ viewModelScope.launch {
+ val newParam = currentState.kernelParam.copy(
+ isTaskerParam = newState,
+ taskerList = listId
+ )
+ runCatching {
+ upsertUserParam(newParam)
+ }.onSuccess {
+ setState { copy(kernelParam = newParam) }
+ }.onFailure {
+ Log.e("EditParamViewModel", "Failed to update tasker status", it)
+ setEffect {
+ EditParamViewEffect.ShowError("Failed to update tasker status")
+ }
}
}
- if (documentation.isEmpty()) return null
-
- /*
- Trying to match:
-
- ===============
-
- paramName
-
- the <==
- actual <==
- documentation <==
-
- ===============
- */
- val info: String? = runCatching {
- documentation
- .split("=+".toRegex())
- .last { it.contains("$paramName\n") }
- .split("$paramName\n")
- .last()
- }.getOrNull()
+ }
- return info.takeIf { it.isNullOrEmpty().not() }
+ private fun onDocumentationReadMoreClicked() {
+ currentState.documentation?.url?.let { documentationUrl ->
+ setEffect { EditParamViewEffect.OpenBrowser(documentationUrl) }
+ }
}
- private fun isTaskerInstalled(context: Context): Boolean {
- val packageManager = context.packageManager
+ private fun guessKeyboardType(paramValue: String): KeyboardType {
+ if (!appPrefs.guessInputType) return KeyboardType.Text
- return runCatching {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- packageManager.getPackageInfo(
- TASKER_PACKAGE_NAME,
- PackageManager.PackageInfoFlags.of(0L)
- )
- } else {
- packageManager.getPackageInfo(TASKER_PACKAGE_NAME, 0)
- }
- true
- }.getOrDefault(false)
+ return when {
+ paramValue.toIntOrNull() != null -> KeyboardType.Number
+ paramValue.toDoubleOrNull() != null -> KeyboardType.Decimal
+ else -> KeyboardType.Text
+ }
}
companion object {
- private const val PARAM_LENGTH_INPUT_THRESHOLD = 12
- private const val SUBDIR_THRESHOLD = 4
- private const val TASKER_PACKAGE_NAME = "net.dinglisch.android.taskerm"
+ private const val PARAM_NAME_KEY = "paramName"
}
}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditParamViewState.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditParamViewState.kt
index 7f7105c..549e388 100644
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditParamViewState.kt
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditParamViewState.kt
@@ -1,15 +1,27 @@
package com.androidvip.sysctlgui.ui.params.edit
import androidx.compose.ui.text.input.KeyboardType
-import com.androidvip.sysctlgui.data.models.KernelParam
+import com.androidvip.sysctlgui.domain.models.ParamDocumentation
+import com.androidvip.sysctlgui.models.UiKernelParam
data class EditParamViewState(
- val param: KernelParam = KernelParam(),
- val restoreValue: String = "", // Backup,
- val typedValue: String = "",
- val hasApplied: Boolean = false,
- val paramInfo: String? = null,
+ val kernelParam: UiKernelParam = UiKernelParam(),
val taskerAvailable: Boolean = false,
val keyboardType: KeyboardType = KeyboardType.Text,
- val singleLine: Boolean = true
+ val documentation: ParamDocumentation? = null,
)
+
+sealed interface EditParamViewEffect {
+ data class OpenBrowser(val url: String) : EditParamViewEffect
+ data class ShowApplySuccess(val previousValue: String) : EditParamViewEffect
+ data class ShowError(val message: String) : EditParamViewEffect
+ data object GoBack : EditParamViewEffect
+}
+
+sealed interface EditParamViewEvent {
+ data class ApplyPressed(val newValue: String) : EditParamViewEvent
+ data object UndoRequested : EditParamViewEvent
+ data class FavoriteTogglePressed(val newState: Boolean) : EditParamViewEvent
+ data class TaskerTogglePressed(val newState: Boolean, val listId: Int) : EditParamViewEvent
+ data object DocumentationReadMoreClicked : EditParamViewEvent
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/list/KernelParamListFragment.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/list/KernelParamListFragment.kt
deleted file mode 100644
index 62fe69d..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/list/KernelParamListFragment.kt
+++ /dev/null
@@ -1,135 +0,0 @@
-package com.androidvip.sysctlgui.ui.params.list
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.Menu
-import android.view.MenuInflater
-import android.view.MenuItem
-import android.view.View
-import android.view.ViewGroup
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.itemsIndexed
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.pullrefresh.PullRefreshIndicator
-import androidx.compose.material.pullrefresh.pullRefresh
-import androidx.compose.material.pullrefresh.rememberPullRefreshState
-import androidx.compose.material3.Divider
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.unit.dp
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.navigation.fragment.findNavController
-import com.androidvip.sysctlgui.R
-import com.androidvip.sysctlgui.data.models.KernelParam
-import com.androidvip.sysctlgui.ui.base.BaseSearchFragment
-import com.androidvip.sysctlgui.ui.params.EmptyParamsWarning
-import com.androidvip.sysctlgui.ui.params.edit.EditKernelParamActivity
-import com.androidvip.sysctlgui.utils.ComposeTheme
-import kotlinx.coroutines.launch
-import org.koin.androidx.viewmodel.ext.android.viewModel
-
-class KernelParamListFragment : BaseSearchFragment() {
- private val viewModel: ListParamsViewModel by viewModel()
-
- @OptIn(ExperimentalMaterialApi::class)
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- return ComposeView(requireContext()).apply {
- setContent {
- ComposeTheme {
- val state by viewModel.uiState.collectAsStateWithLifecycle()
- val refreshing = state.isLoading
- val refreshState = rememberPullRefreshState(
- refreshing = refreshing,
- onRefresh = { refreshList() }
- )
-
- Box(Modifier.pullRefresh(refreshState)) {
- if (state.showEmptyState) {
- EmptyParamsWarning()
- } else {
- KernelParamsList(state.data)
- }
-
- PullRefreshIndicator(
- modifier = Modifier.align(Alignment.TopCenter),
- refreshing = refreshing,
- state = refreshState,
- backgroundColor = MaterialTheme.colorScheme.tertiaryContainer,
- contentColor = MaterialTheme.colorScheme.onTertiaryContainer
- )
- }
- }
- }
- }
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- lifecycleScope.launch {
- viewModel.effect.collect(::processEffect)
- }
- }
-
- override fun onStart() {
- super.onStart()
- refreshList()
- }
-
- override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
- inflater.inflate(R.menu.menu_main_search, menu)
- setUpSearchView(menu)
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- when (item.itemId) {
- R.id.action_favorites -> findNavController().navigate(R.id.navigateFavoritesParams)
- else -> return false
- }
-
- return true
- }
-
- override fun onQueryTextChanged() {
- viewModel.onEvent(ParamViewEvent.SearchExpressionChanged(searchExpression))
- }
-
- private fun onParamItemClicked(param: KernelParam) {
- startActivity(EditKernelParamActivity.getIntent(requireContext(), param))
- }
-
- private fun refreshList() {
- viewModel.onEvent(ParamViewEvent.RefreshRequested)
- }
-
- private fun processEffect(effect: ParamViewEffect) {
- when (effect) {
- is ParamViewEffect.NavigateToParamDetails -> onParamItemClicked(effect.param)
- }
- }
-
- @Composable
- private fun KernelParamsList(params: List) {
- LazyColumn {
- itemsIndexed(params) { index, param ->
- ParamItem(
- onParamClick = { viewModel.onEvent(ParamViewEvent.ParamClicked(param)) },
- param = param
- )
- if (index < params.lastIndex) {
- Divider(color = MaterialTheme.colorScheme.outlineVariant, thickness = 1.dp)
- }
- }
- }
- }
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/list/ListParamsViewModel.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/list/ListParamsViewModel.kt
deleted file mode 100644
index 59273ff..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/list/ListParamsViewModel.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.androidvip.sysctlgui.ui.params.list
-
-import androidx.lifecycle.viewModelScope
-import com.androidvip.sysctlgui.data.mapper.DomainParamMapper
-import com.androidvip.sysctlgui.domain.usecase.GetRuntimeParamsUseCase
-import com.androidvip.sysctlgui.utils.BaseViewModel
-import kotlinx.coroutines.launch
-
-class ListParamsViewModel(
- private val getParamsUseCase: GetRuntimeParamsUseCase
-) : BaseViewModel() {
- private var searchExpression = ""
-
- private fun requestKernelParams() {
- viewModelScope.launch {
- setState { copy(isLoading = true) }
- val params = getParamsUseCase()
- .map(DomainParamMapper::map)
- .filter { param ->
- if (searchExpression.isNotEmpty()) {
- param.name.lowercase()
- .replace(".", "")
- .contains(searchExpression.lowercase())
- } else {
- true
- }
- }
- setState { copy(isLoading = false, data = params, showEmptyState = params.isEmpty()) }
- }
- }
-
- override fun createInitialState(): ParamViewState = ParamViewState()
-
- override fun onEvent(event: ParamViewEvent) {
- when (event) {
- is ParamViewEvent.ParamClicked -> setEffect {
- ParamViewEffect.NavigateToParamDetails(DomainParamMapper.map(event.param))
- }
- is ParamViewEvent.SearchExpressionChanged -> {
- searchExpression = event.data
- requestKernelParams()
- }
- ParamViewEvent.RefreshRequested -> requestKernelParams()
- else -> Unit
- }
- }
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/list/ParamItem.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/list/ParamItem.kt
deleted file mode 100644
index 340b65f..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/list/ParamItem.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-package com.androidvip.sysctlgui.ui.params.list
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import com.androidvip.sysctlgui.data.models.KernelParam
-import com.androidvip.sysctlgui.design.theme.md_theme_light_background
-
-@Composable
-fun ParamItem(onParamClick: (KernelParam) -> Unit, param: KernelParam) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .clickable { onParamClick(param) }
- ) {
- Text(
- modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp),
- text = param.shortName,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- style = MaterialTheme.typography.bodyLarge,
- color = MaterialTheme.colorScheme.secondary
- )
- Spacer(modifier = Modifier.height(2.dp))
- Text(
- modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
- text = param.value,
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onBackground
- )
- }
-}
-
-@Preview
-@Composable
-fun ParamItemPreview() {
- val param = KernelParam(name = "test", value = "success")
- Box(modifier = Modifier.background(md_theme_light_background)) {
- ParamItem(onParamClick = {}, param = param)
- }
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/list/ParamViewEffect.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/list/ParamViewEffect.kt
deleted file mode 100644
index 51a92db..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/list/ParamViewEffect.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.androidvip.sysctlgui.ui.params.list
-
-import com.androidvip.sysctlgui.data.models.KernelParam
-
-sealed interface ParamViewEffect {
- class NavigateToParamDetails(val param: KernelParam) : ParamViewEffect
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/list/ParamViewEvent.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/list/ParamViewEvent.kt
deleted file mode 100644
index f60b160..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/list/ParamViewEvent.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.androidvip.sysctlgui.ui.params.list
-
-import com.androidvip.sysctlgui.domain.models.DomainKernelParam
-
-sealed interface ParamViewEvent {
- object RefreshRequested : ParamViewEvent
- class SearchExpressionChanged(val data: String) : ParamViewEvent
- class ParamClicked(val param: DomainKernelParam) : ParamViewEvent
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/list/ParamViewState.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/list/ParamViewState.kt
deleted file mode 100644
index 865215c..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/list/ParamViewState.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.androidvip.sysctlgui.ui.params.list
-
-import com.androidvip.sysctlgui.data.models.KernelParam
-
-data class ParamViewState(
- var data: List = listOf(),
- var isLoading: Boolean = true,
- var showEmptyState: Boolean = false
-)
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/BaseManageParamsActivity.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/BaseManageParamsActivity.kt
deleted file mode 100644
index 4f23947..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/BaseManageParamsActivity.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.androidvip.sysctlgui.ui.params.user
-
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.compose.runtime.getValue
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.androidvip.sysctlgui.data.models.KernelParam
-import com.androidvip.sysctlgui.ui.params.edit.EditKernelParamActivity
-import com.androidvip.sysctlgui.utils.ComposeTheme
-import org.koin.androidx.viewmodel.ext.android.viewModel
-
-abstract class BaseManageParamsActivity : ComponentActivity() {
- private val viewModel: UserParamsViewModel by viewModel()
- abstract val filterPredicate: (KernelParam) -> Boolean
- abstract val title: String
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- setContent {
- ComposeTheme {
- val state by viewModel.uiState.collectAsStateWithLifecycle()
- UserParamsScreen(
- topBarTitle = title,
- params = state.params,
- searchViewVisible = state.searchViewVisible,
- onQueryChanged = {
- viewModel.onEvent(UserParamsViewEvent.SearchQueryChanged(it))
- },
- onSearch = { viewModel.onEvent(UserParamsViewEvent.SearchPressed) },
- onSearchPressed = { viewModel.onEvent(UserParamsViewEvent.SearchViewPressed) },
- onSearchClose = { viewModel.onEvent(UserParamsViewEvent.CloseSearchPressed) },
- onParamClicked = { startActivity(EditKernelParamActivity.getIntent(this, it)) },
- onDelete = { viewModel.onEvent(UserParamsViewEvent.DeleteSwipe(it)) },
- onBackPressed = { onBackPressedDispatcher.onBackPressed() }
- )
- }
- }
- }
-
- override fun onStart() {
- super.onStart()
- viewModel.setBaseFilterPredicate(filterPredicate)
- viewModel.onEvent(UserParamsViewEvent.ParamsRequested)
- }
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/ManageFavoritesParamsActivity.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/ManageFavoritesParamsActivity.kt
deleted file mode 100644
index 8e2b1be..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/ManageFavoritesParamsActivity.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.androidvip.sysctlgui.ui.params.user
-
-import com.androidvip.sysctlgui.R
-import com.androidvip.sysctlgui.data.models.KernelParam
-
-class ManageFavoritesParamsActivity : BaseManageParamsActivity() {
- override val title: String
- get() = getString(R.string.tasker_list_plugin_favorites)
-
- override val filterPredicate: (KernelParam) -> Boolean
- get() = { it.favorite }
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/ManageOnStartUpParamsActivity.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/ManageOnStartUpParamsActivity.kt
deleted file mode 100644
index 9b6a726..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/ManageOnStartUpParamsActivity.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.androidvip.sysctlgui.ui.params.user
-
-import com.androidvip.sysctlgui.R
-import com.androidvip.sysctlgui.data.models.KernelParam
-
-class ManageOnStartUpParamsActivity : BaseManageParamsActivity() {
- override val title: String
- get() = getString(R.string.manage_parameters)
-
- override val filterPredicate: (KernelParam) -> Boolean
- get() = { true }
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/UserParamsScreen.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/UserParamsScreen.kt
deleted file mode 100644
index 5f5abf0..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/UserParamsScreen.kt
+++ /dev/null
@@ -1,242 +0,0 @@
-package com.androidvip.sysctlgui.ui.params.user
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.outlined.ArrowBack
-import androidx.compose.material.icons.outlined.Close
-import androidx.compose.material.icons.outlined.Search
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.HorizontalDivider
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.SearchBar
-import androidx.compose.material3.SwipeToDismissBox
-import androidx.compose.material3.SwipeToDismissBoxValue
-import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
-import androidx.compose.material3.rememberSwipeToDismissBoxState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import com.androidvip.sysctlgui.R
-import com.androidvip.sysctlgui.data.models.KernelParam
-import com.androidvip.sysctlgui.design.theme.md_theme_light_background
-import com.androidvip.sysctlgui.ui.params.EmptyParamsWarning
-import com.androidvip.sysctlgui.ui.params.list.ParamItem
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun UserParamsScreen(
- topBarTitle: String,
- params: List,
- searchViewVisible: Boolean,
- onQueryChanged: (String) -> Unit,
- onSearch: (String) -> Unit,
- onSearchPressed: () -> Unit,
- onSearchClose: () -> Unit,
- onDelete: (KernelParam) -> Unit,
- onParamClicked: (KernelParam) -> Unit,
- onBackPressed: () -> Unit
-) {
- val listState = rememberLazyListState()
-
- Scaffold(
- topBar = {
- if (searchViewVisible) {
- ParamSearch(
- onSearch = onSearch,
- onClose = onSearchClose,
- onQueryChanged = onQueryChanged
- )
- } else {
- TopAppBar(
- title = { Text(text = topBarTitle) },
- navigationIcon = {
- IconButton(onClick = onBackPressed) {
- Icon(
- imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
- contentDescription = stringResource(id = R.string.restore_param),
- tint = MaterialTheme.colorScheme.onPrimaryContainer
- )
- }
- },
- actions = {
- IconButton(onClick = onSearchPressed) {
- Icon(
- imageVector = Icons.Outlined.Search,
- contentDescription = stringResource(id = R.string.search),
- tint = MaterialTheme.colorScheme.onSurface
- )
- }
- }
- )
- }
- }
- ) { contentPadding ->
- if (params.isEmpty()) {
- Box(modifier = Modifier.padding(top = 64.dp)) { EmptyParamsWarning() }
- } else {
- LazyColumn(
- modifier = Modifier
- .fillMaxSize()
- .padding(contentPadding),
- state = listState
- ) {
- items(
- items = params,
- key = { param -> param.id },
- itemContent = { param ->
- SwipeToDismissContent(
- onParamClick = onParamClicked,
- onDelete = onDelete,
- param = param
- )
- }
- )
- }
- }
- }
-}
-
-@Composable
-@OptIn(ExperimentalMaterial3Api::class)
-private fun SwipeToDismissContent(
- onParamClick: (KernelParam) -> Unit,
- onDelete: (KernelParam) -> Unit,
- param: KernelParam
-) {
- val currentParam by rememberUpdatedState(newValue = param)
- val dismissState = rememberSwipeToDismissBoxState(
- confirmValueChange = {
- fun getResultFromValueChange(): Boolean {
- if (it == SwipeToDismissBoxValue.EndToStart) {
- onDelete(currentParam)
- return true
- }
- return false
- }
- getResultFromValueChange()
- }
- )
-
- SwipeToDismissBox(
- state = dismissState,
- enableDismissFromEndToStart = true,
- backgroundContent = {
- Box(
- modifier = Modifier
- .fillMaxSize()
- .background(MaterialTheme.colorScheme.error),
- contentAlignment = Alignment.CenterEnd
- ) {
- Icon(
- modifier = Modifier.padding(end = 16.dp),
- painter = painterResource(id = R.drawable.ic_delete_sweep),
- contentDescription = "",
- tint = MaterialTheme.colorScheme.onError
- )
- }
- }
- ) {
- Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
- ParamItem(
- onParamClick = onParamClick,
- param = param
- )
- HorizontalDivider(
- thickness = 1.dp,
- color = MaterialTheme.colorScheme.outlineVariant
- )
- }
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun ParamSearch(onSearch: (String) -> Unit, onClose: () -> Unit, onQueryChanged: (String) -> Unit) {
- var searchText by remember { mutableStateOf("") }
-
- SearchBar(
- modifier = Modifier.fillMaxWidth(),
- query = searchText,
- onQueryChange = { searchText = it; onQueryChanged(it) },
- onSearch = onSearch,
- active = false,
- shape = RoundedCornerShape(0.dp),
- onActiveChange = { },
- leadingIcon = {
- Icon(
- imageVector = Icons.Outlined.Search,
- contentDescription = stringResource(id = R.string.search),
- tint = MaterialTheme.colorScheme.onPrimaryContainer
- )
- },
- trailingIcon = {
- IconButton(onClick = onClose) {
- Icon(
- imageVector = Icons.Outlined.Close,
- contentDescription = stringResource(id = android.R.string.cancel),
- tint = MaterialTheme.colorScheme.onPrimaryContainer
- )
- }
- },
- placeholder = {
- Text(
- text = stringResource(id = R.string.search),
- color = MaterialTheme.colorScheme.onPrimaryContainer
- )
- }
- ) {
- }
-}
-
-@Preview
-@Composable
-private fun UserParamsScreenPreview() {
- val params = buildList {
- repeat(15) { n ->
- add(
- KernelParam(
- id = n,
- favorite = n % 3 == 0,
- name = buildString { (0..n).forEach { append((it * 4).toChar()) } },
- value = "${n * 31}"
- )
- )
- }
- }
- Box(modifier = Modifier.background(md_theme_light_background)) {
- UserParamsScreen(
- topBarTitle = "Favorites",
- params = params,
- searchViewVisible = false,
- onQueryChanged = {},
- onSearch = {},
- onSearchPressed = {},
- onParamClicked = {},
- onDelete = {},
- onSearchClose = {},
- onBackPressed = {}
- )
- }
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/UserParamsViewEvent.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/UserParamsViewEvent.kt
deleted file mode 100644
index 2d43672..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/UserParamsViewEvent.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.androidvip.sysctlgui.ui.params.user
-
-import com.androidvip.sysctlgui.data.models.KernelParam
-
-sealed interface UserParamsViewEvent {
- object ParamsRequested : UserParamsViewEvent
- object SearchViewPressed : UserParamsViewEvent
- object SearchPressed : UserParamsViewEvent
- object CloseSearchPressed : UserParamsViewEvent
- class DeleteSwipe(val param: KernelParam) : UserParamsViewEvent
- class SearchQueryChanged(val query: String) : UserParamsViewEvent
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/UserParamsViewModel.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/UserParamsViewModel.kt
deleted file mode 100644
index 8ea0e27..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/UserParamsViewModel.kt
+++ /dev/null
@@ -1,80 +0,0 @@
-package com.androidvip.sysctlgui.ui.params.user
-
-import androidx.lifecycle.viewModelScope
-import com.androidvip.sysctlgui.data.mapper.DomainParamMapper
-import com.androidvip.sysctlgui.data.models.KernelParam
-import com.androidvip.sysctlgui.domain.usecase.GetUserParamsUseCase
-import com.androidvip.sysctlgui.domain.usecase.RemoveUserParamUseCase
-import com.androidvip.sysctlgui.domain.usecase.UpdateUserParamUseCase
-import com.androidvip.sysctlgui.utils.BaseViewModel
-import kotlinx.coroutines.launch
-
-typealias ParamFilterPredicate = (KernelParam) -> Boolean
-
-class UserParamsViewModel(
- private val getParamsUseCase: GetUserParamsUseCase,
- private val removeParamUseCase: RemoveUserParamUseCase,
- private val updateParamUseCase: UpdateUserParamUseCase
-) : BaseViewModel() {
- private var baseFilterPredicate: ParamFilterPredicate = { true }
- private var currentFilterPredicate: ParamFilterPredicate = { true }
-
- override fun createInitialState(): UserParamsViewState = UserParamsViewState()
-
- override fun onEvent(event: UserParamsViewEvent) {
- when (event) {
- UserParamsViewEvent.ParamsRequested -> getParams()
- UserParamsViewEvent.SearchPressed -> getParams()
- UserParamsViewEvent.CloseSearchPressed -> {
- setState { copy(searchViewVisible = false) }
- }
- UserParamsViewEvent.SearchViewPressed -> {
- setState { copy(searchViewVisible = true) }
- }
- is UserParamsViewEvent.DeleteSwipe -> {
- if (event.param.favorite) {
- event.param.favorite = false
- update(event.param)
- } else {
- delete(event.param)
- }
- }
- is UserParamsViewEvent.SearchQueryChanged -> {
- currentFilterPredicate = {
- it.name
- .replace(".", "")
- .contains(event.query, ignoreCase = true) &&
- baseFilterPredicate(it)
- }
- }
- }
- }
- private fun getParams() {
- viewModelScope.launch {
- val params = getParamsUseCase()
- .map { DomainParamMapper.map(it) }
- .filter(currentFilterPredicate)
-
- setState { copy(params = params) }
- }
- }
-
- fun setBaseFilterPredicate(predicate: ParamFilterPredicate) {
- baseFilterPredicate = predicate
- currentFilterPredicate = baseFilterPredicate
- }
-
- private fun delete(kernelParam: KernelParam) {
- viewModelScope.launch {
- removeParamUseCase.execute(kernelParam)
- getParams()
- }
- }
-
- private fun update(kernelParam: KernelParam) {
- viewModelScope.launch {
- updateParamUseCase(kernelParam)
- getParams()
- }
- }
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/UserParamsViewState.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/UserParamsViewState.kt
deleted file mode 100644
index d3bcc4f..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/user/UserParamsViewState.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.androidvip.sysctlgui.ui.params.user
-
-import com.androidvip.sysctlgui.data.models.KernelParam
-
-data class UserParamsViewState(
- val searchViewVisible: Boolean = false,
- val params: List = emptyList(),
-)
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/presets/ImportPresetScreen.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/presets/ImportPresetScreen.kt
new file mode 100644
index 0000000..7f78ae5
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/presets/ImportPresetScreen.kt
@@ -0,0 +1,357 @@
+package com.androidvip.sysctlgui.ui.presets
+
+import android.widget.Toast
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.scaleIn
+import androidx.compose.animation.scaleOut
+import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.CheckCircle
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LinearProgressIndicator
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.tooling.preview.PreviewDynamicColors
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import com.androidvip.sysctlgui.ui.main.MainViewEvent
+import com.androidvip.sysctlgui.ui.main.MainViewModel
+import com.androidvip.sysctlgui.ui.main.MainViewState
+import org.koin.androidx.compose.koinViewModel
+
+private const val SUCCESS_ANIMATION_DURATION = 4000
+
+@OptIn(ExperimentalAnimationApi::class)
+@Composable
+fun ImportPresetScreen(
+ viewModel: PresetsViewModel = koinViewModel(),
+ mainViewModel: MainViewModel = koinViewModel(),
+ onNavigateBack: () -> Unit
+) {
+ val context = LocalContext.current
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
+
+ LaunchedEffect(Unit) {
+ mainViewModel.onEvent(
+ MainViewEvent.OnSateChangeRequested(
+ MainViewState(
+ topBarTitle = "Applying preset",
+ showTopBar = true,
+ showNavBar = false,
+ showBackButton = true,
+ showSearchAction = false
+ )
+ )
+ )
+ }
+
+ LaunchedEffect(viewModel.effect) {
+ viewModel.effect.collect { effect ->
+ when (effect) {
+ is PresetsViewEffect.ShowError -> {
+ Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()
+ }
+ is PresetsViewEffect.ShowToast -> {
+ Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()
+ }
+ PresetsViewEffect.GoBack -> onNavigateBack()
+ else -> {}
+ }
+ }
+ }
+
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ AnimatedContent(
+ targetState = state.incomingPresetsScreenState,
+ transitionSpec = {
+ val isLoadingInitialState = initialState == IncomingPresetsScreenState.Loading
+ val isSuccessTargetState = targetState == IncomingPresetsScreenState.Success
+ if (isLoadingInitialState && isSuccessTargetState) {
+ val enterTransition = fadeIn() + scaleIn(initialScale = 0.8f)
+ val exitTransition = fadeOut() + scaleOut(targetScale = 0.9f)
+ enterTransition togetherWith exitTransition
+ } else {
+ fadeIn() togetherWith fadeOut()
+ }
+ }
+ ) { targetState ->
+ when (targetState) {
+ IncomingPresetsScreenState.Idle -> {
+ IncomingPresetsContent(
+ paramsToImport = state.paramsToImport,
+ onImportPressed = {
+ viewModel.onEvent(PresetsViewEvent.ConfirmImportPressed)
+ },
+ onCancelPressed = {
+ viewModel.onEvent(PresetsViewEvent.CancelImportPressed)
+ }
+ )
+ }
+
+ IncomingPresetsScreenState.Loading -> {
+ LoadingIndicator()
+ }
+
+ IncomingPresetsScreenState.Success -> {
+ SuccessIndicator(
+ onAnimationEnd = {
+ viewModel.onEvent(PresetsViewEvent.CancelImportPressed)
+ }
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun IncomingPresetsContent(
+ paramsToImport: List,
+ onImportPressed: () -> Unit,
+ onCancelPressed: () -> Unit
+) {
+ Column(modifier = Modifier.fillMaxSize()) {
+ Text(
+ text = "${paramsToImport.size} parameters found",
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onBackground,
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(MaterialTheme.colorScheme.surfaceContainerHigh)
+ .padding(16.dp)
+ )
+
+ HorizontalDivider()
+
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ ) {
+ itemsIndexed(
+ items = paramsToImport,
+ key = { index, item -> item.name }
+ ) { index, item ->
+ Row(
+ modifier = Modifier
+ .height(IntrinsicSize.Min)
+ .fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(
+ modifier = Modifier
+ .width(36.dp)
+ .fillMaxHeight()
+ .background(MaterialTheme.colorScheme.surfaceContainerHigh)
+ ) {
+ Text(
+ text = "${index + 1}",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurface,
+ fontFamily = FontFamily.Monospace,
+ fontWeight = FontWeight.Medium,
+ textAlign = TextAlign.End,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(4.dp)
+ )
+ }
+
+ val text = buildAnnotatedString {
+ withStyle(
+ SpanStyle(
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ append(item.name)
+ }
+ append("=")
+ withStyle(
+ SpanStyle(
+ fontWeight = FontWeight.Medium,
+ color = MaterialTheme.colorScheme.tertiary
+ )
+ ) {
+ append(item.value)
+ }
+ }
+ Text(
+ text = text,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurface,
+ fontFamily = FontFamily.Monospace,
+ fontWeight = FontWeight.Medium,
+ modifier = Modifier
+ .weight(1f)
+ .padding(4.dp)
+ )
+ }
+ }
+ }
+
+ HorizontalDivider()
+
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(MaterialTheme.colorScheme.surfaceContainer)
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally)
+ ) {
+ OutlinedButton(
+ modifier = Modifier.weight(1f),
+ onClick = onCancelPressed
+ ) {
+ Text(text = "Cancel")
+ }
+
+ OutlinedButton(
+ modifier = Modifier.weight(1f),
+ onClick = onImportPressed,
+ enabled = paramsToImport.isNotEmpty(),
+ colors = ButtonDefaults.outlinedButtonColors(
+ containerColor = MaterialTheme.colorScheme.primary,
+ contentColor = MaterialTheme.colorScheme.onPrimary
+ )
+ ) {
+ Text(text = "Import")
+ }
+ }
+ }
+}
+
+@Composable
+private fun LoadingIndicator() {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(32.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically)
+ ) {
+ CircularProgressIndicator()
+ Text(
+ text = "Loading preset...",
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier.fillMaxWidth(),
+ color = MaterialTheme.colorScheme.onBackground,
+ textAlign = TextAlign.Center
+ )
+ }
+}
+
+@Composable
+private fun SuccessIndicator(onAnimationEnd: () -> Unit) {
+ var animationStarted by remember { mutableStateOf(false) }
+ val progressTarget = if (animationStarted) 0f else 1f
+
+ val progress by animateFloatAsState(
+ targetValue = progressTarget,
+ animationSpec = tween(durationMillis = SUCCESS_ANIMATION_DURATION, easing = LinearEasing),
+ finishedListener = { value ->
+ if (value == 0f) {
+ onAnimationEnd()
+ }
+ }
+ )
+
+ LaunchedEffect(Unit) { animationStarted = true }
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(32.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically)
+ ) {
+ Icon(
+ imageVector = Icons.Rounded.CheckCircle,
+ contentDescription = "Success",
+ tint = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.size(128.dp)
+ )
+
+ Text(
+ text = "Presets successfully imported",
+ style = MaterialTheme.typography.titleLarge,
+ color = MaterialTheme.colorScheme.onBackground,
+ textAlign = TextAlign.Center
+ )
+
+ LinearProgressIndicator(
+ modifier = Modifier.fillMaxWidth(),
+ color = MaterialTheme.colorScheme.primary,
+ trackColor = MaterialTheme.colorScheme.surfaceVariant,
+ progress = { progress }
+ )
+ }
+}
+
+@Composable
+@PreviewLightDark
+@PreviewDynamicColors
+private fun IncomingPresetsScreenPreview() {
+ SysctlGuiTheme(dynamicColor = true) {
+ Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
+ IncomingPresetsContent(
+ paramsToImport = buildList {
+ repeat(16) {
+ add(
+ KernelParam(
+ name = "vm.swappiness.$it",
+ value = "value$it",
+ path = ""
+ )
+ )
+ }
+ },
+ onImportPressed = {},
+ onCancelPressed = {},
+ )
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/presets/PresetsScreen.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/presets/PresetsScreen.kt
new file mode 100644
index 0000000..3b3b98a
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/presets/PresetsScreen.kt
@@ -0,0 +1,215 @@
+package com.androidvip.sysctlgui.ui.presets
+
+import android.widget.Toast
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Card
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.tooling.preview.PreviewDynamicColors
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.androidvip.sysctlgui.R
+import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
+import com.androidvip.sysctlgui.ui.components.ErrorContainer
+import com.androidvip.sysctlgui.ui.main.MainViewEvent
+import com.androidvip.sysctlgui.ui.main.MainViewModel
+import com.androidvip.sysctlgui.ui.main.MainViewState
+import org.koin.compose.viewmodel.koinViewModel
+
+@Composable
+fun PresetsScreen(
+ viewModel: PresetsViewModel = koinViewModel(),
+ mainViewModel: MainViewModel = koinViewModel(),
+ onNavigateBack: () -> Unit,
+ onNavigateToImport: () -> Unit,
+) {
+ val context = LocalContext.current
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
+ val pickFileLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.OpenDocument(),
+ onResult = { uri ->
+ viewModel.onEvent(PresetsViewEvent.PresetFilePicked(uri))
+ }
+ )
+ val createFileLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.CreateDocument("text/plain"),
+ onResult = { uri ->
+ viewModel.onEvent(PresetsViewEvent.BackUpFileCreated(uri))
+ }
+ )
+ var showError by remember { mutableStateOf(false) }
+ var errorMessage by remember { mutableStateOf("") }
+
+ LaunchedEffect(Unit) {
+ mainViewModel.onEvent(
+ MainViewEvent.OnSateChangeRequested(
+ MainViewState(
+ showTopBar = true,
+ showNavBar = true,
+ showBackButton = false,
+ showSearchAction = false
+ )
+ )
+ )
+ }
+
+ LaunchedEffect(viewModel.effect) {
+ viewModel.effect.collect { effect ->
+ when (effect) {
+ is PresetsViewEffect.ShowError -> {
+ errorMessage = effect.message
+ showError = true
+ }
+ PresetsViewEffect.ShowImportScreen -> onNavigateToImport()
+ is PresetsViewEffect.ShowToast -> {
+ Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()
+ }
+ PresetsViewEffect.GoBack -> onNavigateBack()
+ }
+ }
+ }
+
+ AnimatedVisibility(visible = state.loading) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator()
+ }
+ }
+
+ PresetsScreenContent(
+ onImportPressed = { pickFileLauncher.launch(arrayOf("*/*")) },
+ onExportPressed = {
+ val defaultFileName = "backup.conf"
+ createFileLauncher.launch(defaultFileName)
+ },
+ onErrorAnimationEnd = { showError = false },
+ showError = showError,
+ errorMessage = errorMessage
+ )
+}
+
+@Composable
+private fun PresetsScreenContent(
+ onImportPressed: () -> Unit,
+ onExportPressed: () -> Unit,
+ onErrorAnimationEnd: () -> Unit,
+ showError: Boolean,
+ errorMessage: String
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ ImportCards(
+ onClick = onImportPressed,
+ title = "Import",
+ description = "Import presets from a file",
+ iconRes = R.drawable.ic_import
+ )
+ ImportCards(
+ onClick = onExportPressed,
+ title = "Export",
+ description = "Export presets to a file",
+ iconRes = R.drawable.ic_export
+ )
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ AnimatedVisibility(
+ visible = showError && errorMessage.isNotEmpty(),
+ enter = slideInVertically { it / 2 } + fadeIn(),
+ exit = slideOutVertically{ it / 2 } + fadeOut(),
+ modifier = Modifier.padding(bottom = 16.dp)
+ ) {
+ ErrorContainer(message = errorMessage, onAnimationEnd = onErrorAnimationEnd)
+ }
+ }
+}
+
+@Composable
+private fun ImportCards(onClick: () -> Unit, title: String, description: String, iconRes: Int) {
+ Card(onClick = onClick, modifier = Modifier.fillMaxWidth()) {
+ Row(
+ modifier = Modifier
+ .padding(16.dp)
+ .semantics(mergeDescendants = true) {
+ contentDescription = "$title - $description"
+ },
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Icon(
+ modifier = Modifier.size(40.dp),
+ painter = painterResource(id = iconRes),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary
+ )
+ Column {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ Text(
+ text = description,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
+ }
+ }
+}
+
+
+
+@Composable
+@PreviewLightDark
+@PreviewDynamicColors
+private fun PresetsScreenPreview() {
+ SysctlGuiTheme(dynamicColor = true) {
+ Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
+ PresetsScreenContent(
+ onImportPressed = {},
+ onExportPressed = {},
+ onErrorAnimationEnd = {},
+ showError = true,
+ errorMessage = "Error message"
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/presets/PresetsViewModel.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/presets/PresetsViewModel.kt
new file mode 100644
index 0000000..2ef1f1f
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/presets/PresetsViewModel.kt
@@ -0,0 +1,109 @@
+package com.androidvip.sysctlgui.ui.presets
+
+import android.net.Uri
+import android.util.Log
+import androidx.lifecycle.viewModelScope
+import com.androidvip.sysctlgui.R
+import com.androidvip.sysctlgui.data.utils.PresetsFileProcessor
+import com.androidvip.sysctlgui.domain.StringProvider
+import com.androidvip.sysctlgui.domain.exceptions.EmptyFileException
+import com.androidvip.sysctlgui.domain.exceptions.MalformedLineException
+import com.androidvip.sysctlgui.domain.exceptions.NoValidParamException
+import com.androidvip.sysctlgui.domain.usecase.AddUserParamsUseCase
+import com.androidvip.sysctlgui.domain.usecase.GetUserParamsUseCase
+import com.androidvip.sysctlgui.utils.BaseViewModel
+import kotlinx.coroutines.launch
+import java.io.IOException
+
+class PresetsViewModel(
+ private val getUserParams: GetUserParamsUseCase,
+ private val addUserParams: AddUserParamsUseCase,
+ private val presetsFileProcessor: PresetsFileProcessor,
+ private val stringProvider: StringProvider
+) : BaseViewModel() {
+ override fun createInitialState() = PresetsViewState()
+
+ override fun onEvent(event: PresetsViewEvent) {
+ when (event) {
+ PresetsViewEvent.CancelImportPressed -> setEffect { PresetsViewEffect.GoBack }
+ PresetsViewEvent.ConfirmImportPressed -> confirmImport()
+ is PresetsViewEvent.PresetFilePicked -> handleImportPreset(event.uri)
+ is PresetsViewEvent.BackUpFileCreated -> handleBackup(event.uri)
+ }
+ }
+
+ private fun confirmImport() {
+ viewModelScope.launch {
+ setState { copy(incomingPresetsScreenState = IncomingPresetsScreenState.Loading) }
+ val paramsToImport = uiState.value.paramsToImport
+ runCatching {
+ addUserParams(paramsToImport)
+ }.onSuccess {
+ setState {
+ copy(
+ paramsToImport = emptyList(),
+ incomingPresetsScreenState = IncomingPresetsScreenState.Success
+ )
+ }
+ }.onFailure {
+ setState { copy(incomingPresetsScreenState = IncomingPresetsScreenState.Idle) }
+ setEffect { PresetsViewEffect.ShowError("Failed to import params") }
+ }
+ }
+ }
+
+ private fun handleImportPreset(uri: Uri?) {
+ if (uri == null) {
+ setEffect { PresetsViewEffect.ShowError(stringProvider.getString(R.string.preset_error_file_picking)) }
+ return
+ }
+
+ viewModelScope.launch {
+ try {
+ val params = presetsFileProcessor.getKernelParamsFromUri(uri)
+ setState {
+ copy(
+ paramsToImport = params,
+ incomingPresetsScreenState = IncomingPresetsScreenState.Idle
+ )
+ }
+ setEffect { PresetsViewEffect.ShowImportScreen }
+ } catch (_: EmptyFileException) {
+ setEffect { PresetsViewEffect.ShowError(stringProvider.getString(R.string.import_error_empty_file)) }
+ } catch (_: MalformedLineException) {
+ setEffect { PresetsViewEffect.ShowError(stringProvider.getString(R.string.import_error_malformed_line)) }
+ } catch (_: NoValidParamException) {
+ setEffect { PresetsViewEffect.ShowError(stringProvider.getString(R.string.export_error_no_param)) }
+ } catch (_: IOException) {
+ setEffect { PresetsViewEffect.ShowError(stringProvider.getString(R.string.export_error_io)) }
+ } catch (e: Exception) {
+ Log.e("PresetsViewModel", "Error importing file", e)
+ setEffect { PresetsViewEffect.ShowError(stringProvider.getString(R.string.import_error)) }
+ }
+ }
+ }
+
+ private fun handleBackup(uri: Uri?) {
+ if (uri == null) {
+ setEffect { PresetsViewEffect.ShowError(stringProvider.getString(R.string.preset_error_file_creation)) }
+ return
+ }
+
+ viewModelScope.launch {
+ try {
+ val userParams = getUserParams()
+
+ runCatching {
+ presetsFileProcessor.backupParamsToUri(uri, userParams)
+ }.onSuccess {
+ setEffect { PresetsViewEffect.ShowToast("Export complete") }
+ }.onFailure {
+ setEffect { PresetsViewEffect.ShowError(stringProvider.getString(R.string.preset_error_processing_file)) }
+ }
+ } catch (e: Exception) {
+ Log.e("PresetsViewModel", "Error saving file", e)
+ setEffect { PresetsViewEffect.ShowError(stringProvider.getString(R.string.preset_error_opening_file)) }
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/presets/PresetsViewState.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/presets/PresetsViewState.kt
new file mode 100644
index 0000000..80f4a7a
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/presets/PresetsViewState.kt
@@ -0,0 +1,30 @@
+package com.androidvip.sysctlgui.ui.presets
+
+import android.net.Uri
+import com.androidvip.sysctlgui.domain.models.KernelParam
+
+data class PresetsViewState(
+ val paramsToImport: List = emptyList(),
+ val loading: Boolean = false,
+ val incomingPresetsScreenState: IncomingPresetsScreenState = IncomingPresetsScreenState.Idle
+)
+
+enum class IncomingPresetsScreenState {
+ Idle,
+ Loading,
+ Success
+}
+
+sealed interface PresetsViewEvent {
+ data class PresetFilePicked(val uri: Uri?) : PresetsViewEvent
+ data class BackUpFileCreated(val uri: Uri?) : PresetsViewEvent
+ data object ConfirmImportPressed : PresetsViewEvent
+ data object CancelImportPressed : PresetsViewEvent
+}
+
+sealed interface PresetsViewEffect {
+ data class ShowError(val message: String) : PresetsViewEffect
+ data class ShowToast(val message: String) : PresetsViewEffect
+ data object ShowImportScreen : PresetsViewEffect
+ data object GoBack : PresetsViewEffect
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/search/SearchScreen.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/search/SearchScreen.kt
new file mode 100644
index 0000000..b6ab33a
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/search/SearchScreen.kt
@@ -0,0 +1,462 @@
+package com.androidvip.sysctlgui.ui.search
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.expandHorizontally
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkHorizontally
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.ArrowBack
+import androidx.compose.material.icons.rounded.Clear
+import androidx.compose.material.icons.rounded.Search
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.ListItem
+import androidx.compose.material3.ListItemDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SearchBar
+import androidx.compose.material3.SearchBarDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.androidvip.sysctlgui.R
+import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
+import com.androidvip.sysctlgui.models.SearchHint
+import com.androidvip.sysctlgui.models.UiKernelParam
+import com.androidvip.sysctlgui.ui.main.MainViewEvent
+import com.androidvip.sysctlgui.ui.main.MainViewModel
+import com.androidvip.sysctlgui.ui.main.MainViewState
+import com.androidvip.sysctlgui.ui.params.browse.ParamRow
+import org.koin.compose.viewmodel.koinViewModel
+
+@Composable
+fun SearchScreen(
+ viewModel: SearchViewModel = koinViewModel(),
+ mainViewModel: MainViewModel = koinViewModel(),
+ onParamSelected: (UiKernelParam) -> Unit,
+ onNavigateBack: () -> Unit
+) {
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
+ var searchQuery by remember { mutableStateOf("") }
+ var searchActive by remember { mutableStateOf(false) }
+
+ LaunchedEffect(Unit) {
+ mainViewModel.onEvent(
+ MainViewEvent.OnSateChangeRequested(
+ MainViewState(
+ showTopBar = false,
+ showNavBar = true,
+ )
+ )
+ )
+ }
+
+ LaunchedEffect(viewModel.effect) {
+ viewModel.effect.collect { effect ->
+ when (effect) {
+ is SearchViewEffect.EditKernelParam -> onParamSelected(effect.param)
+ SearchViewEffect.NavigateBack -> onNavigateBack()
+ }
+ }
+ }
+
+ SearchScreenContent(
+ searchQuery = searchQuery,
+ onSearchQueryChange = {
+ searchQuery = it
+ viewModel.onEvent(SearchViewEvent.SearchQueryChange(it))
+ },
+ searchActive = searchActive,
+ onSearchActiveChange = { searchActive = it },
+ searchHints = state.searchHints,
+ searchResults = state.searchResults,
+ onNavigateBack = { viewModel.onEvent(SearchViewEvent.BackClicked) },
+ onHistoryItemRemoveClicked = {
+ viewModel.onEvent(SearchViewEvent.HistoryItemRemoveClicked(it))
+ },
+ onSearch = { query ->
+ searchActive = false
+ viewModel.onEvent(SearchViewEvent.SearchRequested(query))
+ }
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun SearchScreenContent(
+ searchQuery: String,
+ onSearchQueryChange: (String) -> Unit,
+ searchActive: Boolean,
+ onSearchActiveChange: (Boolean) -> Unit,
+ searchHints: List,
+ searchResults: List,
+ onHistoryItemRemoveClicked: (SearchHint) -> Unit,
+ onNavigateBack: () -> Unit,
+ onSearch: (String) -> Unit
+) {
+ val focusManager = LocalFocusManager.current
+ val searchBarHorizontalPadding by animateDpAsState(
+ targetValue = if (searchActive) 0.dp else 16.dp,
+ label = "SearchBarHorizontalPadding",
+ animationSpec = tween(durationMillis = 300)
+ )
+ val searchBarTopPadding by animateDpAsState(
+ targetValue = if (searchActive) 0.dp else 8.dp,
+ label = "SearchBarTopPadding",
+ animationSpec = tween(durationMillis = 300)
+ )
+
+ Scaffold(
+ topBar = {
+ val onActiveChange: (Boolean) -> Unit = { isActive ->
+ if (searchActive != isActive) {
+ onSearchActiveChange(isActive)
+ }
+ if (!isActive && searchQuery.isNotEmpty()) {
+ onSearchQueryChange("")
+ }
+ }
+ val searchBarColors = SearchBarDefaults.colors()
+ SearchBar(
+ inputField = {
+ SearchBarDefaults.InputField(
+ query = searchQuery,
+ onQueryChange = onSearchQueryChange,
+ onSearch = onSearch,
+ expanded = searchActive,
+ onExpandedChange = onActiveChange,
+ placeholder = { Text("Search kernel parameters") },
+ leadingIcon = {
+ AnimatedVisibility(
+ visible = searchActive,
+ enter = expandVertically() + fadeIn(),
+ exit = shrinkVertically() + fadeOut()
+ ) {
+ IconButton(onClick = {
+ focusManager.clearFocus()
+ onSearchActiveChange(false)
+ onSearchQueryChange("")
+ if (searchQuery.isEmpty()) {
+ onNavigateBack()
+ }
+ }) {
+ Icon(
+ Icons.AutoMirrored.Rounded.ArrowBack,
+ contentDescription = "Back"
+ )
+ }
+ }
+
+
+ AnimatedVisibility(
+ visible = !searchActive,
+ enter = expandVertically(
+ animationSpec = tween(delayMillis = 200)
+ ) + fadeIn(
+ animationSpec = tween(delayMillis = 200)
+ ),
+ exit = shrinkVertically(
+ animationSpec = tween(durationMillis = 0)
+ ) + fadeOut(
+ animationSpec = tween(durationMillis = 0)
+ )
+ ) {
+ Icon(Icons.Rounded.Search, contentDescription = "Search Icon")
+ }
+ },
+ trailingIcon = {
+ AnimatedVisibility(
+ visible = searchQuery.isNotEmpty() && searchActive,
+ enter = expandHorizontally(expandFrom = Alignment.Start) + fadeIn(),
+ exit = shrinkHorizontally(shrinkTowards = Alignment.Start) + fadeOut()
+ ) {
+ IconButton(onClick = {
+ onSearchQueryChange("")
+ }) {
+ Icon(Icons.Rounded.Clear, contentDescription = "Clear search")
+ }
+ }
+ },
+ colors = searchBarColors.inputFieldColors,
+ )
+ },
+ expanded = searchActive,
+ onExpandedChange = onActiveChange,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = searchBarHorizontalPadding)
+ .padding(top = searchBarTopPadding),
+ shape = SearchBarDefaults.inputFieldShape,
+ colors = searchBarColors,
+ tonalElevation = SearchBarDefaults.TonalElevation,
+ shadowElevation = SearchBarDefaults.ShadowElevation,
+ windowInsets = SearchBarDefaults.windowInsets,
+ ) {
+ SearchViewContent(
+ searchHints = searchHints,
+ onHistoryItemRemoveClicked = onHistoryItemRemoveClicked,
+ onSearchQueryChange = onSearchQueryChange,
+ onSearch = onSearch,
+ onSearchActiveChange = onSearchActiveChange,
+ searchQuery = searchQuery
+ )
+ }
+ }
+ ) { innerPadding ->
+ Box(modifier = Modifier.padding(innerPadding)) {
+ if (!searchActive) {
+ SearchResultsContent(searchResults, searchQuery)
+ }
+
+ CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
+ }
+ }
+}
+
+@Composable
+private fun SearchViewContent(
+ searchHints: List,
+ onHistoryItemRemoveClicked: (SearchHint) -> Unit,
+ onSearchQueryChange: (String) -> Unit,
+ onSearch: (String) -> Unit,
+ onSearchActiveChange: (Boolean) -> Unit,
+ searchQuery: String
+) {
+ val historyHints = searchHints.filter { it.isFromHistory }
+ val suggestionHints = searchHints.filter { !it.isFromHistory }
+
+ LazyColumn(modifier = Modifier.fillMaxWidth()) {
+ if (historyHints.isNotEmpty()) {
+ item(key = "history_header") {
+ Text(
+ text = "Recent Searches",
+ style = MaterialTheme.typography.titleSmall,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.primary,
+ modifier = Modifier
+ .animateItem()
+ .padding(horizontal = 16.dp, vertical = 12.dp)
+ )
+ }
+ items(historyHints, key = { it.hint }) { hintItem ->
+ ListItem(
+ colors = ListItemDefaults.colors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
+ ),
+ headlineContent = { Text(hintItem.hint) },
+ leadingContent = {
+ Icon(
+ painter = painterResource(R.drawable.ic_history),
+ contentDescription = "History item"
+ )
+ },
+ trailingContent = {
+ IconButton(
+ onClick = { onHistoryItemRemoveClicked(hintItem) },
+ modifier = Modifier.offset(16.dp)
+ ) {
+ Icon(
+ Icons.Rounded.Clear,
+ contentDescription = "Clear history item"
+ )
+ }
+ },
+ modifier = Modifier
+ .clickable {
+ onSearchQueryChange(hintItem.hint)
+ onSearch(hintItem.hint)
+ onSearchActiveChange(false)
+ }
+ .animateItem()
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ )
+ }
+ if (suggestionHints.isNotEmpty()) {
+ item { HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) }
+ }
+ }
+
+ if (suggestionHints.isNotEmpty()) {
+ item {
+ Text(
+ text = "Suggestions",
+ style = MaterialTheme.typography.titleSmall,
+ color = MaterialTheme.colorScheme.primary,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
+ )
+ }
+ items(suggestionHints, key = { it.hint }) { hintItem ->
+ ListItem(
+ colors = ListItemDefaults.colors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
+ ),
+ headlineContent = { Text(hintItem.hint) },
+ modifier = Modifier
+ .clickable {
+ onSearchQueryChange(hintItem.hint)
+ onSearch(hintItem.hint)
+ onSearchActiveChange(false)
+ }
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp)
+ )
+ }
+ }
+
+ if (searchHints.isEmpty() && searchQuery.isBlank()) {
+ item {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Text("No suggestions available.")
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun SearchResultsContent(searchResults: List, searchQuery: String) {
+ if (searchResults.isNotEmpty()) {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(top = 8.dp)
+ ) {
+ itemsIndexed(searchResults) { index, param ->
+ ParamRow(
+ modifier = Modifier.animateItem(),
+ param = param,
+ showFullName = true,
+ onParamClicked = {}
+ )
+
+ if (index < searchResults.lastIndex) {
+ HorizontalDivider()
+ }
+ }
+ }
+ } else if (searchQuery.isNotEmpty()) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(24.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = "No results found for \"$searchQuery\"",
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onBackground,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ } else {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(24.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = "Enter a query to search for kernel parameters.",
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onBackground,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+}
+
+@Composable
+@Preview
+private fun SearchScreenPreview() {
+ SysctlGuiTheme {
+ var searchQuery by remember { mutableStateOf("") }
+ var searchActive by remember { mutableStateOf(false) }
+ var searchHints by remember {
+ mutableStateOf(
+ listOf(
+ SearchHint("vm.swappiness", isFromHistory = false),
+ SearchHint("net.ipv4.tcp_congestion_control", isFromHistory = false),
+ SearchHint("kernel.panic", isFromHistory = true),
+ SearchHint("fs.file-max", isFromHistory = true)
+ )
+ )
+ }
+ var searchResults by remember {
+ mutableStateOf(
+ listOf(
+ UiKernelParam(
+ name = "vm.swappiness",
+ path = "/proc/sys/vm/swappiness",
+ isFavorite = false
+ ),
+ UiKernelParam(
+ name = "vm.overcommit_memory",
+ path = "/proc/sys/vm/overcommit_memory",
+ value = "1",
+ isFavorite = true
+ ),
+ UiKernelParam(
+ name = "net.ipv4.tcp_congestion_control",
+ path = "/proc/sys/net/ipv4/tcp_congestion_control",
+ value = "cubic"
+ )
+ )
+ )
+ }
+
+ SearchScreenContent(
+ searchQuery = searchQuery,
+ onSearchQueryChange = { searchQuery = it },
+ searchActive = searchActive,
+ onSearchActiveChange = { searchActive = it },
+ searchHints = searchHints,
+ searchResults = searchResults,
+ onNavigateBack = {},
+ onHistoryItemRemoveClicked = { searchHints = searchHints - it },
+ onSearch = {
+ searchResults = searchResults.filter {
+ it.name.contains(searchQuery, ignoreCase = true)
+ }
+ }
+ )
+ }
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/search/SearchViewModel.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/search/SearchViewModel.kt
new file mode 100644
index 0000000..b024a89
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/search/SearchViewModel.kt
@@ -0,0 +1,140 @@
+package com.androidvip.sysctlgui.ui.search
+
+import androidx.lifecycle.viewModelScope
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import com.androidvip.sysctlgui.domain.repository.AppPrefs
+import com.androidvip.sysctlgui.domain.usecase.GetRuntimeParamsUseCase
+import com.androidvip.sysctlgui.domain.usecase.GetUserParamsUseCase
+import com.androidvip.sysctlgui.helpers.UiKernelParamMapper
+import com.androidvip.sysctlgui.models.SearchHint
+import com.androidvip.sysctlgui.models.UiKernelParam
+import com.androidvip.sysctlgui.utils.BaseViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class SearchViewModel(
+ private val getUserParams: GetUserParamsUseCase,
+ private val getRuntimeParams: GetRuntimeParamsUseCase,
+ private val appPrefs: AppPrefs
+) : BaseViewModel() {
+ private var preSearchJob: Job? = null
+ private val searchableParams = mutableListOf()
+ private val searchHistory = mutableSetOf()
+
+ init {
+ viewModelScope.launch {
+ setState { copy(loading = true) }
+
+ val fetchedHistory = appPrefs.searchHistory
+ val fetchedParams = fetchParams()
+
+ searchHistory.addAll(fetchedHistory)
+ searchableParams.addAll(fetchedParams)
+
+ val uiSearchHints = fetchedHistory.map { SearchHint(hint = it, isFromHistory = true) }
+
+ setState {
+ copy(
+ loading = false,
+ searchHints = uiSearchHints
+ )
+ }
+ }
+ }
+
+ override fun createInitialState() = SearchViewState()
+
+ private suspend fun fetchParams(): List {
+ val userParams = getUserParams()
+ return getRuntimeParams(userParams).map(UiKernelParamMapper::map)
+ }
+
+ override fun onEvent(event: SearchViewEvent) {
+ when (event) {
+ SearchViewEvent.BackClicked -> setEffect { SearchViewEffect.NavigateBack }
+ is SearchViewEvent.HistoryItemRemoveClicked -> handleRemoveFromHistory(event.hint)
+ is SearchViewEvent.SearchRequested -> handleSearch(event.query)
+ is SearchViewEvent.SearchQueryChange -> handleSearchQueryChange(event.query)
+ is SearchViewEvent.ParamClicked -> setEffect {
+ SearchViewEffect.EditKernelParam(event.param)
+ }
+ }
+ }
+
+ private fun handleSearchQueryChange(query: String) {
+ preSearchJob?.cancel()
+ if (query.isEmpty()) {
+ setState { copy(searchResults = emptyList()) }
+ } else if (query.length >= MIN_PRE_SEARCH_QUERY_LENGTH) {
+ preSearchJob = viewModelScope.launch {
+ delay(300L) // Debounce delay
+ handlePreSearch(query)
+ }
+ }
+ }
+
+ private fun handleSearch(query: String) {
+ viewModelScope.launch {
+ setState { copy(loading = true) }
+ searchHistory.add(query)
+ appPrefs.addSearchToHistory(query)
+
+ val hints = withContext(Dispatchers.IO) {
+ val historyHints = searchHistory
+ .take(MAX_SEARCH_HISTORY)
+ .map { SearchHint(hint = it, isFromHistory = true) }
+
+ val paramHints = searchableParams
+ .filter { it.name.contains(query, ignoreCase = true) }
+ .take(MAX_SEARCH_HINTS)
+ .map { SearchHint(it.name) }
+
+ historyHints + paramHints
+ }
+
+ setState {
+ copy(loading = false, searchHints = hints)
+ }
+ }
+ }
+
+ private fun handlePreSearch(query: String) {
+ viewModelScope.launch {
+ val hints = withContext(Dispatchers.IO) {
+ searchableParams
+ .filter { it.name.contains(query, ignoreCase = true) }
+ .map(UiKernelParamMapper::map)
+ }
+
+ setState {
+ copy(loading = false, searchResults = hints)
+ }
+ }
+ }
+
+ private fun handleRemoveFromHistory(searchHint: SearchHint) {
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ appPrefs.removeSearchFromHistory(searchHint.hint)
+ searchHistory.remove(searchHint.hint)
+ }
+
+ setState {
+ copy(
+ searchHints = searchHistory
+ .take(MAX_SEARCH_HISTORY)
+ .map { SearchHint(hint = it, isFromHistory = true) }
+ )
+ }
+ }
+ }
+
+ companion object {
+ private const val MIN_PRE_SEARCH_QUERY_LENGTH = 4
+ private const val MAX_SEARCH_HISTORY = 3
+ private const val MAX_SEARCH_HINTS = 5
+ }
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/search/SearchViewState.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/search/SearchViewState.kt
new file mode 100644
index 0000000..eea820a
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/search/SearchViewState.kt
@@ -0,0 +1,23 @@
+package com.androidvip.sysctlgui.ui.search
+
+import com.androidvip.sysctlgui.models.SearchHint
+import com.androidvip.sysctlgui.models.UiKernelParam
+
+data class SearchViewState(
+ val loading: Boolean = true,
+ val searchHints: List = emptyList(),
+ val searchResults: List = emptyList()
+)
+
+sealed interface SearchViewEvent {
+ data object BackClicked : SearchViewEvent
+ data class SearchQueryChange(val query: String) : SearchViewEvent
+ data class HistoryItemRemoveClicked(val hint: SearchHint) : SearchViewEvent
+ data class SearchRequested(val query: String) : SearchViewEvent
+ data class ParamClicked(val param: UiKernelParam) : SearchViewEvent
+}
+
+sealed interface SearchViewEffect {
+ data object NavigateBack : SearchViewEffect
+ data class EditKernelParam(val param: UiKernelParam) : SearchViewEffect
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/SettingsFragment.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/SettingsFragment.kt
deleted file mode 100644
index afb695a..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/SettingsFragment.kt
+++ /dev/null
@@ -1,120 +0,0 @@
-package com.androidvip.sysctlgui.ui.settings
-
-import android.app.NotificationManager
-import android.content.Context
-import android.os.Build
-import android.os.Bundle
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.lifecycle.lifecycleScope
-import androidx.preference.Preference
-import androidx.preference.PreferenceFragmentCompat
-import androidx.preference.SwitchPreferenceCompat
-import com.androidvip.sysctlgui.R
-import com.androidvip.sysctlgui.data.utils.RootUtils
-import com.androidvip.sysctlgui.domain.repository.AppPrefs
-import com.androidvip.sysctlgui.helpers.StartUpServiceToggle
-import com.androidvip.sysctlgui.utils.Consts
-import com.google.android.material.color.DynamicColors
-import kotlinx.coroutines.launch
-import org.koin.android.ext.android.inject
-
-class SettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceChangeListener {
- private val prefs: AppPrefs by inject()
- private val rootUtils: RootUtils by inject()
-
- private val notificationPermissionLauncher =
- registerForActivityResult(ActivityResultContracts.RequestPermission()) { _ -> }
-
- override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
- setPreferencesFromResource(R.xml.preferences, rootKey)
-
- val currCommitMode = prefs.commitMode
- val commitModePref = findPreference(Consts.Prefs.COMMIT_MODE)
- commitModePref?.summary = if (currCommitMode == "sysctl") {
- "Use sysctl -w"
- } else {
- "Use echo 'value' > /proc/sys/…"
- }
-
- val startupDelay = prefs.startUpDelay
- val startupDelayPref = findPreference(Consts.Prefs.START_UP_DELAY)
-
- startupDelayPref?.summary = if (startupDelay > 0) {
- getString(R.string.startup_delay_sum, startupDelay)
- } else {
- getString(R.string.startup_delay_disabled)
- }
-
- val useBusyboxPref = findPreference(Consts.Prefs.USE_BUSYBOX) as SwitchPreferenceCompat?
- lifecycleScope.launch {
- if (rootUtils.isBusyboxAvailable()) {
- useBusyboxPref?.isEnabled = true
- } else {
- useBusyboxPref?.isChecked = false
- useBusyboxPref?.isEnabled = false
- }
- }
-
- val dynamicColorsPref = findPreference(Consts.Prefs.DYNAMIC_COLORS) as SwitchPreferenceCompat?
- dynamicColorsPref?.isEnabled = DynamicColors.isDynamicColorAvailable()
-
- commitModePref?.onPreferenceChangeListener = this
- startupDelayPref?.onPreferenceChangeListener = this
- dynamicColorsPref?.onPreferenceChangeListener = this
- findPreference(Consts.Prefs.RUN_ON_START_UP)?.onPreferenceChangeListener = this
- findPreference(Consts.Prefs.FORCE_DARK_THEME)?.onPreferenceChangeListener = this
- }
-
- override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean {
- when (preference.key) {
- Consts.Prefs.RUN_ON_START_UP -> {
- StartUpServiceToggle.toggleStartUpService(requireContext(), newValue == true)
- askForNotificationPermission()
- }
-
- Consts.Prefs.COMMIT_MODE -> {
- preference.summary = if (newValue == "sysctl") {
- "Use sysctl -w"
- } else {
- "Use echo 'value' > /proc/sys/…"
- }
- }
-
- Consts.Prefs.START_UP_DELAY -> {
- val selectedValue = (newValue as? Int) ?: 0
-
- preference.summary = if (selectedValue > 0) {
- getString(R.string.startup_delay_sum, selectedValue)
- } else {
- getString(R.string.startup_delay_disabled)
- }
- }
-
- Consts.Prefs.FORCE_DARK_THEME -> {
- requireActivity().recreate()
- }
-
- Consts.Prefs.DYNAMIC_COLORS -> {
- if (newValue == true) {
- DynamicColors.applyToActivitiesIfAvailable(requireActivity().application)
- }
- requireActivity().recreate()
- }
- }
-
- return true
- }
-
- private fun askForNotificationPermission() {
- val manager = requireContext().getSystemService(Context.NOTIFICATION_SERVICE)
- as NotificationManager
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- if (!manager.areNotificationsEnabled()) {
- notificationPermissionLauncher.launch(
- android.Manifest.permission.POST_NOTIFICATIONS
- )
- }
- }
- }
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/SettingsScreen.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/SettingsScreen.kt
new file mode 100644
index 0000000..534a37e
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/SettingsScreen.kt
@@ -0,0 +1,233 @@
+package com.androidvip.sysctlgui.ui.settings
+
+import android.Manifest
+import android.content.pm.PackageManager
+import android.os.Build
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+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.tooling.preview.PreviewLightDark
+import androidx.compose.ui.unit.dp
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.androidvip.sysctlgui.data.Prefs
+import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
+import com.androidvip.sysctlgui.domain.enums.CommitMode
+import com.androidvip.sysctlgui.domain.enums.SettingItemType
+import com.androidvip.sysctlgui.domain.models.AppSetting
+import com.androidvip.sysctlgui.ui.main.MainViewEvent
+import com.androidvip.sysctlgui.ui.main.MainViewModel
+import com.androidvip.sysctlgui.ui.main.MainViewState
+import com.androidvip.sysctlgui.ui.settings.components.HeaderComponent
+import com.androidvip.sysctlgui.ui.settings.components.SliderSettingComponent
+import com.androidvip.sysctlgui.ui.settings.components.SwitchSettingComponent
+import com.androidvip.sysctlgui.ui.settings.components.TextSettingComponent
+import com.androidvip.sysctlgui.ui.settings.model.SettingsViewEffect
+import com.androidvip.sysctlgui.ui.settings.model.SettingsViewEvent
+import org.koin.androidx.compose.koinViewModel
+
+internal const val DISABLED_ALPHA = 0.38f
+
+@Composable
+internal fun SettingsScreen(
+ mainViewModel: MainViewModel = koinViewModel(),
+ viewModel: SettingsViewModel = koinViewModel(),
+ onNavigateToUserParams: () -> Unit
+) {
+ val state = viewModel.uiState.collectAsStateWithLifecycle()
+ val context = LocalContext.current
+ var hasNotificationPermission by remember {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ mutableStateOf(
+ ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) == PackageManager.PERMISSION_GRANTED
+ )
+ } else {
+ mutableStateOf(true)
+ }
+ }
+
+ val permissionLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.RequestPermission(),
+ onResult = { isGranted ->
+ hasNotificationPermission = isGranted
+ }
+ )
+
+
+ LaunchedEffect(Unit) {
+ mainViewModel.onEvent(
+ MainViewEvent.OnSateChangeRequested(
+ MainViewState(
+ showTopBar = true,
+ showNavBar = true,
+ )
+ )
+ )
+ }
+
+ LaunchedEffect(Unit) {
+ viewModel.effect.collect { effect ->
+ when (effect) {
+ SettingsViewEffect.RequestNotificationPermission -> {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (!hasNotificationPermission) {
+ permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ SettingsScreenContent(
+ settings = state.value.settings,
+ onNavigateToUserParams = onNavigateToUserParams,
+ onValueChanged = { appSetting, newValue ->
+ viewModel.onEvent(SettingsViewEvent.SettingValueChanged(appSetting, newValue))
+ }
+ )
+}
+
+@Composable
+private fun SettingsScreenContent(
+ settings: List> = emptyList(),
+ onNavigateToUserParams: () -> Unit,
+ onValueChanged: (AppSetting<*>, Any) -> Unit
+) {
+ val groupedSettings = settings.groupBy { it.category }
+ LazyColumn(modifier = Modifier.fillMaxWidth()) {
+ groupedSettings.forEach { (category, settings) ->
+ item {
+ Text(
+ text = category,
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.padding(
+ top = 8.dp,
+ bottom = 8.dp,
+ start = 56.dp,
+ end = 16.dp,
+ )
+ )
+ }
+ items(settings.size) {
+ val appSetting = settings[it]
+ when (appSetting.type) {
+ SettingItemType.Text -> {
+ HeaderComponent(
+ modifier = Modifier.fillMaxWidth(),
+ appSetting = appSetting,
+ onClick = onNavigateToUserParams
+ )
+ }
+ SettingItemType.List -> {
+ TextSettingComponent(
+ modifier = Modifier.fillMaxWidth(),
+ appSetting = appSetting,
+ onValueChange = { newValue ->
+ onValueChanged(appSetting, newValue)
+ }
+ )
+ }
+ SettingItemType.Switch -> {
+ SwitchSettingComponent(
+ modifier = Modifier.fillMaxWidth(),
+ appSetting = appSetting,
+ onValueChange = { newValue ->
+ onValueChanged(appSetting, newValue)
+ }
+ )
+ }
+ SettingItemType.Slider -> {
+ SliderSettingComponent(
+ modifier = Modifier.fillMaxWidth(),
+ appSetting = appSetting,
+ onValueChange = { newValue ->
+ onValueChanged(appSetting, newValue)
+ }
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+@PreviewLightDark
+internal fun SettingsScreenPreview() {
+ SysctlGuiTheme {
+ val settings = listOf(
+ /////////// GENERAL SETTINGS ////////////
+ AppSetting(
+ key = Prefs.ListFoldersFirst.key,
+ value = true,
+ category = "General",
+ title = "List folders first",
+ description = "List folders first when using the kernel parameter browser option",
+ type = SettingItemType.Switch,
+ ),
+
+ /////////// THEME SETTINGS ////////////
+
+ AppSetting(
+ key = Prefs.ContrastLevel.key,
+ value = 1,
+ category = "Theme",
+ title = "Contrast level",
+ description = "Contrast level for the theme colors",
+ type = SettingItemType.Slider,
+ values = listOf(1, 2, 3),
+ ),
+
+ /////////// COMMIT SETTINGS ////////////
+
+ AppSetting(
+ key = Prefs.CommitMode.key,
+ value = "sysctl",
+ category = "Operations",
+ title = "Commit mode",
+ description = "Command used when applying the parameter value",
+ type = SettingItemType.List,
+ values = listOf(
+ CommitMode.SYSCTL.name.lowercase(),
+ CommitMode.ECHO.name.lowercase(),
+ )
+ ),
+
+ /////////// STARTUP SETTINGS ////////////
+ AppSetting(
+ key = "",
+ value = Unit,
+ category = "Startup",
+ title = "Manage parameters",
+ description = "Manage the parameters that will be applied at startup",
+ type = SettingItemType.Text,
+ )
+ )
+ Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
+ SettingsScreenContent(
+ settings = settings,
+ onNavigateToUserParams = {},
+ onValueChanged = { _, _ -> }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/SettingsViewModel.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/SettingsViewModel.kt
new file mode 100644
index 0000000..0828835
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/SettingsViewModel.kt
@@ -0,0 +1,68 @@
+package com.androidvip.sysctlgui.ui.settings
+
+import android.content.SharedPreferences
+import androidx.core.content.edit
+import androidx.lifecycle.viewModelScope
+import com.androidvip.sysctlgui.data.Prefs
+import com.androidvip.sysctlgui.domain.enums.SettingItemType
+import com.androidvip.sysctlgui.domain.usecase.GetAppSettingsUseCase
+import com.androidvip.sysctlgui.ui.settings.model.SettingsViewEffect
+import com.androidvip.sysctlgui.ui.settings.model.SettingsViewEvent
+import com.androidvip.sysctlgui.ui.settings.model.SettingsViewState
+import com.androidvip.sysctlgui.utils.BaseViewModel
+import kotlinx.coroutines.launch
+
+class SettingsViewModel(
+ private val sharedPreferences: SharedPreferences,
+ private val getSettings: GetAppSettingsUseCase
+) : BaseViewModel() {
+
+ init {
+ viewModelScope.launch {
+ val settings = getSettings()
+ setState { copy(settings = settings) }
+ }
+ }
+
+ override fun createInitialState() = SettingsViewState()
+
+ override fun onEvent(event: SettingsViewEvent) {
+ when (event) {
+ is SettingsViewEvent.SettingValueChanged<*> -> {
+ if (event.appSetting.type == SettingItemType.Text) return
+ if (event.appSetting.key.isEmpty()) return
+
+ when (event.newValue) {
+ is Boolean -> sharedPreferences.edit {
+ putBoolean(event.appSetting.key, event.newValue)
+ }
+
+ is Int -> sharedPreferences.edit {
+ putInt(event.appSetting.key, event.newValue)
+ }
+
+ is Long -> sharedPreferences.edit {
+ putLong(event.appSetting.key, event.newValue)
+ }
+
+ is Float -> sharedPreferences.edit {
+ putFloat(event.appSetting.key, event.newValue)
+ }
+
+ is String -> sharedPreferences.edit {
+ putString(event.appSetting.key, event.newValue)
+ }
+
+ is List<*> -> sharedPreferences.edit {
+ val stringValues = event.newValue.filterIsInstance().toSet()
+ putStringSet(event.appSetting.key, stringValues)
+ }
+ }
+
+ if (event.appSetting.key == Prefs.RunOnStartup.key) {
+ setEffect { SettingsViewEffect.RequestNotificationPermission }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/components/HeaderComponent.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/components/HeaderComponent.kt
new file mode 100644
index 0000000..2c7c59a
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/components/HeaderComponent.kt
@@ -0,0 +1,46 @@
+package com.androidvip.sysctlgui.ui.settings.components
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.androidvip.sysctlgui.domain.models.AppSetting
+
+@Composable
+fun HeaderComponent(
+ modifier: Modifier = Modifier,
+ appSetting: AppSetting<*>,
+ onClick: () -> Unit,
+ icon: @Composable (() -> Unit)? = null
+) {
+ Box(modifier = modifier.clickable(onClick = onClick)) {
+ Row(
+ modifier = Modifier.padding(all = 16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(space = 16.dp)
+ ) {
+
+ Box(
+ modifier = Modifier
+ .align(Alignment.CenterVertically)
+ .size(24.dp)
+ ) {
+ icon?.invoke()
+ }
+
+ SettingsComponentColumn(
+ title = appSetting.title,
+ description = appSetting.description,
+ enabled = appSetting.enabled,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/components/SettingsComponentColumn.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/components/SettingsComponentColumn.kt
new file mode 100644
index 0000000..29e877a
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/components/SettingsComponentColumn.kt
@@ -0,0 +1,45 @@
+package com.androidvip.sysctlgui.ui.settings.components
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.androidvip.sysctlgui.ui.settings.DISABLED_ALPHA
+
+@Composable
+internal fun SettingsComponentColumn(
+ modifier: Modifier = Modifier,
+ title: String,
+ description: String? = null,
+ enabled: Boolean = true,
+ bottomContent: @Composable (ColumnScope.() -> Unit)? = null
+) {
+ Column(
+ verticalArrangement = Arrangement.Center,
+ modifier = modifier
+ ) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onBackground
+ .copy(alpha = if (enabled) 1f else DISABLED_ALPHA)
+ )
+ description?.let {
+ Text(
+ text = it,
+ modifier = Modifier.padding(top = 2.dp),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onBackground
+ .copy(alpha = if (enabled) 0.8f else DISABLED_ALPHA)
+ )
+ }
+ bottomContent?.let {
+ bottomContent()
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/components/SliderSettingComponent.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/components/SliderSettingComponent.kt
new file mode 100644
index 0000000..c2e563b
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/components/SliderSettingComponent.kt
@@ -0,0 +1,89 @@
+package com.androidvip.sysctlgui.ui.settings.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Slider
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.compose.ui.unit.dp
+import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
+import com.androidvip.sysctlgui.domain.enums.SettingItemType
+import com.androidvip.sysctlgui.domain.models.AppSetting
+
+@Composable
+fun SliderSettingComponent(
+ modifier: Modifier = Modifier,
+ appSetting: AppSetting<*>,
+ onValueChange: (Int) -> Unit,
+ icon: @Composable (() -> Unit)? = null
+) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = modifier.padding(all = 16.dp),
+ horizontalArrangement = Arrangement.spacedBy(space = 16.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .align(Alignment.CenterVertically)
+ .size(24.dp)
+ ) {
+ icon?.invoke()
+ }
+
+ val values = appSetting.values?.filterIsInstance() ?: emptyList()
+ val minValue = values.min().toFloat()
+ val maxValue = values.max().toFloat()
+ var value by remember {
+ mutableFloatStateOf((appSetting.value as? Int)?.toFloat() ?: minValue)
+ }
+
+ SettingsComponentColumn(
+ title = appSetting.title,
+ description = appSetting.description + " (${value.toInt()})",
+ enabled = appSetting.enabled,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Slider(
+ modifier = Modifier.padding(top = 4.dp),
+ value = value,
+ enabled = appSetting.enabled,
+ onValueChange = { value = it.toInt().toFloat(); onValueChange(it.toInt()) },
+ valueRange = minValue..maxValue,
+ )
+ }
+ }
+}
+
+@Composable
+@PreviewLightDark
+fun SliderSettingComponentPreview() {
+ SysctlGuiTheme(dynamicColor = true) {
+ Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
+ SliderSettingComponent(
+ appSetting = AppSetting(
+ key = "key",
+ title = "Title",
+ description = "Description",
+ value = 0,
+ category = "",
+ type = SettingItemType.Slider,
+ values = (0..10).toList(),
+ ),
+ onValueChange = {}
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/components/SwitchSettingComponent.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/components/SwitchSettingComponent.kt
new file mode 100644
index 0000000..09a1369
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/components/SwitchSettingComponent.kt
@@ -0,0 +1,91 @@
+package com.androidvip.sysctlgui.ui.settings.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Switch
+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.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.compose.ui.unit.dp
+import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
+import com.androidvip.sysctlgui.domain.enums.SettingItemType
+import com.androidvip.sysctlgui.domain.models.AppSetting
+
+@Composable
+fun SwitchSettingComponent(
+ modifier: Modifier = Modifier,
+ appSetting: AppSetting<*>,
+ onValueChange: (newValue: Boolean) -> Unit,
+ icon: @Composable (() -> Unit)? = null
+) {
+ var checked by remember { mutableStateOf(appSetting.value as? Boolean) }
+
+ Row(
+ modifier = modifier
+ .padding(all = 16.dp)
+ .clickable(enabled = appSetting.enabled) {
+ onValueChange(!(checked ?: false))
+ checked = !(checked ?: false)
+ },
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(space = 16.dp)
+ ) {
+ Box(
+ modifier = Modifier
+ .align(Alignment.CenterVertically)
+ .size(24.dp)
+ ) {
+ icon?.invoke()
+ }
+
+ SettingsComponentColumn(
+ title = appSetting.title,
+ description = appSetting.description,
+ enabled = appSetting.enabled,
+ modifier = Modifier
+ .weight(1f)
+ .padding(end = 16.dp)
+ )
+
+ Switch(
+ checked = checked == true,
+ onCheckedChange = {
+ onValueChange(it)
+ checked = it
+ },
+ enabled = appSetting.enabled,
+ modifier = Modifier.align(Alignment.CenterVertically)
+ )
+ }
+}
+
+@Composable
+@PreviewLightDark
+fun SwitchSettingComponentPreview() {
+ SysctlGuiTheme(dynamicColor = true) {
+ Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
+ SwitchSettingComponent(
+ appSetting = AppSetting(
+ key = "key",
+ title = "Title",
+ description = "Description",
+ value = true,
+ category = "",
+ type = SettingItemType.Switch
+ ),
+ onValueChange = {}
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/components/TextSettingComponent.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/components/TextSettingComponent.kt
new file mode 100644
index 0000000..f8b1071
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/components/TextSettingComponent.kt
@@ -0,0 +1,111 @@
+package com.androidvip.sysctlgui.ui.settings.components
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.compose.ui.unit.dp
+import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
+import com.androidvip.sysctlgui.domain.enums.SettingItemType
+import com.androidvip.sysctlgui.domain.models.AppSetting
+
+@Composable
+fun TextSettingComponent(
+ modifier: Modifier = Modifier,
+ appSetting: AppSetting<*>,
+ onValueChange: (String) -> Unit,
+ icon: @Composable (() -> Unit)? = null
+) {
+ var expanded by remember { mutableStateOf(false) }
+
+ Box(
+ modifier = modifier
+ .clickable(enabled = appSetting.enabled && appSetting.type == SettingItemType.List) {
+ expanded = true
+ }
+ ) {
+ Row(
+ modifier = Modifier.padding(all = 16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(space = 16.dp)
+ ) {
+
+ Box(
+ modifier = Modifier
+ .align(Alignment.CenterVertically)
+ .size(24.dp)
+ ) {
+ icon?.invoke()
+ }
+
+ SettingsComponentColumn(
+ title = appSetting.title,
+ description = appSetting.description,
+ enabled = appSetting.enabled,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+
+
+ DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false }
+ ) {
+ appSetting.values?.forEach { item ->
+ DropdownMenuItem(
+ text = {
+ Text(
+ text = item as String,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurface
+ )
+ },
+ onClick = {
+ onValueChange(item as String)
+ expanded = false
+ }
+ )
+ }
+ }
+}
+
+@Composable
+@PreviewLightDark
+private fun TextSettingComponentPreview() {
+ SysctlGuiTheme(dynamicColor = true) {
+ Box(modifier = Modifier
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.background)) {
+ TextSettingComponent(
+ appSetting = AppSetting(
+ key = "key",
+ title = "Title",
+ description = "Description",
+ value = "sysctl",
+ category = "",
+ values = listOf("sysctl", "echo"),
+ type = SettingItemType.List
+ ),
+ onValueChange = {}
+ )
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/model/SettingsViewEvent.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/model/SettingsViewEvent.kt
new file mode 100644
index 0000000..21a61b8
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/model/SettingsViewEvent.kt
@@ -0,0 +1,15 @@
+package com.androidvip.sysctlgui.ui.settings.model
+
+import com.androidvip.sysctlgui.domain.models.AppSetting
+
+sealed interface SettingsViewEvent {
+ class SettingValueChanged(val appSetting: AppSetting, val newValue: Any) : SettingsViewEvent
+}
+
+sealed interface SettingsViewEffect {
+ object RequestNotificationPermission : SettingsViewEffect
+}
+
+data class SettingsViewState(
+ val settings: List> = emptyList()
+)
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/start/StartActivity.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/start/StartActivity.kt
index 308f14b..5fe650c 100644
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/start/StartActivity.kt
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/start/StartActivity.kt
@@ -4,49 +4,44 @@ import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.widget.Toast
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AppCompatActivity
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope
import com.androidvip.sysctlgui.R
-import com.androidvip.sysctlgui.data.models.KernelParam
import com.androidvip.sysctlgui.data.utils.RootUtils
import com.androidvip.sysctlgui.databinding.ActivitySplashBinding
-import com.androidvip.sysctlgui.domain.usecase.PerformDatabaseMigrationUseCase
+import com.androidvip.sysctlgui.domain.enums.Actions
+import com.androidvip.sysctlgui.domain.repository.AppPrefs
import com.androidvip.sysctlgui.goAway
-import com.androidvip.sysctlgui.helpers.Actions
import com.androidvip.sysctlgui.toast
-import com.androidvip.sysctlgui.ui.base.BaseAppCompatActivity
import com.androidvip.sysctlgui.ui.main.MainActivity
-import com.androidvip.sysctlgui.ui.params.edit.EditKernelParamActivity
-import com.topjohnwu.superuser.Shell
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
import org.koin.android.ext.android.inject
-class StartActivity : BaseAppCompatActivity() {
+class StartActivity : AppCompatActivity() {
private lateinit var binding: ActivitySplashBinding
private val rootUtils: RootUtils by inject()
- private val performDatabaseMigrationUseCase: PerformDatabaseMigrationUseCase by inject()
- private val dispatcher: CoroutineDispatcher by lazy { Dispatchers.Default }
+ private val prefs: AppPrefs by inject()
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
splashScreen.setKeepOnScreenCondition { true }
binding = ActivitySplashBinding.inflate(layoutInflater)
setContentView(binding.root)
lifecycleScope.launch {
+ rootUtils.getRootShell()
val isRootAccessGiven = checkRootAccess()
binding.splashStatusText.setText(R.string.splash_status_checking_busybox)
val isBusyBoxAvailable = checkBusyBox()
binding.splashStatusText.setText(R.string.splash_status_checking_migration)
- checkForDatabaseMigration()
if (isRootAccessGiven) {
if (!isBusyBoxAvailable) {
@@ -67,23 +62,14 @@ class StartActivity : BaseAppCompatActivity() {
}
}
- private suspend fun checkRootAccess() = withContext(dispatcher) {
+ private suspend fun checkRootAccess(): Boolean {
delay(500)
- Shell.rootAccess()
+ return rootUtils.isRootAvailable()
}
- private suspend fun checkBusyBox() = rootUtils.isBusyboxAvailable().also {
+ private suspend fun checkBusyBox(): Boolean {
delay(500)
- }
-
- private suspend fun checkForDatabaseMigration() {
- delay(500)
- if (!prefs.migrationCompleted) {
- binding.splashStatusText.setText(R.string.splash_status_performing_migration)
-
- val result = runCatching { performDatabaseMigrationUseCase() }
- prefs.migrationCompleted = result.isSuccess
- }
+ return rootUtils.isBusyboxAvailable()
}
private fun navigate() {
@@ -100,7 +86,9 @@ class StartActivity : BaseAppCompatActivity() {
}
Actions.EditParam.name -> {
- Intent(this, EditKernelParamActivity::class.java).apply {
+ Intent(this, MainActivity::class.java)
+ // TODO: handle edit param intent
+ /*Intent(this, EditKernelParamActivity::class.java).apply {
putExtra(
EditKernelParamActivity.EXTRA_PARAM,
intent.extras!!.getParcelable(
@@ -114,7 +102,7 @@ class StartActivity : BaseAppCompatActivity() {
false
)
)
- }
+ }*/
}
else -> {
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/start/StartErrorActivity.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/start/StartErrorActivity.kt
index 6c9b9ea..5602352 100644
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/start/StartErrorActivity.kt
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/start/StartErrorActivity.kt
@@ -4,12 +4,12 @@ import android.content.Intent
import android.graphics.drawable.AnimatedVectorDrawable
import android.os.Bundle
import android.os.Handler
+import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.androidvip.sysctlgui.R
import com.androidvip.sysctlgui.databinding.ActivityStartErrorBinding
-import com.androidvip.sysctlgui.ui.base.BaseAppCompatActivity
-class StartErrorActivity : BaseAppCompatActivity() {
+class StartErrorActivity : AppCompatActivity() {
private lateinit var binding: ActivityStartErrorBinding
override fun onCreate(savedInstanceState: Bundle?) {
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/tasker/TaskerPluginActivity.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/tasker/TaskerPluginActivity.kt
index 65b0e85..c3b59d5 100644
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/tasker/TaskerPluginActivity.kt
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/tasker/TaskerPluginActivity.kt
@@ -1,6 +1,5 @@
package com.androidvip.sysctlgui.ui.tasker
-import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
@@ -8,11 +7,10 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
import com.androidvip.sysctlgui.databinding.ActivityTaskerPluginBinding
import com.androidvip.sysctlgui.receivers.TaskerReceiver
-import com.androidvip.sysctlgui.ui.base.BaseAppCompatActivity
import kotlin.contracts.ExperimentalContracts
@ExperimentalContracts
-class TaskerPluginActivity : BaseAppCompatActivity() {
+class TaskerPluginActivity : AppCompatActivity() {
private lateinit var binding: ActivityTaskerPluginBinding
override fun onCreate(savedInstanceState: Bundle?) {
@@ -32,7 +30,7 @@ class TaskerPluginActivity : BaseAppCompatActivity() {
putExtra(TaskerReceiver.EXTRA_BUNDLE, resultBundle)
}
- setResult(Activity.RESULT_OK, resultIntent)
+ setResult(RESULT_OK, resultIntent)
finish()
}
}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/user/UserParamsScreen.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/user/UserParamsScreen.kt
new file mode 100644
index 0000000..8043c5d
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/user/UserParamsScreen.kt
@@ -0,0 +1,262 @@
+package com.androidvip.sysctlgui.ui.user
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SwipeToDismissBox
+import androidx.compose.material3.SwipeToDismissBoxValue
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberSwipeToDismissBoxState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.androidvip.sysctlgui.R
+import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import com.androidvip.sysctlgui.models.UiKernelParam
+import com.androidvip.sysctlgui.ui.main.MainViewEffect
+import com.androidvip.sysctlgui.ui.main.MainViewEvent
+import com.androidvip.sysctlgui.ui.main.MainViewModel
+import com.androidvip.sysctlgui.ui.main.MainViewState
+import com.androidvip.sysctlgui.ui.params.browse.ParamFileRow
+import kotlinx.coroutines.delay
+import org.koin.compose.viewmodel.koinViewModel
+import kotlin.time.Duration.Companion.milliseconds
+
+const val ANIMATION_DURATION = 300
+
+@Composable
+fun UserParamsScreen(
+ mainViewModel: MainViewModel = koinViewModel(),
+ viewModel: UserParamsViewModel = koinViewModel(),
+ filterPredicate: (UiKernelParam) -> Boolean = { true },
+ onParamSelected: (KernelParam) -> Unit,
+) {
+ val context = LocalContext.current
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
+
+ LaunchedEffect(Unit) {
+ viewModel.onEvent(UserParamsViewEvent.ScreenLoaded(filterPredicate))
+
+ mainViewModel.onEvent(
+ MainViewEvent.OnSateChangeRequested(
+ MainViewState(
+ showTopBar = true,
+ showNavBar = true,
+ showBackButton = false,
+ showSearchAction = true
+ )
+ )
+ )
+
+ mainViewModel.effect.collect { effect ->
+ if (effect is MainViewEffect.ActUponSckbarActionPerformed) {
+ viewModel.onEvent(UserParamsViewEvent.ParamRestoreRequested)
+ }
+ }
+ }
+
+ LaunchedEffect(viewModel.effect) {
+ viewModel.effect.collect { effect ->
+ when (effect) {
+ is UserParamsViewEffect.ShowParamDetails -> {
+ onParamSelected(effect.param)
+ }
+
+ is UserParamsViewEffect.ShowUndoSnackBar -> {
+ mainViewModel.onEvent(
+ MainViewEvent.ShowSnackbarRequested(
+ message = context.getString(R.string.favorite_param_deleted_format, effect.param.name),
+ actionLabel = context.getString(R.string.undo)
+ )
+ )
+ }
+ }
+ }
+ }
+
+ FavoritesScreenContent(
+ favoriteParams = state.userParams,
+ loading = state.loading,
+ onParamClicked = {
+ viewModel.onEvent(UserParamsViewEvent.ParamClicked(it))
+ },
+ onParamDeleteRequested = {
+ viewModel.onEvent(UserParamsViewEvent.ParamDeleteRequested(it))
+ }
+ )
+}
+
+@Composable
+private fun FavoritesScreenContent(
+ favoriteParams: List,
+ loading: Boolean,
+ onParamClicked: (UiKernelParam) -> Unit,
+ onParamDeleteRequested: (UiKernelParam) -> Unit
+) {
+ val listState = rememberLazyListState()
+
+ AnimatedVisibility(visible = loading, enter = fadeIn(), exit = fadeOut()) {
+ Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ CircularProgressIndicator()
+ }
+ }
+
+ if (favoriteParams.isEmpty() && !loading) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(32.dp),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_heart_broken),
+ contentDescription = "Empty",
+ tint = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.size(128.dp)
+ )
+ // TODO: Empty state image
+ Text(
+ text = "No favorites added",
+ style = MaterialTheme.typography.titleLarge,
+ color = MaterialTheme.colorScheme.onBackground,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ return
+ }
+
+ LazyColumn(
+ state = listState,
+ modifier = Modifier.fillMaxSize()
+ ) {
+ items(
+ count = favoriteParams.size,
+ key = { index -> favoriteParams[index].name }
+ ) { index ->
+ val param = favoriteParams[index]
+ var showParam by remember { mutableStateOf(true) }
+ val dismissState = rememberSwipeToDismissBoxState()
+
+ LaunchedEffect(showParam, param) {
+ if (!showParam) {
+ delay(ANIMATION_DURATION.milliseconds)
+ onParamDeleteRequested(param)
+ }
+ }
+
+ AnimatedVisibility(
+ visible = showParam,
+ exit = shrinkVertically(
+ animationSpec = tween(durationMillis = ANIMATION_DURATION),
+ shrinkTowards = Alignment.Top
+ ) + fadeOut(animationSpec = tween(durationMillis = ANIMATION_DURATION))
+ ) {
+ val swipeBackgroundColor = MaterialTheme.colorScheme.errorContainer
+ val swipeContentColor = MaterialTheme.colorScheme.onErrorContainer
+
+ SwipeToDismissBox(
+ state = dismissState,
+ enableDismissFromStartToEnd = false,
+ enableDismissFromEndToStart = true,
+ onDismiss = { showParam = false },
+ backgroundContent = {
+ val color = when (dismissState.dismissDirection) {
+ SwipeToDismissBoxValue.EndToStart -> swipeBackgroundColor
+ else -> Color.Transparent
+ }
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(color)
+ .padding(horizontal = 16.dp),
+ contentAlignment = Alignment.CenterEnd
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.ic_delete_sweep),
+ contentDescription = "Delete",
+ tint = swipeContentColor
+ )
+ }
+ }
+ ) {
+ ParamFileRow(
+ modifier = Modifier.background(MaterialTheme.colorScheme.background),
+ param = param,
+ showFavoriteIcon = false,
+ onParamClicked = onParamClicked
+ )
+ }
+ }
+
+ if (index < favoriteParams.lastIndex && showParam) {
+ HorizontalDivider()
+ }
+ }
+ }
+}
+
+@Composable
+@Preview
+private fun FavoriteScreenContentPreview() {
+ val params = listOf(
+ UiKernelParam(
+ name = "vm.swappiness",
+ path = "/proc/sys/vm/swappiness",
+ value = "100",
+ isFavorite = true
+ ),
+ UiKernelParam(
+ name = "vm.overcommit_memory",
+ path = "/proc/sys/vm/overcommit_memory",
+ value = "1",
+ isFavorite = true
+ ),
+ UiKernelParam(
+ name = "vm.overcommit_ratio",
+ path = "/proc/sys/vm/overcommit_ratio",
+ value = "2",
+ isFavorite = true
+ )
+ )
+ SysctlGuiTheme(dynamicColor = true) {
+ Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
+ FavoritesScreenContent(
+ favoriteParams = params,
+ loading = false,
+ onParamClicked = {},
+ onParamDeleteRequested = {}
+ )
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/user/UserParamsViewModel.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/user/UserParamsViewModel.kt
new file mode 100644
index 0000000..cff23a5
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/user/UserParamsViewModel.kt
@@ -0,0 +1,69 @@
+package com.androidvip.sysctlgui.ui.user
+
+import androidx.lifecycle.viewModelScope
+import com.androidvip.sysctlgui.domain.usecase.GetUserParamsUseCase
+import com.androidvip.sysctlgui.domain.usecase.RemoveUserParamUseCase
+import com.androidvip.sysctlgui.domain.usecase.UpsertUserParamUseCase
+import com.androidvip.sysctlgui.helpers.UiKernelParamMapper
+import com.androidvip.sysctlgui.models.UiKernelParam
+import com.androidvip.sysctlgui.utils.BaseViewModel
+import kotlinx.coroutines.launch
+
+class UserParamsViewModel(
+ private val getUserParams: GetUserParamsUseCase,
+ private val removeParam: RemoveUserParamUseCase,
+ private val upsertParam: UpsertUserParamUseCase,
+) : BaseViewModel() {
+ override fun createInitialState() = UserParamsViewState()
+
+ private var mostRecentlyRemovedParam: UiKernelParam? = null
+
+ override fun onEvent(event: UserParamsViewEvent) {
+ when (event) {
+ is UserParamsViewEvent.ScreenLoaded -> loadParams(event.filterPredicate)
+
+ is UserParamsViewEvent.ParamClicked -> setEffect {
+ UserParamsViewEffect.ShowParamDetails(event.param)
+ }
+
+ is UserParamsViewEvent.ParamDeleteRequested -> removeParam(event.param)
+
+ is UserParamsViewEvent.ParamRestoreRequested -> {
+ mostRecentlyRemovedParam?.let { reAddParam(it) }
+ }
+ }
+ }
+
+ private fun loadParams(predicate: (UiKernelParam) -> Boolean) {
+ viewModelScope.launch {
+ val params = getUserParams()
+ .map(UiKernelParamMapper::map)
+ .filter(predicate)
+ setState { copy(userParams = params) }
+ }
+ }
+
+ private fun removeParam(param: UiKernelParam) {
+ viewModelScope.launch {
+ runCatching {
+ removeParam.invoke(param)
+ }.onSuccess {
+ setState { copy(userParams = userParams - param) }
+ setEffect { UserParamsViewEffect.ShowUndoSnackBar(param) }
+ mostRecentlyRemovedParam = param
+ }
+ }
+ }
+
+ private fun reAddParam(param: UiKernelParam) {
+ viewModelScope.launch {
+ runCatching {
+ val newId = upsertParam(param)
+ require(newId > 0)
+ }.onSuccess {
+ setState { copy(userParams = userParams + param) }
+ mostRecentlyRemovedParam = null
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/user/UserParamsViewState.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/user/UserParamsViewState.kt
new file mode 100644
index 0000000..c460907
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/user/UserParamsViewState.kt
@@ -0,0 +1,20 @@
+package com.androidvip.sysctlgui.ui.user
+
+import com.androidvip.sysctlgui.models.UiKernelParam
+
+data class UserParamsViewState(
+ val userParams: List = emptyList(),
+ val loading: Boolean = true
+)
+
+sealed interface UserParamsViewEvent {
+ data class ScreenLoaded(val filterPredicate: (UiKernelParam) -> Boolean) : UserParamsViewEvent
+ data class ParamClicked(val param: UiKernelParam) : UserParamsViewEvent
+ data class ParamDeleteRequested(val param: UiKernelParam) : UserParamsViewEvent
+ data object ParamRestoreRequested : UserParamsViewEvent
+}
+
+sealed interface UserParamsViewEffect {
+ data class ShowParamDetails(val param: UiKernelParam) : UserParamsViewEffect
+ data class ShowUndoSnackBar(val param: UiKernelParam) : UserParamsViewEffect
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/utils/DataBindingUtils.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/utils/DataBindingUtils.kt
deleted file mode 100644
index 289ec2a..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/utils/DataBindingUtils.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.androidvip.sysctlgui.utils
-
-import androidx.annotation.DrawableRes
-import androidx.appcompat.widget.AppCompatImageView
-import androidx.databinding.BindingAdapter
-
-@BindingAdapter("binding:srcCompatRes")
-fun AppCompatImageView.setImageResourceCompat(@DrawableRes res: Int) {
- setImageResource(res)
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/utils/KernelParamUtils.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/utils/KernelParamUtils.kt
deleted file mode 100644
index ef59fdb..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/utils/KernelParamUtils.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.androidvip.sysctlgui.utils
-
-import android.content.Context
-import android.net.Uri
-import com.androidvip.sysctlgui.domain.models.DomainKernelParam
-import com.google.gson.Gson
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import java.io.FileOutputStream
-
-// TODO: move to repository
-object KernelParamUtils {
-
- suspend fun writeParamsToUri(
- context: Context,
- params: List,
- uri: Uri
- ) = withContext(Dispatchers.IO) {
- return@withContext runCatching {
- context.contentResolver.openFileDescriptor(uri, "w")?.use {
- FileOutputStream(it.fileDescriptor).use { fileOutputStream ->
- fileOutputStream.write(Gson().toJson(params).toByteArray())
- }
- }
- true
- }.getOrElse {
- it.printStackTrace()
- false
- }
- }
-
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/utils/ThemeExt.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/utils/ThemeExt.kt
deleted file mode 100644
index f05ffeb..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/utils/ThemeExt.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.androidvip.sysctlgui.utils
-
-import android.app.Activity
-import androidx.compose.runtime.Composable
-import androidx.fragment.app.Fragment
-import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
-import com.androidvip.sysctlgui.domain.repository.AppPrefs
-import org.koin.android.ext.android.get
-
-@Composable
-fun Activity.ComposeTheme(content: @Composable () -> Unit) {
- val prefs: AppPrefs = get()
- SysctlGuiTheme(
- forceDark = prefs.forceDark,
- dynamicColor = prefs.dynamicColors,
- content = content
- )
-}
-
-@Composable
-fun Fragment.ComposeTheme(content: @Composable () -> Unit) {
- val prefs: AppPrefs = get()
- SysctlGuiTheme(
- forceDark = prefs.forceDark,
- dynamicColor = prefs.dynamicColors,
- content = content
- )
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/FavoriteWidgetParamUpdater.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/FavoriteWidgetParamUpdater.kt
index 82a3ac6..c2a67f0 100644
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/FavoriteWidgetParamUpdater.kt
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/FavoriteWidgetParamUpdater.kt
@@ -5,7 +5,6 @@ import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
-import android.os.Build
import com.androidvip.sysctlgui.data.repository.ParamsRepositoryImpl
class FavoriteWidgetParamUpdater(private val context: Context) :
@@ -19,11 +18,7 @@ class FavoriteWidgetParamUpdater(private val context: Context) :
if (idArray.isEmpty()) return
- val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
- } else {
- PendingIntent.FLAG_UPDATE_CURRENT
- }
+ val flags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
idArray.forEach {
intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(it))
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/FavoritesWidget.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/FavoritesWidget.kt
index a02911d..afec2c9 100644
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/FavoritesWidget.kt
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/FavoritesWidget.kt
@@ -6,19 +6,19 @@ import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
-import android.net.Uri
-import android.os.Build
import android.widget.RemoteViews
+import androidx.core.net.toUri
import com.androidvip.sysctlgui.R
-import com.androidvip.sysctlgui.data.mapper.DomainParamMapper
+import com.androidvip.sysctlgui.domain.enums.Actions
import com.androidvip.sysctlgui.domain.usecase.GetUserParamsUseCase
-import com.androidvip.sysctlgui.helpers.Actions
-import com.androidvip.sysctlgui.ui.params.edit.EditKernelParamActivity
+import com.androidvip.sysctlgui.helpers.UiKernelParamMapper
+import com.androidvip.sysctlgui.models.UiKernelParam
import com.androidvip.sysctlgui.ui.start.StartActivity
import com.androidvip.sysctlgui.widgets.FavoritesWidget.Companion.EDIT_PARAM_EXTRA
import kotlinx.coroutines.runBlocking
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
+import kotlin.collections.map
class FavoritesWidget : AppWidgetProvider(), KoinComponent {
private val getUserParamsUseCase: GetUserParamsUseCase by inject()
@@ -41,11 +41,10 @@ class FavoritesWidget : AppWidgetProvider(), KoinComponent {
}
runBlocking {
- val params = getUserParamsUseCase().filter {
- it.favorite
- }.map {
- DomainParamMapper.map(it)
- }.toMutableList()
+ val params = getUserParamsUseCase()
+ .filter { it.isFavorite }
+ .map(UiKernelParamMapper::map)
+ .toMutableList()
if (params.isEmpty()) return@runBlocking
@@ -53,8 +52,9 @@ class FavoritesWidget : AppWidgetProvider(), KoinComponent {
Intent(context, StartActivity::class.java).apply {
flags = FLAG_ACTIVITY_NEW_TASK
action = Actions.EditParam.name
- putExtra(EditKernelParamActivity.EXTRA_PARAM, param)
- putExtra(EditKernelParamActivity.EXTRA_EDIT_SAVED_PARAM, true)
+ // TODO
+ /*putExtra(EditKernelParamActivity.EXTRA_PARAM, param)
+ putExtra(EditKernelParamActivity.EXTRA_EDIT_SAVED_PARAM, true)*/
context.startActivity(this)
}
}
@@ -79,7 +79,7 @@ internal fun updateAppWidget(
) {
val intent = Intent(context, FavoritesWidgetService::class.java).apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
- data = Uri.parse(this.toUri(Intent.URI_INTENT_SCHEME))
+ data = toUri(Intent.URI_INTENT_SCHEME).toUri()
}
val views = RemoteViews(
@@ -96,13 +96,9 @@ internal fun updateAppWidget(
).run {
action = EDIT_PARAM_EXTRA
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
- data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
+ data = toUri(Intent.URI_INTENT_SCHEME).toUri()
- val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
- } else {
- PendingIntent.FLAG_UPDATE_CURRENT
- }
+ val flags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
PendingIntent.getBroadcast(context, 0, this, flags)
}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/FavoritesWidgetService.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/FavoritesWidgetService.kt
index 9b7b82d..2876d99 100644
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/FavoritesWidgetService.kt
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/FavoritesWidgetService.kt
@@ -6,8 +6,7 @@ import android.content.Intent
import android.widget.RemoteViews
import android.widget.RemoteViewsService
import com.androidvip.sysctlgui.R
-import com.androidvip.sysctlgui.data.mapper.DomainParamMapper
-import com.androidvip.sysctlgui.data.models.KernelParam
+import com.androidvip.sysctlgui.domain.models.KernelParam
import com.androidvip.sysctlgui.domain.usecase.GetUserParamsUseCase
import com.androidvip.sysctlgui.widgets.FavoritesWidget.Companion.EXTRA_ITEM
import kotlinx.coroutines.runBlocking
@@ -36,9 +35,7 @@ class FavoritesRemoteViewsFactory(
override fun onCreate() {
runBlocking {
params = getUserParamsUseCase().filter {
- it.favorite
- }.map {
- DomainParamMapper.map(it)
+ it.isFavorite
}.toMutableList()
}
}
@@ -51,11 +48,7 @@ class FavoritesRemoteViewsFactory(
override fun onDataSetChanged() {
runBlocking {
- params = getUserParamsUseCase().filter {
- it.favorite
- }.map {
- DomainParamMapper.map(it)
- }.toMutableList()
+ params = getUserParamsUseCase().filter { it.isFavorite }.toMutableList()
}
}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/work/StartUpWorker.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/work/StartUpWorker.kt
index 708f1d5..05e18e0 100644
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/work/StartUpWorker.kt
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/work/StartUpWorker.kt
@@ -17,9 +17,8 @@ import androidx.work.WorkerParameters
import com.androidvip.sysctlgui.R
import com.androidvip.sysctlgui.data.utils.RootUtils
import com.androidvip.sysctlgui.domain.repository.AppPrefs
-import com.androidvip.sysctlgui.domain.usecase.ApplyParamsUseCase
+import com.androidvip.sysctlgui.domain.usecase.ApplyParamUseCase
import com.androidvip.sysctlgui.domain.usecase.GetUserParamsUseCase
-import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
@@ -36,8 +35,8 @@ class StartUpWorker(
private val workerContext = Dispatchers.Default
private val appPrefs: AppPrefs by inject()
private val rootUtils: RootUtils by inject()
- private val getUserParamsUseCase: GetUserParamsUseCase by inject()
- private val applyParamsUseCase: ApplyParamsUseCase by inject()
+ private val getUserParams: GetUserParamsUseCase by inject()
+ private val applyParam: ApplyParamUseCase by inject()
private val notificationManager: NotificationManagerCompat
get() = NotificationManagerCompat.from(context)
@@ -63,16 +62,16 @@ class StartUpWorker(
}
private suspend fun applyConfig(builder: NotificationCompat.Builder) {
- getUserParamsUseCase().forEach {
- builder.setContentText(it.toString())
+ getUserParams().forEach { param ->
+ builder.setContentText(param.toString())
notifyIfPossible(builder)
delay(250L)
- applyParamsUseCase(it)
+ applyParam(param)
}
}
- private suspend fun checkRequirements() = withContext(workerContext) {
- appPrefs.runOnStartUp && Shell.rootAccess()
+ private suspend fun checkRequirements(): Boolean {
+ return appPrefs.runOnStartUp && rootUtils.isRootAvailable()
}
private suspend inline fun showNotificationAndThen(
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/work/TaskerWorker.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/work/TaskerWorker.kt
index 410750e..c081e78 100644
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/work/TaskerWorker.kt
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/work/TaskerWorker.kt
@@ -10,7 +10,7 @@ import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.androidvip.sysctlgui.R
import com.androidvip.sysctlgui.domain.repository.AppPrefs
-import com.androidvip.sysctlgui.domain.usecase.ApplyParamsUseCase
+import com.androidvip.sysctlgui.domain.usecase.ApplyParamUseCase
import com.androidvip.sysctlgui.domain.usecase.GetUserParamsUseCase
import com.androidvip.sysctlgui.receivers.TaskerReceiver
import com.androidvip.sysctlgui.toast
@@ -30,8 +30,8 @@ class TaskerWorker(
private val mainContext = Dispatchers.Main + SupervisorJob()
private val workerContext = Dispatchers.IO
private val appPrefs: AppPrefs by inject()
- private val getUserParamsUseCase: GetUserParamsUseCase by inject()
- private val applyParamsUseCase: ApplyParamsUseCase by inject()
+ private val getUserParams: GetUserParamsUseCase by inject()
+ private val applyParam: ApplyParamUseCase by inject()
override suspend fun doWork(): Result {
withContext(workerContext) {
@@ -54,16 +54,16 @@ class TaskerWorker(
}
private suspend fun applyParams(listNumber: Int) {
- val params = getUserParamsUseCase()
+ val params = getUserParams()
when (listNumber) {
Consts.LIST_NUMBER_PRIMARY_TASKER,
- Consts.LIST_NUMBER_SECONDARY_TASKER -> params.filter { it.taskerParam }
- Consts.LIST_NUMBER_FAVORITES -> params.filter { it.favorite }
+ Consts.LIST_NUMBER_SECONDARY_TASKER -> params.filter { it.isTaskerParam }
+ Consts.LIST_NUMBER_FAVORITES -> params.filter { it.isFavorite }
Consts.LIST_NUMBER_APPLY_ON_BOOT -> params
else -> emptyList()
}.forEach {
- applyParamsUseCase(it)
+ applyParam(it)
}
}
diff --git a/app/src/main/res/drawable-night/ic_launcher_background.xml b/app/src/main/res/drawable-night/ic_launcher_background.xml
deleted file mode 100644
index 5314cbc..0000000
--- a/app/src/main/res/drawable-night/ic_launcher_background.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/circle_file.xml b/app/src/main/res/drawable/circle_file.xml
deleted file mode 100644
index 5e09b4b..0000000
--- a/app/src/main/res/drawable/circle_file.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable/circle_folder.xml b/app/src/main/res/drawable/circle_folder.xml
deleted file mode 100644
index 6c8c6fc..0000000
--- a/app/src/main/res/drawable/circle_folder.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/fast_scroll_thumb.xml b/app/src/main/res/drawable/fast_scroll_thumb.xml
deleted file mode 100644
index 5735284..0000000
--- a/app/src/main/res/drawable/fast_scroll_thumb.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
- -
-
-
-
-
- -
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable/fast_scroll_track.xml b/app/src/main/res/drawable/fast_scroll_track.xml
deleted file mode 100644
index c6471b0..0000000
--- a/app/src/main/res/drawable/fast_scroll_track.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_action_tasker.xml b/app/src/main/res/drawable/ic_action_tasker.xml
deleted file mode 100644
index 59e6a77..0000000
--- a/app/src/main/res/drawable/ic_action_tasker.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_arrow_upward.xml b/app/src/main/res/drawable/ic_arrow_upward.xml
new file mode 100644
index 0000000..8eff018
--- /dev/null
+++ b/app/src/main/res/drawable/ic_arrow_upward.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml
deleted file mode 100644
index 8d6dbeb..0000000
--- a/app/src/main/res/drawable/ic_close.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_config.xml b/app/src/main/res/drawable/ic_config.xml
deleted file mode 100644
index a69f689..0000000
--- a/app/src/main/res/drawable/ic_config.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_delete_sweep.xml b/app/src/main/res/drawable/ic_delete_sweep.xml
index 940a568..4ff2112 100644
--- a/app/src/main/res/drawable/ic_delete_sweep.xml
+++ b/app/src/main/res/drawable/ic_delete_sweep.xml
@@ -1,8 +1,12 @@
-
-
\ No newline at end of file
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_documentation.xml b/app/src/main/res/drawable/ic_documentation.xml
index d9e0a7e..428b620 100644
--- a/app/src/main/res/drawable/ic_documentation.xml
+++ b/app/src/main/res/drawable/ic_documentation.xml
@@ -1,7 +1,13 @@
-
-
\ No newline at end of file
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_edit_outline.xml b/app/src/main/res/drawable/ic_edit_outline.xml
deleted file mode 100644
index 2271737..0000000
--- a/app/src/main/res/drawable/ic_edit_outline.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_export.xml b/app/src/main/res/drawable/ic_export.xml
new file mode 100644
index 0000000..248a716
--- /dev/null
+++ b/app/src/main/res/drawable/ic_export.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_favorite.xml b/app/src/main/res/drawable/ic_favorite.xml
new file mode 100644
index 0000000..a55d082
--- /dev/null
+++ b/app/src/main/res/drawable/ic_favorite.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_favorite_outlined.xml b/app/src/main/res/drawable/ic_favorite_outlined.xml
new file mode 100644
index 0000000..75654f6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_favorite_outlined.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_file.xml b/app/src/main/res/drawable/ic_file.xml
new file mode 100644
index 0000000..1655262
--- /dev/null
+++ b/app/src/main/res/drawable/ic_file.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_file_outline.xml b/app/src/main/res/drawable/ic_file_outline.xml
deleted file mode 100644
index f925ee2..0000000
--- a/app/src/main/res/drawable/ic_file_outline.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_folder.xml b/app/src/main/res/drawable/ic_folder.xml
new file mode 100644
index 0000000..769af8f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_folder.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_folder_outline.xml b/app/src/main/res/drawable/ic_folder_outline.xml
deleted file mode 100644
index 512eb78..0000000
--- a/app/src/main/res/drawable/ic_folder_outline.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_heart_broken.xml b/app/src/main/res/drawable/ic_heart_broken.xml
new file mode 100644
index 0000000..8b4943e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_heart_broken.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_history.xml b/app/src/main/res/drawable/ic_history.xml
new file mode 100644
index 0000000..041a166
--- /dev/null
+++ b/app/src/main/res/drawable/ic_history.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_import.xml b/app/src/main/res/drawable/ic_import.xml
new file mode 100644
index 0000000..4ad7f44
--- /dev/null
+++ b/app/src/main/res/drawable/ic_import.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_info_outline.xml b/app/src/main/res/drawable/ic_info_outline.xml
deleted file mode 100644
index 5aa45fb..0000000
--- a/app/src/main/res/drawable/ic_info_outline.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
index 2c34399..5314cbc 100644
--- a/app/src/main/res/drawable/ic_launcher_background.xml
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -6,5 +6,5 @@
android:width="108dp"
android:height="108dp" />
-
+
diff --git a/app/src/main/res/drawable/ic_more_vert.xml b/app/src/main/res/drawable/ic_more_vert.xml
deleted file mode 100644
index 0f7ce68..0000000
--- a/app/src/main/res/drawable/ic_more_vert.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_name.xml b/app/src/main/res/drawable/ic_name.xml
deleted file mode 100644
index 91e26d4..0000000
--- a/app/src/main/res/drawable/ic_name.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_open_in_browser.xml b/app/src/main/res/drawable/ic_open_in_browser.xml
new file mode 100644
index 0000000..5a2c383
--- /dev/null
+++ b/app/src/main/res/drawable/ic_open_in_browser.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml
deleted file mode 100644
index 22f41a3..0000000
--- a/app/src/main/res/drawable/ic_search.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_settings_outline.xml b/app/src/main/res/drawable/ic_settings_outline.xml
deleted file mode 100644
index 711fde8..0000000
--- a/app/src/main/res/drawable/ic_settings_outline.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_tasker.xml b/app/src/main/res/drawable/ic_tasker.xml
new file mode 100644
index 0000000..905fb28
--- /dev/null
+++ b/app/src/main/res/drawable/ic_tasker.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_tasker_outlined.xml b/app/src/main/res/drawable/ic_tasker_outlined.xml
new file mode 100644
index 0000000..165e5d7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_tasker_outlined.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/app/src/main/res/layout-land/activity_main2.xml b/app/src/main/res/layout-land/activity_main2.xml
deleted file mode 100644
index 8a6a6bd..0000000
--- a/app/src/main/res/layout-land/activity_main2.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/activity_main2.xml b/app/src/main/res/layout/activity_main2.xml
deleted file mode 100644
index 7d94f7e..0000000
--- a/app/src/main/res/layout/activity_main2.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/activity_splash.xml b/app/src/main/res/layout/activity_splash.xml
index d270c1f..ba0c7bc 100644
--- a/app/src/main/res/layout/activity_splash.xml
+++ b/app/src/main/res/layout/activity_splash.xml
@@ -25,7 +25,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/d16"
- android:fontFamily="sans-serif-medium"
android:gravity="center_horizontal"
android:text="@string/splash_status_checking_root"
android:textAppearance="?textAppearanceHeadline5" />
diff --git a/app/src/main/res/layout/activity_tasker_plugin.xml b/app/src/main/res/layout/activity_tasker_plugin.xml
index 1981a4a..5a11583 100644
--- a/app/src/main/res/layout/activity_tasker_plugin.xml
+++ b/app/src/main/res/layout/activity_tasker_plugin.xml
@@ -13,8 +13,10 @@
android:layout_marginEnd="@dimen/d16"
android:layout_marginBottom="@dimen/d16"
android:text="@string/done"
- app:backgroundTint="?colorSecondary"
- app:icon="@drawable/ic_check" />
+ android:textColor="?colorOnTertiary"
+ app:backgroundTint="?colorTertiary"
+ app:icon="@drawable/ic_check"
+ app:iconTint="?colorOnPrimary" />
+ android:textColor="?colorPrimary"
+ android:textStyle="bold" />
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/list_item_settings.xml b/app/src/main/res/layout/list_item_settings.xml
deleted file mode 100644
index fa3909c..0000000
--- a/app/src/main/res/layout/list_item_settings.xml
+++ /dev/null
@@ -1,74 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/settings_activity.xml b/app/src/main/res/layout/settings_activity.xml
deleted file mode 100644
index 01ee430..0000000
--- a/app/src/main/res/layout/settings_activity.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/menu/menu_browse_params.xml b/app/src/main/res/menu/menu_browse_params.xml
deleted file mode 100644
index e78ed41..0000000
--- a/app/src/main/res/menu/menu_browse_params.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml
deleted file mode 100644
index db1f6b9..0000000
--- a/app/src/main/res/menu/menu_main.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
diff --git a/app/src/main/res/menu/menu_main_search.xml b/app/src/main/res/menu/menu_main_search.xml
deleted file mode 100644
index 46a8dac..0000000
--- a/app/src/main/res/menu/menu_main_search.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
diff --git a/app/src/main/res/menu/menu_search.xml b/app/src/main/res/menu/menu_search.xml
deleted file mode 100644
index 392e261..0000000
--- a/app/src/main/res/menu/menu_search.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
\ No newline at end of file
diff --git a/app/src/main/res/menu/nav_main.xml b/app/src/main/res/menu/nav_main.xml
deleted file mode 100644
index 58fdaf5..0000000
--- a/app/src/main/res/menu/nav_main.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
diff --git a/app/src/main/res/menu/popup_manage_params.xml b/app/src/main/res/menu/popup_manage_params.xml
deleted file mode 100644
index 6d3116c..0000000
--- a/app/src/main/res/menu/popup_manage_params.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
\ No newline at end of file
diff --git a/app/src/main/res/navigation/main_navigation.xml b/app/src/main/res/navigation/main_navigation.xml
deleted file mode 100644
index 5647a7f..0000000
--- a/app/src/main/res/navigation/main_navigation.xml
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/values-land/integers.xml b/app/src/main/res/values-land/integers.xml
deleted file mode 100644
index 169ae66..0000000
--- a/app/src/main/res/values-land/integers.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- 2
-
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index e3152f0..bf64536 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -96,7 +96,7 @@
Restaurar a sua cópia de segurança anterior
Opções de exportação
Parâmetros exportados com sucesso
- Para na exportação de parâmetros: nenhum parâmetro encontrado
+ Falha: nenhum parâmetro encontrado
A exportação de parâmetros falhou devido a um erro do armazenamento
Falha ao exportar parâmetros
Importar, exportar ou fazer backup dos parâmetros do kernel
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 84ea67b..02d9158 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -1,5 +1,4 @@
- Sysctl GUI
Kök erişimi denetleniyor
Busybox kontrol ediliyor
Eski veritabanı şeması kontrol ediliyor
diff --git a/app/src/main/res/values-v14/dimens.xml b/app/src/main/res/values-v14/dimens.xml
deleted file mode 100644
index 212af5f..0000000
--- a/app/src/main/res/values-v14/dimens.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
- 0dp
-
-
diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml
deleted file mode 100644
index 62657f3..0000000
--- a/app/src/main/res/values/integers.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- 1
-
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b334345..b22598b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -98,7 +98,7 @@
Backup all current runtime parameters. Please note that not all of these can be reapplied back once restored.
Failed to export parameters
Parameter export failed due to an IO error
- Parameter export failed: no parameter to export
+ Failed: no parameter found
Export options
Parameters successfully exported
Import, export or back up kernel parameters
@@ -119,4 +119,11 @@
Use dynamic colors (Monet theme) when available
Force dark theme
Force dark theme when available
+ Favorites
+ Presets
+ File creation cancelled or failed
+ File picking cancelled or failed
+ There was an error while opening the file
+ There was an error while processing the file\n
+ Param deleted: %s
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
deleted file mode 100644
index 6238da5..0000000
--- a/app/src/main/res/xml/preferences.xml
+++ /dev/null
@@ -1,86 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/buildSrc/src/main/kotlin/AndroidX.kt b/buildSrc/src/main/kotlin/AndroidX.kt
deleted file mode 100644
index 52d512d..0000000
--- a/buildSrc/src/main/kotlin/AndroidX.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-object AndroidX {
- const val activity = "androidx.activity:activity-ktx:1.7.2"
- const val appCompat = "androidx.appcompat:appcompat:1.6.1"
- const val constraintLayout = "androidx.constraintlayout:constraintlayout:2.1.4"
- const val core = "androidx.core:core-ktx:1.10.1"
- const val splashScreen = "androidx.core:core-splashscreen:1.0.1"
-
- private const val lifecycleVersion = "2.6.1"
- const val lifecycleLiveData = "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
- const val lifecycleViewModel = "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
-
- const val preference = "androidx.preference:preference-ktx:1.2.0"
- const val swipeRefreshLayout = "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
-
- private const val navigationVersion = "2.6.0"
- const val navigationFragment = "androidx.navigation:navigation-fragment-ktx:$navigationVersion"
- const val navigationUi = "androidx.navigation:navigation-ui-ktx:$navigationVersion"
-
- private const val roomVersion = "2.5.2"
- const val room = "androidx.room:room-ktx:$roomVersion"
- const val roomRuntime = "androidx.room:room-runtime:$roomVersion"
- const val roomCompiler = "androidx.room:room-compiler:$roomVersion"
-
- private const val workManagerVersion = "2.9.0"
- const val workManager = "androidx.work:work-runtime-ktx:$workManagerVersion"
-}
diff --git a/buildSrc/src/main/kotlin/BuildPlugins.kt b/buildSrc/src/main/kotlin/BuildPlugins.kt
deleted file mode 100644
index 168cf36..0000000
--- a/buildSrc/src/main/kotlin/BuildPlugins.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-object BuildPlugins {
- private const val agpVersion = "7.4.2"
- const val gradle = "com.android.tools.build:gradle:$agpVersion"
-
- private const val kotlinVersion = "1.9.24"
- const val kotlin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
-}
diff --git a/buildSrc/src/main/kotlin/Compose.kt b/buildSrc/src/main/kotlin/Compose.kt
deleted file mode 100644
index 6ee5046..0000000
--- a/buildSrc/src/main/kotlin/Compose.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-object Compose {
- const val BoM = "androidx.compose:compose-bom:2024.06.00"
- const val kotlinCompilerExtensionVersion = "1.5.14"
- const val material3 = "androidx.compose.material3:material3"
- const val material = "androidx.compose.material:material"
- const val uiTooling = "androidx.compose.ui:ui-tooling"
- const val activity = "androidx.activity:activity-compose:1.6.1"
-}
diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt
deleted file mode 100644
index a6e42e6..0000000
--- a/buildSrc/src/main/kotlin/Dependencies.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-
-object Dependencies {
- private const val koinVersion = "3.4.2"
- const val koinAndroid = "io.insert-koin:koin-android:$koinVersion"
- const val koinCore = "io.insert-koin:koin-core:$koinVersion"
-
- const val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"
- const val tapTargetView = "com.getkeepsafe.taptargetview:taptargetview:1.13.3"
- const val libSuCore = "com.github.topjohnwu.libsu:core:2.5.1"
- const val libSuIo = "com.github.topjohnwu.libsu:io:2.5.1"
- const val liveEvent = "com.github.hadilq:live-event:1.3.0"
-}
diff --git a/buildSrc/src/main/kotlin/Google.kt b/buildSrc/src/main/kotlin/Google.kt
deleted file mode 100644
index 6ab7f05..0000000
--- a/buildSrc/src/main/kotlin/Google.kt
+++ /dev/null
@@ -1,4 +0,0 @@
-object Google {
- const val material = "com.google.android.material:material:1.9.0"
- const val gson = "com.google.code.gson:gson:2.8.9"
-}
diff --git a/buildSrc/src/main/kotlin/Modules.kt b/buildSrc/src/main/kotlin/Modules.kt
deleted file mode 100644
index d81ddd5..0000000
--- a/buildSrc/src/main/kotlin/Modules.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-object Modules {
- const val main = ":app"
- const val domain = ":domain"
- const val data = ":data"
- const val utils = ":common:utils"
- const val design = ":common:design"
-}
diff --git a/common/design/build.gradle.kts b/common/design/build.gradle.kts
index d9ddb38..7208696 100644
--- a/common/design/build.gradle.kts
+++ b/common/design/build.gradle.kts
@@ -51,11 +51,7 @@ dependencies {
api(libs.androidx.material.icons.core)
api(libs.androidx.window)
- api(AndroidX.constraintLayout)
- api(AndroidX.swipeRefreshLayout)
- api(Compose.material)
- implementation(AndroidX.splashScreen)
- implementation(Google.material)
+ api(libs.material)
androidTestApi(platform(libs.androidx.compose.bom))
debugApi(libs.androidx.ui.tooling)
diff --git a/common/design/src/main/java/com/androidvip/sysctlgui/design/BaseBottomSheetFragment.kt b/common/design/src/main/java/com/androidvip/sysctlgui/design/BaseBottomSheetFragment.kt
deleted file mode 100644
index c237184..0000000
--- a/common/design/src/main/java/com/androidvip/sysctlgui/design/BaseBottomSheetFragment.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-package com.androidvip.sysctlgui.design
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.core.view.ViewCompat
-import androidx.viewbinding.ViewBinding
-import com.google.android.material.bottomsheet.BottomSheetBehavior
-import com.google.android.material.bottomsheet.BottomSheetDialogFragment
-import com.google.android.material.shape.MaterialShapeDrawable
-import com.google.android.material.shape.ShapeAppearanceModel
-
-abstract class BaseBottomSheetFragment : BottomSheetDialogFragment() {
-
- lateinit var binding: Binding
-
- abstract fun setViewBinding(inflater: LayoutInflater, container: ViewGroup?): Binding
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View? {
- binding = this.setViewBinding(inflater, container)
- return binding.root
- }
-
- override fun onStart() {
- super.onStart()
- val bottomSheetBehavior = BottomSheetBehavior.from(view?.parent as? View ?: return)
- bottomSheetBehavior.addBottomSheetCallback(
- object : BottomSheetBehavior.BottomSheetCallback() {
- override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit
-
- override fun onStateChanged(bottomSheet: View, newState: Int) {
- when (newState) {
- BottomSheetBehavior.STATE_EXPANDED -> {
- val shape = createMaterialShapeDrawable(bottomSheet)
- ViewCompat.setBackground(bottomSheet, shape)
- }
- BottomSheetBehavior.STATE_HIDDEN -> dismiss()
- else -> Unit
- }
- }
- }
- )
- }
-
- private fun createMaterialShapeDrawable(bottomSheet: View): MaterialShapeDrawable {
- val shapeAppearanceModel = ShapeAppearanceModel.builder(
- context,
- 0,
- R.style.ShapeAppearance_SysctlGui_BottomSheet
- ).build()
-
- val currentShape = bottomSheet.background as MaterialShapeDrawable
- return MaterialShapeDrawable(shapeAppearanceModel).apply {
- initializeElevationOverlay(context)
- fillColor = currentShape.fillColor
- tintList = currentShape.tintList
- elevation = currentShape.elevation
- strokeWidth = currentShape.strokeWidth
- strokeColor = currentShape.strokeColor
- }
- }
-}
diff --git a/common/design/src/main/java/com/androidvip/sysctlgui/design/DesignResources.kt b/common/design/src/main/java/com/androidvip/sysctlgui/design/DesignResources.kt
deleted file mode 100644
index 2a29b4f..0000000
--- a/common/design/src/main/java/com/androidvip/sysctlgui/design/DesignResources.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.androidvip.sysctlgui.design
-
-typealias DesignIds = com.androidvip.sysctlgui.design.R.id
-typealias DesignLayouts = com.androidvip.sysctlgui.design.R.layout
-typealias DesignStyles = com.androidvip.sysctlgui.design.R.style
diff --git a/common/design/src/main/java/com/androidvip/sysctlgui/design/ModalBottomSheet.kt b/common/design/src/main/java/com/androidvip/sysctlgui/design/ModalBottomSheet.kt
deleted file mode 100644
index 24968e5..0000000
--- a/common/design/src/main/java/com/androidvip/sysctlgui/design/ModalBottomSheet.kt
+++ /dev/null
@@ -1,88 +0,0 @@
-package com.androidvip.sysctlgui.design
-
-import android.content.Context
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.core.os.bundleOf
-import com.androidvip.sysctlgui.design.databinding.ModalBottomSheetBinding
-
-open class ModalBottomSheet : BaseBottomSheetFragment() {
-
- private var listener: EventListener? = null
-
- override fun setViewBinding(
- inflater: LayoutInflater,
- container: ViewGroup?
- ): ModalBottomSheetBinding = ModalBottomSheetBinding.inflate(inflater)
-
- override fun onAttach(context: Context) {
- super.onAttach(context)
- val parent = parentFragment
- if (parent != null) {
- listener = parent as? EventListener
- }
-
- if (listener == null) {
- listener = context as? EventListener
- }
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- binding.sheetTitle.text = arguments?.getString(ARG_TITLE).orEmpty()
- binding.sheetDescription.text = arguments?.getCharSequence(ARG_MESSAGE) ?: ""
-
- arguments?.getString(ARG_POSITIVE_BUTTON_TEXT)?.let {
- binding.positiveButton.apply {
- text = it
- visibility = View.VISIBLE
- setOnClickListener {
- listener?.onContinuePressed()
- dismiss()
- }
- }
- }
-
- arguments?.getString(ARG_NEGATIVE_BUTTON_TEXT)?.let {
- binding.negativeButton.apply {
- text = it
- visibility = View.VISIBLE
- setOnClickListener {
- listener?.onCancelPressed()
- dismiss()
- }
- }
- }
- }
-
- interface EventListener {
- fun onContinuePressed()
- fun onCancelPressed()
- }
-
- companion object {
- private const val ARG_TITLE = "title"
- private const val ARG_MESSAGE = "message"
- private const val ARG_POSITIVE_BUTTON_TEXT = "positiveButtonText"
- private const val ARG_NEGATIVE_BUTTON_TEXT = "negativeButtonText"
-
- fun newInstance(
- title: String,
- message: CharSequence,
- positiveButtonText: String? = null,
- negativeButtonText: String? = null
- ): ModalBottomSheet {
- return ModalBottomSheet().apply {
- arguments = bundleOf(
- ARG_TITLE to title,
- ARG_MESSAGE to message,
- ARG_POSITIVE_BUTTON_TEXT to positiveButtonText,
- ARG_NEGATIVE_BUTTON_TEXT to negativeButtonText
- )
- }
- }
- }
-}
diff --git a/common/design/src/main/java/com/androidvip/sysctlgui/design/theme/Color.kt b/common/design/src/main/java/com/androidvip/sysctlgui/design/theme/Color.kt
index 14a01ef..e8fc935 100644
--- a/common/design/src/main/java/com/androidvip/sysctlgui/design/theme/Color.kt
+++ b/common/design/src/main/java/com/androidvip/sysctlgui/design/theme/Color.kt
@@ -2,88 +2,218 @@ package com.androidvip.sysctlgui.design.theme
import androidx.compose.ui.graphics.Color
-val md_theme_light_primary = Color(0xFF006685)
-val md_theme_light_onPrimary = Color(0xFFFFFFFF)
-val md_theme_light_primaryContainer = Color(0xFFBFE9FF)
-val md_theme_light_onPrimaryContainer = Color(0xFF001F2A)
-val md_theme_light_secondary = Color(0xFF4D616C)
-val md_theme_light_onSecondary = Color(0xFFFFFFFF)
-val md_theme_light_secondaryContainer = Color(0xFFD0E6F3)
-val md_theme_light_onSecondaryContainer = Color(0xFF081E27)
-val md_theme_light_tertiary = Color(0xFF5E5A7D)
-val md_theme_light_onTertiary = Color(0xFFFFFFFF)
-val md_theme_light_tertiaryContainer = Color(0xFFE4DFFF)
-val md_theme_light_onTertiaryContainer = Color(0xFF1A1836)
-val md_theme_light_error = Color(0xFFBA1A1A)
-val md_theme_light_errorContainer = Color(0xFFFFDAD6)
-val md_theme_light_onError = Color(0xFFFFFFFF)
-val md_theme_light_onErrorContainer = Color(0xFF410002)
-val md_theme_light_background = Color(0xFFFBFCFE)
-val md_theme_light_onBackground = Color(0xFF191C1E)
-val md_theme_light_surface = Color(0xFFFBFCFE)
-val md_theme_light_onSurface = Color(0xFF191C1E)
-val md_theme_light_surfaceVariant = Color(0xFFDCE3E9)
-val md_theme_light_onSurfaceVariant = Color(0xFF40484C)
-val md_theme_light_outline = Color(0xFF70787D)
-val md_theme_light_inverseOnSurface = Color(0xFFF0F1F3)
-val md_theme_light_inverseSurface = Color(0xFF2E3133)
-val md_theme_light_inversePrimary = Color(0xFF6DD2FF)
-val md_theme_light_shadow = Color(0xFF000000)
-val md_theme_light_surfaceTint = Color(0xFF006685)
-val md_theme_light_outlineVariant = Color(0xFFC0C8CD)
-val md_theme_light_scrim = Color(0xFF000000)
+val primaryLight = Color(0xFF4B5C92)
+val onPrimaryLight = Color(0xFFFFFFFF)
+val primaryContainerLight = Color(0xFFDBE1FF)
+val onPrimaryContainerLight = Color(0xFF334478)
+val secondaryLight = Color(0xFF595E72)
+val onSecondaryLight = Color(0xFFFFFFFF)
+val secondaryContainerLight = Color(0xFFDDE1F9)
+val onSecondaryContainerLight = Color(0xFF414659)
+val tertiaryLight = Color(0xFF1D6B50)
+val onTertiaryLight = Color(0xFFFFFFFF)
+val tertiaryContainerLight = Color(0xFFA7F2D0)
+val onTertiaryContainerLight = Color(0xFF00513A)
+val errorLight = Color(0xFFBA1A1A)
+val onErrorLight = Color(0xFFFFFFFF)
+val errorContainerLight = Color(0xFFFFDAD6)
+val onErrorContainerLight = Color(0xFF93000A)
+val backgroundLight = Color(0xFFFAF8FF)
+val onBackgroundLight = Color(0xFF1A1B21)
+val surfaceLight = Color(0xFFFAF8FF)
+val onSurfaceLight = Color(0xFF1A1B21)
+val surfaceVariantLight = Color(0xFFE2E2EC)
+val onSurfaceVariantLight = Color(0xFF45464F)
+val outlineLight = Color(0xFF757680)
+val outlineVariantLight = Color(0xFFC5C6D0)
+val scrimLight = Color(0xFF000000)
+val inverseSurfaceLight = Color(0xFF2F3036)
+val inverseOnSurfaceLight = Color(0xFFF1F0F7)
+val inversePrimaryLight = Color(0xFFB4C5FF)
+val surfaceDimLight = Color(0xFFDAD9E0)
+val surfaceBrightLight = Color(0xFFFAF8FF)
+val surfaceContainerLowestLight = Color(0xFFFFFFFF)
+val surfaceContainerLowLight = Color(0xFFF4F3FA)
+val surfaceContainerLight = Color(0xFFEEEDF4)
+val surfaceContainerHighLight = Color(0xFFE8E7EF)
+val surfaceContainerHighestLight = Color(0xFFE3E2E9)
-val md_theme_dark_primary = Color(0xFF6DD2FF)
-val md_theme_dark_onPrimary = Color(0xFF003547)
-val md_theme_dark_primaryContainer = Color(0xFF004D65)
-val md_theme_dark_onPrimaryContainer = Color(0xFFBFE9FF)
-val md_theme_dark_secondary = Color(0xFFB4CAD6)
-val md_theme_dark_onSecondary = Color(0xFF1F333D)
-val md_theme_dark_secondaryContainer = Color(0xFF364954)
-val md_theme_dark_onSecondaryContainer = Color(0xFFD0E6F3)
-val md_theme_dark_tertiary = Color(0xFFC7C2EA)
-val md_theme_dark_onTertiary = Color(0xFF2F2D4C)
-val md_theme_dark_tertiaryContainer = Color(0xFF464364)
-val md_theme_dark_onTertiaryContainer = Color(0xFFE4DFFF)
-val md_theme_dark_error = Color(0xFFFFB4AB)
-val md_theme_dark_errorContainer = Color(0xFF93000A)
-val md_theme_dark_onError = Color(0xFF690005)
-val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
-val md_theme_dark_background = Color(0xFF191C1E)
-val md_theme_dark_onBackground = Color(0xFFE1E2E5)
-val md_theme_dark_surface = Color(0xFF191C1E)
-val md_theme_dark_onSurface = Color(0xFFE1E2E5)
-val md_theme_dark_surfaceVariant = Color(0xFF40484C)
-val md_theme_dark_onSurfaceVariant = Color(0xFFC0C8CD)
-val md_theme_dark_outline = Color(0xFF8A9297)
-val md_theme_dark_inverseOnSurface = Color(0xFF191C1E)
-val md_theme_dark_inverseSurface = Color(0xFFE1E2E5)
-val md_theme_dark_inversePrimary = Color(0xFF006685)
-val md_theme_dark_shadow = Color(0xFF000000)
-val md_theme_dark_surfaceTint = Color(0xFF6DD2FF)
-val md_theme_dark_outlineVariant = Color(0xFF40484C)
-val md_theme_dark_scrim = Color(0xFF000000)
+val primaryLightMediumContrast = Color(0xFF213367)
+val onPrimaryLightMediumContrast = Color(0xFFFFFFFF)
+val primaryContainerLightMediumContrast = Color(0xFF5A6BA2)
+val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF)
+val secondaryLightMediumContrast = Color(0xFF303648)
+val onSecondaryLightMediumContrast = Color(0xFFFFFFFF)
+val secondaryContainerLightMediumContrast = Color(0xFF676C81)
+val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF)
+val tertiaryLightMediumContrast = Color(0xFF003F2C)
+val onTertiaryLightMediumContrast = Color(0xFFFFFFFF)
+val tertiaryContainerLightMediumContrast = Color(0xFF307A5E)
+val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF)
+val errorLightMediumContrast = Color(0xFF740006)
+val onErrorLightMediumContrast = Color(0xFFFFFFFF)
+val errorContainerLightMediumContrast = Color(0xFFCF2C27)
+val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF)
+val backgroundLightMediumContrast = Color(0xFFFAF8FF)
+val onBackgroundLightMediumContrast = Color(0xFF1A1B21)
+val surfaceLightMediumContrast = Color(0xFFFAF8FF)
+val onSurfaceLightMediumContrast = Color(0xFF101116)
+val surfaceVariantLightMediumContrast = Color(0xFFE2E2EC)
+val onSurfaceVariantLightMediumContrast = Color(0xFF34363E)
+val outlineLightMediumContrast = Color(0xFF51525B)
+val outlineVariantLightMediumContrast = Color(0xFF6B6C75)
+val scrimLightMediumContrast = Color(0xFF000000)
+val inverseSurfaceLightMediumContrast = Color(0xFF2F3036)
+val inverseOnSurfaceLightMediumContrast = Color(0xFFF1F0F7)
+val inversePrimaryLightMediumContrast = Color(0xFFB4C5FF)
+val surfaceDimLightMediumContrast = Color(0xFFC6C6CD)
+val surfaceBrightLightMediumContrast = Color(0xFFFAF8FF)
+val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF)
+val surfaceContainerLowLightMediumContrast = Color(0xFFF4F3FA)
+val surfaceContainerLightMediumContrast = Color(0xFFE8E7EF)
+val surfaceContainerHighLightMediumContrast = Color(0xFFDDDCE3)
+val surfaceContainerHighestLightMediumContrast = Color(0xFFD2D1D8)
-val green_200 = Color(0xFF80E4A9)
-val green_500 = Color(0xFF00C853)
-val green_800 = Color(0xFF00B439)
+val primaryLightHighContrast = Color(0xFF15295C)
+val onPrimaryLightHighContrast = Color(0xFFFFFFFF)
+val primaryContainerLightHighContrast = Color(0xFF35477B)
+val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF)
+val secondaryLightHighContrast = Color(0xFF262C3D)
+val onSecondaryLightHighContrast = Color(0xFFFFFFFF)
+val secondaryContainerLightHighContrast = Color(0xFF43485C)
+val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF)
+val tertiaryLightHighContrast = Color(0xFF003323)
+val onTertiaryLightHighContrast = Color(0xFFFFFFFF)
+val tertiaryContainerLightHighContrast = Color(0xFF00543C)
+val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF)
+val errorLightHighContrast = Color(0xFF600004)
+val onErrorLightHighContrast = Color(0xFFFFFFFF)
+val errorContainerLightHighContrast = Color(0xFF98000A)
+val onErrorContainerLightHighContrast = Color(0xFFFFFFFF)
+val backgroundLightHighContrast = Color(0xFFFAF8FF)
+val onBackgroundLightHighContrast = Color(0xFF1A1B21)
+val surfaceLightHighContrast = Color(0xFFFAF8FF)
+val onSurfaceLightHighContrast = Color(0xFF000000)
+val surfaceVariantLightHighContrast = Color(0xFFE2E2EC)
+val onSurfaceVariantLightHighContrast = Color(0xFF000000)
+val outlineLightHighContrast = Color(0xFF2A2C34)
+val outlineVariantLightHighContrast = Color(0xFF474951)
+val scrimLightHighContrast = Color(0xFF000000)
+val inverseSurfaceLightHighContrast = Color(0xFF2F3036)
+val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF)
+val inversePrimaryLightHighContrast = Color(0xFFB4C5FF)
+val surfaceDimLightHighContrast = Color(0xFFB9B8BF)
+val surfaceBrightLightHighContrast = Color(0xFFFAF8FF)
+val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF)
+val surfaceContainerLowLightHighContrast = Color(0xFFF1F0F7)
+val surfaceContainerLightHighContrast = Color(0xFFE3E2E9)
+val surfaceContainerHighLightHighContrast = Color(0xFFD5D3DB)
+val surfaceContainerHighestLightHighContrast = Color(0xFFC6C6CD)
-val orange200 = Color(0xFFFFCB80)
-val orange500 = Color(0xFFFF9600)
-val orange800 = Color(0xFFFF7900)
+val primaryDark = Color(0xFFB4C5FF)
+val onPrimaryDark = Color(0xFF1A2D60)
+val primaryContainerDark = Color(0xFF334478)
+val onPrimaryContainerDark = Color(0xFFDBE1FF)
+val secondaryDark = Color(0xFFC1C5DD)
+val onSecondaryDark = Color(0xFF2B3042)
+val secondaryContainerDark = Color(0xFF414659)
+val onSecondaryContainerDark = Color(0xFFDDE1F9)
+val tertiaryDark = Color(0xFF8CD5B4)
+val onTertiaryDark = Color(0xFF003827)
+val tertiaryContainerDark = Color(0xFF00513A)
+val onTertiaryContainerDark = Color(0xFFA7F2D0)
+val errorDark = Color(0xFFFFB4AB)
+val onErrorDark = Color(0xFF690005)
+val errorContainerDark = Color(0xFF93000A)
+val onErrorContainerDark = Color(0xFFFFDAD6)
+val backgroundDark = Color(0xFF121318)
+val onBackgroundDark = Color(0xFFE3E2E9)
+val surfaceDark = Color(0xFF121318)
+val onSurfaceDark = Color(0xFFE3E2E9)
+val surfaceVariantDark = Color(0xFF45464F)
+val onSurfaceVariantDark = Color(0xFFC5C6D0)
+val outlineDark = Color(0xFF8F909A)
+val outlineVariantDark = Color(0xFF45464F)
+val scrimDark = Color(0xFF000000)
+val inverseSurfaceDark = Color(0xFFE3E2E9)
+val inverseOnSurfaceDark = Color(0xFF2F3036)
+val inversePrimaryDark = Color(0xFF4B5C92)
+val surfaceDimDark = Color(0xFF121318)
+val surfaceBrightDark = Color(0xFF38393F)
+val surfaceContainerLowestDark = Color(0xFF0D0E13)
+val surfaceContainerLowDark = Color(0xFF1A1B21)
+val surfaceContainerDark = Color(0xFF1E1F25)
+val surfaceContainerHighDark = Color(0xFF292A2F)
+val surfaceContainerHighestDark = Color(0xFF34343A)
-val white = Color.White
-val white87 = Color(0xDEFFFFFF)
-val white70 = Color(0xB3FFFFFF)
-val white54 = Color(0x8AFFFFFF)
-val white50 = Color(0x80FFFFFF)
-val white38 = Color(0x61FFFFFF)
-val white12 = Color(0x12FFFFFF)
+val primaryDarkMediumContrast = Color(0xFFD3DBFF)
+val onPrimaryDarkMediumContrast = Color(0xFF0D2255)
+val primaryContainerDarkMediumContrast = Color(0xFF7D8FC8)
+val onPrimaryContainerDarkMediumContrast = Color(0xFF000000)
+val secondaryDarkMediumContrast = Color(0xFFD7DBF3)
+val onSecondaryDarkMediumContrast = Color(0xFF202536)
+val secondaryContainerDarkMediumContrast = Color(0xFF8B90A5)
+val onSecondaryContainerDarkMediumContrast = Color(0xFF000000)
+val tertiaryDarkMediumContrast = Color(0xFFA1ECCA)
+val onTertiaryDarkMediumContrast = Color(0xFF002C1E)
+val tertiaryContainerDarkMediumContrast = Color(0xFF569E80)
+val onTertiaryContainerDarkMediumContrast = Color(0xFF000000)
+val errorDarkMediumContrast = Color(0xFFFFD2CC)
+val onErrorDarkMediumContrast = Color(0xFF540003)
+val errorContainerDarkMediumContrast = Color(0xFFFF5449)
+val onErrorContainerDarkMediumContrast = Color(0xFF000000)
+val backgroundDarkMediumContrast = Color(0xFF121318)
+val onBackgroundDarkMediumContrast = Color(0xFFE3E2E9)
+val surfaceDarkMediumContrast = Color(0xFF121318)
+val onSurfaceDarkMediumContrast = Color(0xFFFFFFFF)
+val surfaceVariantDarkMediumContrast = Color(0xFF45464F)
+val onSurfaceVariantDarkMediumContrast = Color(0xFFDBDBE6)
+val outlineDarkMediumContrast = Color(0xFFB1B1BB)
+val outlineVariantDarkMediumContrast = Color(0xFF8F9099)
+val scrimDarkMediumContrast = Color(0xFF000000)
+val inverseSurfaceDarkMediumContrast = Color(0xFFE3E2E9)
+val inverseOnSurfaceDarkMediumContrast = Color(0xFF292A2F)
+val inversePrimaryDarkMediumContrast = Color(0xFF34467A)
+val surfaceDimDarkMediumContrast = Color(0xFF121318)
+val surfaceBrightDarkMediumContrast = Color(0xFF43444A)
+val surfaceContainerLowestDarkMediumContrast = Color(0xFF06070C)
+val surfaceContainerLowDarkMediumContrast = Color(0xFF1C1D23)
+val surfaceContainerDarkMediumContrast = Color(0xFF27282D)
+val surfaceContainerHighDarkMediumContrast = Color(0xFF313238)
+val surfaceContainerHighestDarkMediumContrast = Color(0xFF3C3D43)
-val black = Color.Black
-val black87 = Color(0xDE000000)
-val black70 = Color(0xB3000000)
-val black54 = Color(0x8A000000)
-val black50 = Color(0x80000000)
-val black38 = Color(0x61000000)
-val black12 = Color(0x12000000)
+val primaryDarkHighContrast = Color(0xFFEDEFFF)
+val onPrimaryDarkHighContrast = Color(0xFF000000)
+val primaryContainerDarkHighContrast = Color(0xFFAFC1FD)
+val onPrimaryContainerDarkHighContrast = Color(0xFF000928)
+val secondaryDarkHighContrast = Color(0xFFEDEFFF)
+val onSecondaryDarkHighContrast = Color(0xFF000000)
+val secondaryContainerDarkHighContrast = Color(0xFFBDC2D9)
+val onSecondaryContainerDarkHighContrast = Color(0xFF060A1B)
+val tertiaryDarkHighContrast = Color(0xFFB8FFDE)
+val onTertiaryDarkHighContrast = Color(0xFF000000)
+val tertiaryContainerDarkHighContrast = Color(0xFF88D2B1)
+val onTertiaryContainerDarkHighContrast = Color(0xFF000E08)
+val errorDarkHighContrast = Color(0xFFFFECE9)
+val onErrorDarkHighContrast = Color(0xFF000000)
+val errorContainerDarkHighContrast = Color(0xFFFFAEA4)
+val onErrorContainerDarkHighContrast = Color(0xFF220001)
+val backgroundDarkHighContrast = Color(0xFF121318)
+val onBackgroundDarkHighContrast = Color(0xFFE3E2E9)
+val surfaceDarkHighContrast = Color(0xFF121318)
+val onSurfaceDarkHighContrast = Color(0xFFFFFFFF)
+val surfaceVariantDarkHighContrast = Color(0xFF45464F)
+val onSurfaceVariantDarkHighContrast = Color(0xFFFFFFFF)
+val outlineDarkHighContrast = Color(0xFFEFEFFA)
+val outlineVariantDarkHighContrast = Color(0xFFC2C2CC)
+val scrimDarkHighContrast = Color(0xFF000000)
+val inverseSurfaceDarkHighContrast = Color(0xFFE3E2E9)
+val inverseOnSurfaceDarkHighContrast = Color(0xFF000000)
+val inversePrimaryDarkHighContrast = Color(0xFF34467A)
+val surfaceDimDarkHighContrast = Color(0xFF121318)
+val surfaceBrightDarkHighContrast = Color(0xFF4F5056)
+val surfaceContainerLowestDarkHighContrast = Color(0xFF000000)
+val surfaceContainerLowDarkHighContrast = Color(0xFF1E1F25)
+val surfaceContainerDarkHighContrast = Color(0xFF2F3036)
+val surfaceContainerHighDarkHighContrast = Color(0xFF3A3B41)
+val surfaceContainerHighestDarkHighContrast = Color(0xFF46464C)
diff --git a/common/design/src/main/java/com/androidvip/sysctlgui/design/theme/Shape.kt b/common/design/src/main/java/com/androidvip/sysctlgui/design/theme/Shape.kt
deleted file mode 100644
index d0f01ac..0000000
--- a/common/design/src/main/java/com/androidvip/sysctlgui/design/theme/Shape.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.androidvip.sysctlgui.design.theme
-
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.Shapes
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.ReadOnlyComposable
-import androidx.compose.ui.unit.dp
-
-val SysctlGuiShapes = Shapes(
- extraSmall = RoundedCornerShape(4.dp),
- small = RoundedCornerShape(8.dp),
- medium = RoundedCornerShape(12.dp),
- large = RoundedCornerShape(16.dp),
- extraLarge = RoundedCornerShape(24.dp),
-)
-
-val MaterialTheme.bottomSheetShape
- @Composable
- @ReadOnlyComposable
- get() = RoundedCornerShape(
- topStart = 24.dp,
- topEnd = 24.dp,
- bottomStart = 0.dp,
- bottomEnd = 0.dp
- )
diff --git a/common/design/src/main/java/com/androidvip/sysctlgui/design/theme/Theme.kt b/common/design/src/main/java/com/androidvip/sysctlgui/design/theme/Theme.kt
index 60d1c57..69a1120 100644
--- a/common/design/src/main/java/com/androidvip/sysctlgui/design/theme/Theme.kt
+++ b/common/design/src/main/java/com/androidvip/sysctlgui/design/theme/Theme.kt
@@ -10,90 +10,263 @@ import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
-internal val LightColors = lightColorScheme(
- primary = md_theme_light_primary,
- onPrimary = md_theme_light_onPrimary,
- primaryContainer = md_theme_light_primaryContainer,
- onPrimaryContainer = md_theme_light_onPrimaryContainer,
- secondary = md_theme_light_secondary,
- onSecondary = md_theme_light_onSecondary,
- secondaryContainer = md_theme_light_secondaryContainer,
- onSecondaryContainer = md_theme_light_onSecondaryContainer,
- tertiary = md_theme_light_tertiary,
- onTertiary = md_theme_light_onTertiary,
- tertiaryContainer = md_theme_light_tertiaryContainer,
- onTertiaryContainer = md_theme_light_onTertiaryContainer,
- error = md_theme_light_error,
- errorContainer = md_theme_light_errorContainer,
- onError = md_theme_light_onError,
- onErrorContainer = md_theme_light_onErrorContainer,
- background = md_theme_light_background,
- onBackground = md_theme_light_onBackground,
- surface = md_theme_light_surface,
- onSurface = md_theme_light_onSurface,
- surfaceVariant = md_theme_light_surfaceVariant,
- onSurfaceVariant = md_theme_light_onSurfaceVariant,
- outline = md_theme_light_outline,
- inverseOnSurface = md_theme_light_inverseOnSurface,
- inverseSurface = md_theme_light_inverseSurface,
- inversePrimary = md_theme_light_inversePrimary,
- surfaceTint = md_theme_light_surfaceTint,
- outlineVariant = md_theme_light_outlineVariant,
- scrim = md_theme_light_scrim
+private val lightScheme = lightColorScheme(
+ primary = primaryLight,
+ onPrimary = onPrimaryLight,
+ primaryContainer = primaryContainerLight,
+ onPrimaryContainer = onPrimaryContainerLight,
+ secondary = secondaryLight,
+ onSecondary = onSecondaryLight,
+ secondaryContainer = secondaryContainerLight,
+ onSecondaryContainer = onSecondaryContainerLight,
+ tertiary = tertiaryLight,
+ onTertiary = onTertiaryLight,
+ tertiaryContainer = tertiaryContainerLight,
+ onTertiaryContainer = onTertiaryContainerLight,
+ error = errorLight,
+ onError = onErrorLight,
+ errorContainer = errorContainerLight,
+ onErrorContainer = onErrorContainerLight,
+ background = backgroundLight,
+ onBackground = onBackgroundLight,
+ surface = surfaceLight,
+ onSurface = onSurfaceLight,
+ surfaceVariant = surfaceVariantLight,
+ onSurfaceVariant = onSurfaceVariantLight,
+ outline = outlineLight,
+ outlineVariant = outlineVariantLight,
+ scrim = scrimLight,
+ inverseSurface = inverseSurfaceLight,
+ inverseOnSurface = inverseOnSurfaceLight,
+ inversePrimary = inversePrimaryLight,
+ surfaceDim = surfaceDimLight,
+ surfaceBright = surfaceBrightLight,
+ surfaceContainerLowest = surfaceContainerLowestLight,
+ surfaceContainerLow = surfaceContainerLowLight,
+ surfaceContainer = surfaceContainerLight,
+ surfaceContainerHigh = surfaceContainerHighLight,
+ surfaceContainerHighest = surfaceContainerHighestLight,
)
-internal val DarkColors = darkColorScheme(
- primary = md_theme_dark_primary,
- onPrimary = md_theme_dark_onPrimary,
- primaryContainer = md_theme_dark_primaryContainer,
- onPrimaryContainer = md_theme_dark_onPrimaryContainer,
- secondary = md_theme_dark_secondary,
- onSecondary = md_theme_dark_onSecondary,
- secondaryContainer = md_theme_dark_secondaryContainer,
- onSecondaryContainer = md_theme_dark_onSecondaryContainer,
- tertiary = md_theme_dark_tertiary,
- onTertiary = md_theme_dark_onTertiary,
- tertiaryContainer = md_theme_dark_tertiaryContainer,
- onTertiaryContainer = md_theme_dark_onTertiaryContainer,
- error = md_theme_dark_error,
- errorContainer = md_theme_dark_errorContainer,
- onError = md_theme_dark_onError,
- onErrorContainer = md_theme_dark_onErrorContainer,
- background = md_theme_dark_background,
- onBackground = md_theme_dark_onBackground,
- surface = md_theme_dark_surface,
- onSurface = md_theme_dark_onSurface,
- surfaceVariant = md_theme_dark_surfaceVariant,
- onSurfaceVariant = md_theme_dark_onSurfaceVariant,
- outline = md_theme_dark_outline,
- inverseOnSurface = md_theme_dark_inverseOnSurface,
- inverseSurface = md_theme_dark_inverseSurface,
- inversePrimary = md_theme_dark_inversePrimary,
- surfaceTint = md_theme_dark_surfaceTint,
- outlineVariant = md_theme_dark_outlineVariant,
- scrim = md_theme_dark_scrim
+private val mediumContrastLightColorScheme = lightColorScheme(
+ primary = primaryLightMediumContrast,
+ onPrimary = onPrimaryLightMediumContrast,
+ primaryContainer = primaryContainerLightMediumContrast,
+ onPrimaryContainer = onPrimaryContainerLightMediumContrast,
+ secondary = secondaryLightMediumContrast,
+ onSecondary = onSecondaryLightMediumContrast,
+ secondaryContainer = secondaryContainerLightMediumContrast,
+ onSecondaryContainer = onSecondaryContainerLightMediumContrast,
+ tertiary = tertiaryLightMediumContrast,
+ onTertiary = onTertiaryLightMediumContrast,
+ tertiaryContainer = tertiaryContainerLightMediumContrast,
+ onTertiaryContainer = onTertiaryContainerLightMediumContrast,
+ error = errorLightMediumContrast,
+ onError = onErrorLightMediumContrast,
+ errorContainer = errorContainerLightMediumContrast,
+ onErrorContainer = onErrorContainerLightMediumContrast,
+ background = backgroundLightMediumContrast,
+ onBackground = onBackgroundLightMediumContrast,
+ surface = surfaceLightMediumContrast,
+ onSurface = onSurfaceLightMediumContrast,
+ surfaceVariant = surfaceVariantLightMediumContrast,
+ onSurfaceVariant = onSurfaceVariantLightMediumContrast,
+ outline = outlineLightMediumContrast,
+ outlineVariant = outlineVariantLightMediumContrast,
+ scrim = scrimLightMediumContrast,
+ inverseSurface = inverseSurfaceLightMediumContrast,
+ inverseOnSurface = inverseOnSurfaceLightMediumContrast,
+ inversePrimary = inversePrimaryLightMediumContrast,
+ surfaceDim = surfaceDimLightMediumContrast,
+ surfaceBright = surfaceBrightLightMediumContrast,
+ surfaceContainerLowest = surfaceContainerLowestLightMediumContrast,
+ surfaceContainerLow = surfaceContainerLowLightMediumContrast,
+ surfaceContainer = surfaceContainerLightMediumContrast,
+ surfaceContainerHigh = surfaceContainerHighLightMediumContrast,
+ surfaceContainerHighest = surfaceContainerHighestLightMediumContrast,
+)
+
+private val highContrastLightColorScheme = lightColorScheme(
+ primary = primaryLightHighContrast,
+ onPrimary = onPrimaryLightHighContrast,
+ primaryContainer = primaryContainerLightHighContrast,
+ onPrimaryContainer = onPrimaryContainerLightHighContrast,
+ secondary = secondaryLightHighContrast,
+ onSecondary = onSecondaryLightHighContrast,
+ secondaryContainer = secondaryContainerLightHighContrast,
+ onSecondaryContainer = onSecondaryContainerLightHighContrast,
+ tertiary = tertiaryLightHighContrast,
+ onTertiary = onTertiaryLightHighContrast,
+ tertiaryContainer = tertiaryContainerLightHighContrast,
+ onTertiaryContainer = onTertiaryContainerLightHighContrast,
+ error = errorLightHighContrast,
+ onError = onErrorLightHighContrast,
+ errorContainer = errorContainerLightHighContrast,
+ onErrorContainer = onErrorContainerLightHighContrast,
+ background = backgroundLightHighContrast,
+ onBackground = onBackgroundLightHighContrast,
+ surface = surfaceLightHighContrast,
+ onSurface = onSurfaceLightHighContrast,
+ surfaceVariant = surfaceVariantLightHighContrast,
+ onSurfaceVariant = onSurfaceVariantLightHighContrast,
+ outline = outlineLightHighContrast,
+ outlineVariant = outlineVariantLightHighContrast,
+ scrim = scrimLightHighContrast,
+ inverseSurface = inverseSurfaceLightHighContrast,
+ inverseOnSurface = inverseOnSurfaceLightHighContrast,
+ inversePrimary = inversePrimaryLightHighContrast,
+ surfaceDim = surfaceDimLightHighContrast,
+ surfaceBright = surfaceBrightLightHighContrast,
+ surfaceContainerLowest = surfaceContainerLowestLightHighContrast,
+ surfaceContainerLow = surfaceContainerLowLightHighContrast,
+ surfaceContainer = surfaceContainerLightHighContrast,
+ surfaceContainerHigh = surfaceContainerHighLightHighContrast,
+ surfaceContainerHighest = surfaceContainerHighestLightHighContrast,
+)
+
+private val darkScheme = darkColorScheme(
+ primary = primaryDark,
+ onPrimary = onPrimaryDark,
+ primaryContainer = primaryContainerDark,
+ onPrimaryContainer = onPrimaryContainerDark,
+ secondary = secondaryDark,
+ onSecondary = onSecondaryDark,
+ secondaryContainer = secondaryContainerDark,
+ onSecondaryContainer = onSecondaryContainerDark,
+ tertiary = tertiaryDark,
+ onTertiary = onTertiaryDark,
+ tertiaryContainer = tertiaryContainerDark,
+ onTertiaryContainer = onTertiaryContainerDark,
+ error = errorDark,
+ onError = onErrorDark,
+ errorContainer = errorContainerDark,
+ onErrorContainer = onErrorContainerDark,
+ background = backgroundDark,
+ onBackground = onBackgroundDark,
+ surface = surfaceDark,
+ onSurface = onSurfaceDark,
+ surfaceVariant = surfaceVariantDark,
+ onSurfaceVariant = onSurfaceVariantDark,
+ outline = outlineDark,
+ outlineVariant = outlineVariantDark,
+ scrim = scrimDark,
+ inverseSurface = inverseSurfaceDark,
+ inverseOnSurface = inverseOnSurfaceDark,
+ inversePrimary = inversePrimaryDark,
+ surfaceDim = surfaceDimDark,
+ surfaceBright = surfaceBrightDark,
+ surfaceContainerLowest = surfaceContainerLowestDark,
+ surfaceContainerLow = surfaceContainerLowDark,
+ surfaceContainer = surfaceContainerDark,
+ surfaceContainerHigh = surfaceContainerHighDark,
+ surfaceContainerHighest = surfaceContainerHighestDark,
+)
+
+private val mediumContrastDarkColorScheme = darkColorScheme(
+ primary = primaryDarkMediumContrast,
+ onPrimary = onPrimaryDarkMediumContrast,
+ primaryContainer = primaryContainerDarkMediumContrast,
+ onPrimaryContainer = onPrimaryContainerDarkMediumContrast,
+ secondary = secondaryDarkMediumContrast,
+ onSecondary = onSecondaryDarkMediumContrast,
+ secondaryContainer = secondaryContainerDarkMediumContrast,
+ onSecondaryContainer = onSecondaryContainerDarkMediumContrast,
+ tertiary = tertiaryDarkMediumContrast,
+ onTertiary = onTertiaryDarkMediumContrast,
+ tertiaryContainer = tertiaryContainerDarkMediumContrast,
+ onTertiaryContainer = onTertiaryContainerDarkMediumContrast,
+ error = errorDarkMediumContrast,
+ onError = onErrorDarkMediumContrast,
+ errorContainer = errorContainerDarkMediumContrast,
+ onErrorContainer = onErrorContainerDarkMediumContrast,
+ background = backgroundDarkMediumContrast,
+ onBackground = onBackgroundDarkMediumContrast,
+ surface = surfaceDarkMediumContrast,
+ onSurface = onSurfaceDarkMediumContrast,
+ surfaceVariant = surfaceVariantDarkMediumContrast,
+ onSurfaceVariant = onSurfaceVariantDarkMediumContrast,
+ outline = outlineDarkMediumContrast,
+ outlineVariant = outlineVariantDarkMediumContrast,
+ scrim = scrimDarkMediumContrast,
+ inverseSurface = inverseSurfaceDarkMediumContrast,
+ inverseOnSurface = inverseOnSurfaceDarkMediumContrast,
+ inversePrimary = inversePrimaryDarkMediumContrast,
+ surfaceDim = surfaceDimDarkMediumContrast,
+ surfaceBright = surfaceBrightDarkMediumContrast,
+ surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast,
+ surfaceContainerLow = surfaceContainerLowDarkMediumContrast,
+ surfaceContainer = surfaceContainerDarkMediumContrast,
+ surfaceContainerHigh = surfaceContainerHighDarkMediumContrast,
+ surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast,
+)
+
+private val highContrastDarkColorScheme = darkColorScheme(
+ primary = primaryDarkHighContrast,
+ onPrimary = onPrimaryDarkHighContrast,
+ primaryContainer = primaryContainerDarkHighContrast,
+ onPrimaryContainer = onPrimaryContainerDarkHighContrast,
+ secondary = secondaryDarkHighContrast,
+ onSecondary = onSecondaryDarkHighContrast,
+ secondaryContainer = secondaryContainerDarkHighContrast,
+ onSecondaryContainer = onSecondaryContainerDarkHighContrast,
+ tertiary = tertiaryDarkHighContrast,
+ onTertiary = onTertiaryDarkHighContrast,
+ tertiaryContainer = tertiaryContainerDarkHighContrast,
+ onTertiaryContainer = onTertiaryContainerDarkHighContrast,
+ error = errorDarkHighContrast,
+ onError = onErrorDarkHighContrast,
+ errorContainer = errorContainerDarkHighContrast,
+ onErrorContainer = onErrorContainerDarkHighContrast,
+ background = backgroundDarkHighContrast,
+ onBackground = onBackgroundDarkHighContrast,
+ surface = surfaceDarkHighContrast,
+ onSurface = onSurfaceDarkHighContrast,
+ surfaceVariant = surfaceVariantDarkHighContrast,
+ onSurfaceVariant = onSurfaceVariantDarkHighContrast,
+ outline = outlineDarkHighContrast,
+ outlineVariant = outlineVariantDarkHighContrast,
+ scrim = scrimDarkHighContrast,
+ inverseSurface = inverseSurfaceDarkHighContrast,
+ inverseOnSurface = inverseOnSurfaceDarkHighContrast,
+ inversePrimary = inversePrimaryDarkHighContrast,
+ surfaceDim = surfaceDimDarkHighContrast,
+ surfaceBright = surfaceBrightDarkHighContrast,
+ surfaceContainerLowest = surfaceContainerLowestDarkHighContrast,
+ surfaceContainerLow = surfaceContainerLowDarkHighContrast,
+ surfaceContainer = surfaceContainerDarkHighContrast,
+ surfaceContainerHigh = surfaceContainerHighDarkHighContrast,
+ surfaceContainerHighest = surfaceContainerHighestDarkHighContrast,
)
@Composable
fun SysctlGuiTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
- forceDark: Boolean = false,
dynamicColor: Boolean = false,
+ contrastLevel: Int = 1,
content: @Composable () -> Unit
) {
val colorScheme = when {
- forceDark -> DarkColors
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
- darkTheme -> DarkColors
- else -> LightColors
+
+ darkTheme -> when (contrastLevel) {
+ 2 -> mediumContrastDarkColorScheme
+ 3 -> highContrastDarkColorScheme
+ else -> darkScheme
+ }
+
+ else -> when (contrastLevel) {
+ 2 -> mediumContrastLightColorScheme
+ 3 -> highContrastLightColorScheme
+ else -> lightScheme
+ }
}
MaterialTheme(
colorScheme = colorScheme,
- shapes = SysctlGuiShapes,
+ typography = Typography,
content = content
)
}
diff --git a/common/design/src/main/java/com/androidvip/sysctlgui/design/theme/Type.kt b/common/design/src/main/java/com/androidvip/sysctlgui/design/theme/Type.kt
new file mode 100644
index 0000000..9277e5c
--- /dev/null
+++ b/common/design/src/main/java/com/androidvip/sysctlgui/design/theme/Type.kt
@@ -0,0 +1,133 @@
+package com.androidvip.sysctlgui.design.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+import com.androidvip.sysctlgui.design.R
+
+val passionOneFontFamily = FontFamily(
+ Font(resId = R.font.passionone_regular, weight = FontWeight.Normal),
+ Font(resId = R.font.passionone_regular, weight = FontWeight.Medium),
+ Font(resId = R.font.passionone_regular, weight = FontWeight.SemiBold),
+ Font(resId = R.font.passionone_bold, weight = FontWeight.Bold),
+ Font(resId = R.font.passionone_bold, weight = FontWeight.ExtraBold),
+ Font(resId = R.font.passionone_bold, weight = FontWeight.Black)
+)
+
+val sansationFontFamily = FontFamily(
+ Font(resId = R.font.sansation_regular),
+ Font(resId = R.font.sansation_regular_italic, style = FontStyle.Italic),
+ Font(resId = R.font.sansation_bold, weight = FontWeight.Medium),
+ Font(resId = R.font.sansation_bold, weight = FontWeight.SemiBold),
+ Font(resId = R.font.sansation_bold, weight = FontWeight.Bold),
+ Font(resId = R.font.sansation_bold_italic, weight = FontWeight.Bold, style = FontStyle.Italic),
+)
+
+val Typography = Typography(
+ displayLarge = TextStyle(
+ fontFamily = passionOneFontFamily,
+ fontWeight = FontWeight.Black,
+ fontSize = 57.sp,
+ lineHeight = 64.sp,
+ letterSpacing = 0.sp
+ ),
+ displayMedium = TextStyle(
+ fontFamily = passionOneFontFamily,
+ fontWeight = FontWeight.Bold,
+ fontSize = 45.sp,
+ lineHeight = 52.sp,
+ letterSpacing = 0.sp
+ ),
+ displaySmall = TextStyle(
+ fontFamily = passionOneFontFamily,
+ fontWeight = FontWeight.Bold,
+ fontSize = 36.sp,
+ lineHeight = 44.sp,
+ letterSpacing = 0.sp
+ ),
+ headlineLarge = TextStyle(
+ fontFamily = passionOneFontFamily,
+ fontWeight = FontWeight.Bold,
+ fontSize = 32.sp,
+ lineHeight = 40.sp,
+ letterSpacing = 0.sp
+ ),
+ headlineMedium = TextStyle(
+ fontFamily = passionOneFontFamily,
+ fontWeight = FontWeight.Bold,
+ fontSize = 28.sp,
+ lineHeight = 36.sp,
+ letterSpacing = 0.sp
+ ),
+ headlineSmall = TextStyle(
+ fontFamily = passionOneFontFamily,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 24.sp,
+ lineHeight = 32.sp,
+ letterSpacing = 0.sp
+ ),
+ titleLarge = TextStyle(
+ fontFamily = sansationFontFamily,
+ fontWeight = FontWeight.Bold,
+ fontSize = 22.sp,
+ lineHeight = 28.sp,
+ letterSpacing = 0.sp
+ ),
+ titleMedium = TextStyle(
+ fontFamily = sansationFontFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.15.sp
+ ),
+ titleSmall = TextStyle(
+ fontFamily = sansationFontFamily,
+ fontWeight = FontWeight.SemiBold,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.1.sp
+ ),
+ bodyLarge = TextStyle(
+ fontWeight = FontWeight.Medium,
+ fontSize = 16.sp,
+ lineHeight = 24.sp,
+ letterSpacing = 0.15.sp
+ ),
+ bodyMedium = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.25.sp
+ ),
+ bodySmall = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 12.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.4.sp
+ ),
+ labelLarge = TextStyle(
+ fontFamily = sansationFontFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ letterSpacing = 0.1.sp
+ ),
+ labelMedium = TextStyle(
+ fontFamily = sansationFontFamily,
+ fontWeight = FontWeight.Medium,
+ fontSize = 12.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ ),
+ labelSmall = TextStyle(
+ fontFamily = sansationFontFamily,
+ fontWeight = FontWeight.Normal,
+ fontSize = 11.sp,
+ lineHeight = 16.sp,
+ letterSpacing = 0.5.sp
+ )
+)
diff --git a/common/design/src/main/res/font/passionone_bold.ttf b/common/design/src/main/res/font/passionone_bold.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..25a35e453ed4ed792204e44ba2f6378eb52b8903
GIT binary patch
literal 22876
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
vmMGy
literal 0
HcmV?d00001
diff --git a/common/design/src/main/res/font/passionone_regular.ttf b/common/design/src/main/res/font/passionone_regular.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..099be0fc86b29052f283167fe539ac9f6333ca25
GIT binary patch
literal 23076
zcmcJ134Bvk_W!*vY0{=!+H76ZEN#=hC25n=6$)i36bck%hi;Uu19aoa_yV87hhzIufXT24J~zZcRtv0A3k5kwYs@(
z{#-m)>oQArJd9^<=CTnC&NNA~BGMA5GqtRZD_NLalmZoXib?;%QcC-(h
zJ7@lapZ@tmDq}$_(EdMj+nVN1S8QB^w*SPXJi$F{j>>OGcg8&)7KuBC!bN`qJ>_BI
zGd#zvM>3`N8?gu8_YptSCnoFX{sCjY<9W}MD}Pk%<^7Uw=uNOWyJg;2XGc$oEcu>=
z%C68i=VVr*4|~(DPDI}#`J=La_!x}wr9Xks<9DziAaq)|6^}Mbcm5u;v!4mgAVo0U
zZ&yeCu2w#puootB+3X-#n^qsd}QHRYL#
zO#MxV%@%X2MX)F=k(OvnoW*F#v<$LLwKS!?{EHm)rt$Z%N4Smm;x1m!NAfXzDj+Wf
zhF{T8QNrc?H8{{ZI$c=!^JRZ-7p65N!d3JfWcs6@BdX{^Zd6szUJ(E2{
z+%fLpYnQH#x>o+{scY$%-@5$9<<~F2cKMadFJ6A;^0CXqF1>T<`HMeaeE;IVFMfIP
zlZzi-eCy&<7x!Mg{~x6wGSTM$_g@U_&1SIO>?&wIl-GkAckzS#dHxE&%)b)SghFAw
zaJz6ocv<)!^cf_ZC)+4H2M@>Ix&sxVcms#-N$wO+Mb^_1!j)z#3DP)BHa=#0>H__rtY
zrLf?z@~~-PH-`Ns?2~X)cy0LH@a5sB!(R^nG(w0-j;M~95V163Tg3elXCf{{d=nWF
zSrR!n@_6J2YC&yL4^S^s?^9n;e;bt@RTb47wLa>2)L)|BjruxT6I~cRE&9&r)6t)6
zQZ;p&n>B|t=QZDGW3)rG&Dyov6fBQ)1S|9F2J|Rv$Y&wk`Jl*t4;p#zn;Sjcbm(BkrZRFXAKO^Wsb5$Hs4re>nbp
z{C5f3g!F{|36m1q6Luv$lkja~ZsLT*O^L@7KQXF|eT;3!`;5;S-%pB6GAG%S`X!A{
zYDrp~v^VKQ(#uH~O$zX9y2)v(GEFwkGi^3KZhGBx#Vj{#%)QM|m|rlzW&XsXwG>+#
zEH_!UTJE=;uzY6KSo>P1T5q&&wjQ$nGdU!=AbDZ(6FxoQIR<&Z&Y!4_Ia9MY$--F#w3?blv1q1&Tstm8_KHLFgBdkuo2LmqgX98^%!;o^vXCko=spA*(5fZO<_}69T(UGY&BcMo@RHk
zD{L*>#BPHWJjC|0TiLtp7Pf_b%f4f`v+Zm>`!l=9zG3&WgY0{DmHoi}#2#k^UC8W0kG-^XJwzG|8ve*R?4IFPJgAu}LvQ
zya|J=gc&nKs~TpsH7snIHoIv_=*+J7VY3?Mw6xUKH8eD}E(n{|+}708I=im5aYln`
zXycp((sTK2yj2XVYgo9TNzp3ZMvAwvk^bG={5~i~Hq^DvX;rj|x3ErF4K4NZ%7txn
zglW@c)27Y9HCy8K2&^isk~{<-4+zJ-?>W;8d%W)j79ni*zL&8gVXXJPoTUnO@B1JY
zC&YN)D_AnW>V27R7tiKHj^^Y2Y~1JIX*0eb4p|urnV62}^C5!+@J%b)njk52z^@Cy
zcXepngeOvq*|<00b1T|6fy+(c&sN-<(8`4N)5W%+wo>F2-lV!-QuvZeK_~y;(B9q8ADXAOAPf-z|9nRrUHXu~rT=1;Hv@O(CCHK&udUA=42em?&B<}Fc}
zuwDQdbs|+;aBqWNFag3ey!uwC3rY@jn?Ic4z@Z6Rb0l7yu{uPXw(HV34_ejUtv9V1
z^*43vPirEzFGi1XqAsO(|9#{@JOX?8M(c;!&4q3wUTqfF$OH?}02^QehNSy@U3c!(
zY6kG7mFe?4>yr*U;?Eg`6Y*~w_($SmA6-qb7Ij!rqJX8_{4DG>Ng?oR-0_}guwncd
zOXXj)7=DhKWSPt)(E5FeXH|Gs#nk)&)AA2kKF?!A_!gGO({P{R`GLQ~47?BIeK!k+
z{OkD_OojJJfMei$@%{pyonv8Q`#jd4-^$GVMdrla$Zugz*&@LI7Ja{BSEEM?t1e~eO2?J`2}X
zwiEO+3VqOjJR5<}55Rh5@zpF3eKTYxmL&_u)rx1g06(JnbuQr)aD9!%ifcgY;JehG
z&$*gqay96u_Ixcwc)kanvuO>wU;LjeUwnqOqqXo|;yRoM%%4D8qWN_$;TAj}$0GS@
z%-8Q?S3I9$9n8|*bD6&l+P?!{I*)eUFQUuS*j>{)_+PXRv|e<79hcs774Y5zT#H}_
zk6mVQ#Am*>=Fgz*0pPnA_nFwU{0nOez9X2z6D(FZj4_Ds=%V%a-H8w9GY*F&@>LMqn&R~F76#@a21-uM!}CuC8S$77(6bfxlb
z7~@a4cC(SP2v#9)LBBH4qZIr_eEuEQrWl{!U|OLsX#PIv3fc%`Sfp?_a6akz3G$jI
z%HzA>JzQv)Bc!9PhlL1oT-R6#_$CBi@i5Q#%q_IQPKPq)k~w6`e5YNn(^@8k9*np*
zV`9e9XZR8Kh?~-Ewv1}DPsQ$YzpxTMbQELJcCFIpvZWR{eQ&ZKTUJ~q6#PZH2`i5c
z*N+GaEgSB=(_T6TYxK5I3;#L^@2pCP&Z@M!#H+yJvf^?m#S42c2Y-85aNiNyWzmU-
z)rQ_x!6ifVtF-ajRob%uv4z-pMds37_*75g!9mlEIKvc=_+vO
z^af?B)uPiHWm<(&XDvvzx%Bpe{2X4LJ+SX;dzrQ&B{#cIpX|P6tNXY@XVqr+;h8ln
zn$(lW%R~1VQX><1(v8NpLL+
zr6Q4Qjoj{Zm2s`Y0z~ZmH@^`wo>=(!--DwP)Uu!ubz-DkKDX@A^0LSHZS+hor)RPt
z>6x(j+Hzs#N1?^(5fRab;?RCs;o;iY(_hS*{n;${#Opq$=moG9YJ}C$bt$lxECnbP
zfH-(D=|B{N!&%B5I;~RYP+BdiHb;R?q0?&eK{SKY<>YJnJ~hLc-;2+)mk*VPtu7v^
zaySbQmrjnosnVc5VV6Yy>Ea$Q({yqRtzwMlMqVUr0FN?_L7_G%b3ndl+MAnK)Ql816x`$9aX54A
z(Pap;ai&IFySJ?iysJ>!oN9IZzL7O6nuXeed%swAbZq8fKAqZXIp&UXN`uly
z{OdBf90nKPS~qC?;K~WbQ{tzoT0`piGs%-mYDbk!O0L&8G-}Z|pH1bD@GnVHQmkMj
z@G6+g!5>++>ZY4kE&Fucx@GIvqumfs2bZy}6e)Arb=n9n2}%CGNoiT>#pQx?>qKi`
zn^8Y1D6+5y?enl3Y2X8~N-V`(VCD_(zj4QNYeh)kvtO{cLtM?}mCGR{ll>)eL{#nv
zPYa(f%CE?MeE!=Hz9qC=BUq3%1N6m<;86|st_%cBwLuP)5_9SedOPu^Qem~&h`LUP
z-Js{Gp^DIi1XUOpLKUhC^`0n0_AM*34bkJ4tLua;?
z3HhmnS8&~Pjpeo5+;;{~sYyu8(Iu7({qpM{Z>#H=)-=6;u}Ps!1Pnc9buVC8!5g42
zMB2!4BffR%9T4nHUf{4h3-U2nTN!`nbaHx5+VqBAl}_s@Q*L=#d4eI=Tx)ZU5f(qt
zt52L#fK1z`yx5sxUd&Z+~6`Np#ohJ`gJJ8T-&8eVfO
zF0JGB_mb{Tnxob7mTluAysLHw^9zB8!jg$FIxXg7BfV_3L}8(nP%s83Pl+vv5qih!
z!)wO=^}S<%R*jwzB_v-lDb=cJrq3o!dXMW)AMMC03<4u^aXwq6l_5Q41*2^}bb9~G
z{8mAI?Q4GiKJW}t_7q0eK{sbWe~PBSOJ9&T-&Fuc(SX9Kwg`}1L+U|qCxePN#3ZSU
z_>x?cRvD>D(1Je%76uF+0|siEyUS9L@1R*LtS)F$n(3=m7`&)szx&b_?LvKC-JfQh
z`suaD+W+Km46PW|M-`Kh*pXb}e%{tANEb_MOj_kL@V1h4v)O8cH=%LJaO0ot*DTW9
z*XEvpNA>kWi+kopq+?{*b20utdW1qZ#*?gO@+hzx44_^C7R%&1M;Vvy{QWcEe?4Vl
zH@s#^OWVq!mGQ%pvMg^8(cGSRA1~f-x;u`%E5hn$aUS4Ru7(B$PJ)O}
zl!O25Vdr1lF#mj#*=aQM{_fL4%TD*{A&*wMPho88yH=bLOXpE$rBw&d3f8IJeUy(^
zJ=oE41kER{Zbq}twy>^TYG;iy^Y#bVtT_bUdt=h-?*Itx;xL~}m=7q#Q!NTDtvZ>#
zD3Dk*9Hflw2L7k1g>*R0)n(TUSjV!kQFS&O=
zJSS=NA0}MG@Zp6AKmS~4xq21tpb*$)w1X{kpv~oj2PLeqTVYqF
z{+cM_JR9+4)YSo#9pRR!5G=B~x@dh!B!9~{+P~2nN{~2%MvHQQJNQRW4^i#gry76a
z#?Qbz?q9jw{j3+eHnc}Z33NnTKHYux(A{WZ#J8ysi#7Nb>qGWUhJR7uHToy4*&R}R
zf1&Db-TQ^X&Wp4_(js92YJ8Y{BjIfp`w1|IOZ&!8;zhId~^GyODQt{~ddG_{s3t
z1khdy4rS3My;@;PRm-soa4hw9xNEm@#k>XXU)y+D;o=C=SrOc9%3#BV|PcUpm15JW^mE0rT!>AFmp|&Jana{>_I-j4>1X^4}e!V
z*-G+5G~U&*cS<0C(~>;uWSQZ>mBvYb)9Yj7?S%V3MiFB*p@Z6vzV?)7e9
zNjN$sStDN`$ddgfM_*Q;o_=hK&&{9q)Hu<}@0%@z&W`pu`1w<&CraLZMvV)e)h_^j
zAMhjnj5Ut3y3BmrfQgW>$UOOn%rsh5CUT`IlbkGP8^rh<`c5{nOm1ol)s<;I@DdX7KH3sl`HU
zlSY8u7XOLth91JU!t>7H>{t5kjZm*P1g9Jw@#2fTf@hvM;eL~P;g4UyxTGUwuqR?4eqrmu&8M@2Gcm%b
zGiUh2FTYG}$yYihwN2+XETq{WbMRB{>GCI^Q}KO4k32q+FS@(ly&7%vJd1e(b^xGj
z5J^5Rp76yN6%`v~s+HG%4D@LMMsxn{eQ#}%v1^17_A!fjn6Eb}BIu1UbpB$)3L#`A
zwP+E7`8uIPoVCGf!`_k)=sUw|omC-ptSc;BM{O9OZ1!9t%@Ra*ceA^dZ{9E+-^@qv
zk-}E`Mq_grF3rBTuMoU(BjhLoD}9)M4qXVp6Sh$A5IyJLwt0MAkj4;Mq^Q#REL`5S
z*cAu87VZ^I_>J-|d$!r^Z_kVv>l9`giA)qMx*B$bY#QlFr-ST)1$N5rJk
zs$2HU&H2b5X6iK7Nd8%i`^ROrL}BH_diOJyDUPsLLc(d*{QXi~Xvjg3SHL4<1=vkhov1bN?qoP)1rcIt6CD>_5w5kGP{X;h#IESghe8
zuRs(85x0ZDO+oPijYR>2kS{vCN-Mk$$&wc2e-fv@A-#E=@}o-g)QH;J=2kw@njDkc
z(J(P=>JUSF+`0unW1hsZWx{&0!WvQsmPC$SEr~ETZ270l`bT7^j|?8PKPyXEuh<+E
z9o_Ce#7DQ=*I?B(*iZc#c!a`67BFUTz=6Sj0wA;+vZqji_@pi7QbV;GjN1b6`I|
zom*P!?%;*75$>mjl`jxI;XSkSpn(|IA?ryI2)KrpmmYlZ;f$#n8B_VYg*BAvsTuV`kl;UA+lG>40HCZq}6WPC7E6Yi?$+bU?>mZ*qL(8ELnVXr86
zj0e)dM+yWl;DbEjS}oE;q{&W|+_l{hKp2TU{E{`d&_`K7y$)p!FL)sx*r^JHaLMZB
z)F;qWvdh3o4L)#ondN+a(_8gZ-);OXKO({r!51!C;Qn#Rk{{Cg#0<>H=$qJ=R>uha
zbBce4oh7*4MJkZyAjvtoosxLJc;Hr5P{Q7R1E*ve2Y7`$bgMAknBM08b-8}GPqfKb
zSj-705nfP%S2FFS16YAMYt4B0+TIZ7*(@Q~gEuCz~_*#43Zs$1kQevDtD^)*;T
ztBgKiH29l}ZHq-7xlis=t>#Vcv#}9;pxbKArG8{&PNE-Keb`#jE}xwCMf1_j@Em^0
zZN`P*47G%RD3qcldFnE_E=sf}g5wfOOP{nUQy(wgr3<#)W)9I)&zE23m%hax^(BjZ
z2z^0aX}98ntwCQUKlG?YVLCiw#PLK$(xIB$Z~xa1J9mCZctI~cEzO6^BDm=DLPJUj
zKHaXVs%?zK7JL3<*&)3!nx?9FWxPVmPrKjbnKUB#8%xn2x)Bx{VOFyN(bc8yo${@_
zc(bBo%hc~Sj{B8b27{MUAep4)A(Bvksn!DWH}jpv*jI`!ppiqR@|4!Th0)2$(awIY
z{q=Ei`u-bAD)+{Ru299_Ika@mo-}K?GBhQXYQ4rozmUg|r7?h+e%!qq1|82cFxEh6
zEQ6?=v31Ph;!@l7dM=Zs3RY6>?je>62ER2gnf+q0&0=$-MZ6`}Ea
zD`}I;W^)g`!L4NFq^MA?xEsbkB?%Q;cPBfOvtn9O^VEa2!q(tE73yHCs&8LSkeTGt
z=~>J^LDovtn(^GKaY;(v{*Esi&QskZ_z`b79HUtfZEg|@MV|NCR58LV?t-Pr^F`6J
zCZ(=!D=J)_meOfjLzPvvd5_(&JYmWRxf&)GgUIp-(H1zQ
zAeR;50Bw#3pc$->ptZYFc~03(?O3f!Gf6d2o2$I5Uv)Kz*Vh``qr#RZEvfvaf17b(xORzYN#&LPnIAoJ!wrYW548_+
z)@Khcs2o4GUqS!u#-f3afmK6egcD<@o}4fd)Prx!KLTH1@>~YwUh`5UZ#gH?B?*+rWE~7b^I18Kjr^EEbMCcy0Fg7S5n|!a6Exha*Pqq+LTk1<1iUMow-7HWeI2D)R7<
zSXHuuhp7!)ZMxXdv7;T+!e$qz_3oXP8(u%$l#>&wpJ2BY{zayXjt~~zCEq5rOK~U4Ur?CJYWLdV
zO=fe6rQ&w8rNnF&lu53nr1)Zbbs?1^V`-iu*=po~DAEOX<|eh-?o_AB5ul;XV{$i-
z&!XV3xqsl{Ygezm_R7b6%ST_hTQBkst*f`8I)75_*v6ugnDSm4L3!=@4jy|~?UgI;
z)=xj|$Wc8g
zHl=^`(Ls7OeN~>!&Z_}E=wbPJBEQ7$^6g7~gcO8GjXuV-F{iBW+I8+9)~%Xd)ou^o{^$eOc6E|5
zMXw!IHY(%(;aqW-`|_qcT$?v@RJe&G#6WDR5Y2BB$m3)b{o}W}zvU4^^nBuhWYmtT
z#GOBEOEfucSbjac0#Yz?(F-fE7Wm3jMipq;|ysZ9f2PRn3
zr@K80yFD{4p^q>!rLs@v;GEpXf%!#MF}W0z&tk`UxwHqTn7oo4WpDV~8;>V%&E`CI
zOF_vdXTR9q26I+smemwflrTV8+*WRt#aG^%P_Ii)n@|}uB?S<7Vkfpl+KF-6#4VXU
zzg*agtr)&DdgemQze|w=gT*F>bhLKK%EL#&&g9z|{k{Wr2Xk~Ol~#GGN_B@Y(|s{N
zpIh9=lWtC>rDj^sGC?KWM3xczQ2HnCX3T=>_M}Z?8y8LIF{^|%*KX$h=DfZb?MJY;
zA^f+|iX~g}?L6wtnYSOu^9MYm`BcwOWN+lOR-&tu%b_?OFu;9Gz3MV8>89`~LuB8O
zyhN8Ru{=qmRE4F7hfqCr7CXgWhX#a4;GzwR%kla``@pHEc)u!O34P2>*pDvn@-~SZ
z{JqV*fp=B*+c{uwvPzX4B4mvkajLfVa6-1)Zbbeh8xS%eIY0U&hwH>3vQoMs;x=;T
zj3Ji&wKHc_^YF5Be#nGcj@lX$&HA^YY3-MRL{-+3ACFX&Vk)TK#<_}{7-U(zu
z5&T|)_+t-d6!W3(egydK?tCfEK~^D-V?xCyxp)}K{CgbQZGu8EX6F9-dTe{HYr4`z
zd$6(J%&`(i&m=w^xp^^8k9=M^(o~qKjJD!Bm>sS~zJsg{Md`H;Z6?;Fz9_xWUy9Ppr0BOS5j-Nwy?^|hVi*B2aAosdWxhB#e{Rhh(t=|2{5SfO=`#vep$d6U^Zt-KRT-xUP~E4t$W?=t|7XqN*#Ie(Ck
zS@M&z4b92f(fu@?a0djJlt%_zRAr^na@a??cb-%M94f7W{|G{NTN^O#AxTMv=JItW
zQ*V>WHvm6lbQlxk3h6c9*a_bz!q<>T7yut^Ey=Fd9soKkrz$I}DyKOk!v|QfrP~Ii
zr}ZfW&)`ou4E+#E`yd=Kz&3+-#`a;+l)|6fM^~pF;BtOI4D!48DMt_>3iS`6TnA^d
zo*Vc%ef};njl&mx~n0#1sVR
zD8>cz0>3k^*V(Tg4-}>?*!~Mj&D@SrPP`+hS#ssh_
zGD9(l>*P#`_qyjz<`MaO=1%xT;;BCbRf9F^4s`%6*ma=3?+=x9riNm5*Tq#@)$V`=
z?-E}J_CI}GEar~9lz`M<54xrYy}J+8$B*jQok<}5ujoU-db|3b*
zKPz5mk`n@$lRW$uet^FZ-bA+DTh2o%_hFN#PHb!(KV{0qhWgfq#)(s>jB99^I0e}k
zikF>4ZtW0!HgF;K*@(PI!8a@<61Eh`^<~+3L>zocd*Omx!nCmm?EUATqioyQcww>3
zwR|uVZbJF8YCh?%cPQyrUr|2ML>dgStM5R6QEPY43z1Fk%ngw}?w%jwe}E4Rb-AR@<9;}3VYEGh{EntbPAxvoejkZ
zen`$9V=?BXAdS%c`rOiyW5y&Eu9FoHSej%iwz)HrP*9Jro%CpyJY&IxdMIjm(SUy#
zI~vL$*)Xp^kV6vvx8KPlg(E-3>wi0!#CK!E^uHS!{PlYoth3mT{ocqzy5Gnh;+!-7
z;GDg-`gf+?UNMsrdcQk=pkXhhuT2Dc+u~n#_tJ?CHZi>PsB~yH~kUZ{C(WtS-yDN*=hzA7UWpkUL4J){*o>e0Yi3pDA?~4xDqQcwX=|EOR^gl$cL77ohIQ(XAlu`&+y5~oqApOuI
zN18}cls^tR1*)J2WImS^Ko8IliBeB>P>01Fd<^aGe-q~vNz{7+H+u!V2mHOFRExd_
zMrarF#b%tv`Ry^B-QD&cqfPRck{%-_1c-)!6AO&JB*vu>W%haT$f_
zIIyDp|MS8YL~_8n$c(#EZOQ@{Wp@lNoj3H20E9t|Gs5ycnVKvMkDsYGr){d5q@Fx;
z!TB57W22)TYJP`8mSS*C3{%`<%$XB1`H9=szdN~pamI+`tc=0rxlr`^1m>?6N21*Z
zOlW7i*40D3{-rD9`lP=$^-pXgbNhCH0`+q6ag5{h_%TxVY%J}uV*E3*eQBK*cF)TG
zwUclE3q11m1K`b}HSGb9j|IR^3_Cjg{Oe%o^}zywrg%A^C)`m|4T~hbK3vJizaHcs
zgbo;_)t}ToM;aSQZAs>igA2R-`|FI^V|N)a(0Zl?h^X
zytH|xGHBBI7XApuv)x}%EiPT3E^~h^R^sw6eYLz%o|F6t&fkdMKJk4wZ@Nc!sL>!^$NsuUu9-HR
zUHN7scrP&LEJPEZ{5L4XbX`zRfaAJBxz`^`2XMjuw+Aj>M(YAJQR1?0uy*=5Oqy9c
zpv96z_kh>K73qSE2w!KSsvG#IfJKI{Pnm5W{d(lKNpbRS`Rz86xNbS_V;}%g5p@;F
zCyR9u-nuDi6VR<}syG|PPz^@0pcSrWPA0@gY
zofbd}MC$M-*89NlcdPdi<1J!#w6pHVXxu+|aA1MT&r{OUrj&u7a0U)FmDqvO=_Jug
z^;Ej3r_J}@;Z}BEjSEEx2DlM3Xw}oSdW|5OC#BdP|oE
z`wE|4=w8F*PW9D7CHpF(#F|nwVr*h0(Z3?fCAloVV&l7f0k_VYV=3|`)>I*j*YTSW
zd?i))`iy;NO_gNziJM1osiF!ycZwy8i7qM=5i9x;>#(HyRad=pqPiwI!Rjlra-Z#2
z>C9B5$AhnsTISEfW5ur62M3jeIGTo&B*?VzXHS;N_Z^VuBxDavRz}B%M`8gkpIH)@
z5xGwv2ho>y)=yx)f}{!(RMI${lm!!OqbtswK|QFk`f;Bo+XERC&&~MNc2UyISKq}~
zojKE4<8_F#XyAQLRoB?>Bgzf81Uq*)C5S3wgv`uC1qF9M9=|o7r=G;mfowx#Nahdh
zUl<3GUYB17ZzY+bB~5(X0Bc`WwZfGVgr8fqZx~CVp{9QaGIz*~1ZJ
z@QXM~5f!3lr^fFlr#MIk0Dcu@XGmFf_hTe&9Sa$tx@Dc{FWC)domFo@0fcxa3L%E>
zk*LV54DNhL6|{PdJmO*Z*|f}+ZC%kQ)t0n^)kWExRVV=W4@N=4qA<>GtiB%72v3<~
zSK_}}LPC+wC@B2QpiA03@u``Uj(K-V_h;H(YHXT4rc|_uuVSOY|D5)$v_;CyT*8%p
zR?$NHCGvhKn*IT(Of}wA<8j?8Z!&H@E4}X|%6G5!=5NpnQ1)3(K?tU-SuCh4y%1(GbYJ`i_z)tS^)F6ie
z65f*_6|PBf@spfAQ0Dzs=H;yvB@Zxr3!4C+GWir(@2uJ*FBnk$k*zDLPe`cm?N`J1
z`g(_BJ?1Ue)62rK+q8;)W#{QxxRYHK^|apv$m5dWSMzr+Bc*JB#bo5MC3f18yO!dI33*5a;5OZ*88B5By5>ew5}a
zR_gI~X?Cb3ghiYMi-Ay?ws(k2p{SYT
z)Tc!YK_#l9IK|K{st%kPD`*%zs3zp@knr9O?mt)604=c^ev`P5XEDQoyI1rV4!FvSmL;Zr=So~#BaLspHb7a9s35H`7LlLOa+c_oh>r=H*oI-^O#g3gE$XGf83pM<4{
zMKMq2~
z@;_s5EuH6sMnjU;#jUMlt9k5_@xz2IbU^(N&eV6BP0ADEoJWd7W({8OYn%HY({gdV
z>DrdNI9YWQgkAyrA?Ak1=zTY*<1ta=o~0cH}Q{bX(K*#MMu@f6~13pB`kJ-
zGq^cdzUJm(3!3BN2R}8geT}?nCE!u4{VTXa0Z(adj=Lt>kS@PR`;Ban(ujU=%sHo6kzHt@4H++4-
zPZo3{()p7G1Ys#4{N`^S=mG$Ao?`C=0F4Sv`F;^o;LwR{=bOM^P4MB~34rRIX7RW8+rk+B4Mc
z%-VbIq4gYzU#%FzAE7!qmqqQct6yEd{H8-(=DtSuwgSHk@>lU3I(gllr)Yg=(tY;|
z+JJvfFJIA%bV_}=xDA(H@tczLO9b+2u_DL!y9t9)TP4=z{BM3cA?W|)R~5MW|Kyhz
zuK%+PL|>|3)QVMDzUoD(Y|&e=h!#|*l_It9R%!8+fWoZ6lTlR16mZ&KI(>Qpl%Wf5DRx9TEOa*YL~^Oq$@jd4^Qg{-
zq`EvDPkpH_2^<{5V^En)r{TyK5_JQP_M;9!Deg-&V)^>VMPo{X{>FRP$c;4#LBqr5
zCdlLG2yt#V7e=`cJ%&F%`s7JK)57YX0j88ilp>MV0u);UJ^%RQF~lF8JNfN_fyYtX
z-J1jBjpB(P{PH8!Cot)(o}{~kLf4_X!+nHYs=t@6O`tT@M!qRb7i;3J2Ax>zh$?3@
zKew-G;Td<8V%J_dKfzyHm9wQ|-t%V`()#7IPsKV?pS}~LPM=csWTa=A%qHtY`=S&L
zw7z(1fSc~&voz=5IQ7u+>N&!z>wbMfsB_-|Ny=iwg*o_*p=c?8if9zA6l|GTwrD}D
z+K_6Y^9?xRk0r}LJHjrv-!QPYcHro|AlJ5$qqZjxPELC($`)y|rSO-ls~e6r)YQ~J
zKJA(Q>({56DsX1p^OL8I|5MgalvGU_FXr@f0pg*7GVUsqxtw=u6bn}s2M^1a>l6#_
zbfOH>oSmc&QtamsEn2#&vew;a?cBuiF