diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..f9e953b
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,46 @@
+name: "CodeQL Advanced"
+
+on:
+ push:
+ branches: [ "develop", "master" ]
+ pull_request:
+ branches: [ "develop", "master" ]
+ schedule:
+ - cron: '16 0 * * 2'
+
+jobs:
+ analyze:
+ name: Analyze (${{ matrix.language }})
+ runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
+ permissions:
+ security-events: write
+ packages: read
+ actions: read
+ contents: read
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - language: java-kotlin
+ build-mode: manual
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: ${{ matrix.language }}
+ build-mode: ${{ matrix.build-mode }}
+ queries: security-extended,security-and-quality
+
+ - if: matrix.build-mode == 'manual'
+ shell: bash
+ run: |
+ ./gradlew assembleDebug testDebug
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
+ with:
+ category: "/language:${{matrix.language}}"
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..25628e3
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,49 @@
+# Contributing to SysctlGUI
+
+First off, thank you for considering contributing to SysctlGUI! Your help is greatly appreciated. Whether it's reporting a bug, discussing improvements, or contributing code, every bit of effort makes this project better.
+
+This document provides a set of guidelines for contributing to SysctlGUI. These are mostly guidelines, not strict rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
+
+## How Can I Contribute?
+
+There are many ways to contribute, and we welcome them all!
+
+### Reporting Bugs
+
+If you find a bug, please make sure it hasn't already been reported by searching the [GitHub Issues](https://github.com/Lennoard/SysctlGUI/issues).
+
+When you are creating a bug report, please include as many details as possible:
+
+* **A clear and descriptive title** for the issue
+* **Steps to reproduce the bug** in a clear, step-by-step list
+* **Describe the expected behavior** and what you observed instead
+* **Provide details about your environment:**
+ * App version
+ * Android version and ROM (e.g., Android 13, LineageOS 20)
+ * Device model
+ * Root solution (e.g., Magisk, KernelSU)
+* **Include logs** (like a Logcat) or screenshots if they are relevant
+
+### Suggesting Enhancements
+
+If you have an idea for a new feature or an improvement to an existing one, please open an issue to start a discussion.
+
+* **Use a clear and descriptive title** for the issue
+* **Provide a detailed description** of the enhancement and why it would be useful
+* **Explain the problem it solves** or the workflow it improves
+* **Include mockups or screenshots** if they help illustrate your idea
+
+
+### Pull Requests
+
+Code contributions are always welcome. If you can contribute with code, please do so.
+
+
+### Translations
+If you are fluent in another language, your help with translations would be invaluable.
+Please see the [translation guide](https://github.com/Lennoard/SysctlGUI/blob/develop/TRANSLATING.md) for detailed instructions on how to contribute translations.
+
+
+
+# License
+By contributing to SysctlGUI, you agree that your contributions will be licensed under the [License](https://github.com/Lennoard/SysctlGUI/blob/develop/LICENSE) that covers the project.
diff --git a/README.md b/README.md
index 884dd9b..665bf89 100644
--- a/README.md
+++ b/README.md
@@ -1,42 +1,75 @@
-
-
-
-
-
-
-
-
+
# SysctlGUI
+SysctlGUI is a Android application designed for power users, developers, and enthusiasts who want to
+fine-tune their device's performance by directly editing kernel parameters.
+It provides a user interface for the sysctl command-line utility,
+making advanced kernel tweaking accessible and manageable.
+
+***Important: This app requires root access to work.***
+
[](https://www.codacy.com/gh/Lennoard/SysctlGUI/dashboard?utm_source=github.com&utm_medium=referral&utm_content=Lennoard/SysctlGUI&utm_campaign=Badge_Grade)
-
-
-
-
-
+[](https://github.com/Lennoard/SysctlGUI/actions/workflows/codeql.yml)
+[](https://app.bitrise.io/app/03e8fa82-8168-4a7f-9005-b8e5d056417f)
-A GUI application for Android sysctl to edit kernel variables
+
+
+
+
## Features
-- Browse filesystem for specific kernel parameters
-- Select parameters from a searchable list
-- Information about known parameters
-- Load parameters from a configuration file
-- Reapply parameters at startup
-- Mark parameters as favorite for easy access
+- **Parameter Management:** Easily browse the filesystem or search a comprehensive list to find kernel parameters, with in-app documentation to help you understand their impact.
+- **Persistent Tweaks:** Automatically reapply your chosen settings on every boot.
+- **Configuration Profiles:** Save and load sets of parameters from configuration files, making it simple to switch between different performance profiles or share your setup.
+- **Favorites System:** Mark frequently used parameters for quick and easy access.
+- **Tasker Integration:** Automate the application of kernel parameters in response to specific events using [Tasker](https://tasker.joaoapps.com/). SysctlGUI provides a Tasker plugin, allowing you to trigger parameter application based on a wide range of conditions/states.
## Technologies
-- MVI / MVVM for user params
-- [Jetpack Compose](https://developer.android.com/jetpack/compose) Material 3 UI
-- [Jetpack Data Binding](https://developer.android.com/topic/libraries/data-binding)
-- [Jetpack View Binding](https://developer.android.com/topic/libraries/view-binding)
-- Lifecycle-aware Kotlin Coroutines
-- Kotlin Flows
-- Dependency injection with [Koin](https://insert-koin.io/)
+This project utilizes a modern Android development stack, leveraging a comprehensive suite of libraries and tools:
+
+- **Core & Architecture:**
+ - Architectural Patterns: MVI, reactive and maintainable.
+ - [Kotlin](https://kotlinlang.org/): For modern, concise, and safe programming.
+ - Android Jetpack:
+ - [Lifecycle](https://developer.android.com/jetpack/androidx/releases/lifecycle)
+ - [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel)
+ - [Navigation Component](https://developer.android.com/guide/navigation)
+ - [Room](https://developer.android.com/training/data-storage/room): For local data persistence.
+ - [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager): For deferrable, asynchronous tasks.
+- **UI Development:**
+ - [Jetpack Compose](https://developer.android.com/jetpack/compose): For building native UIs with a declarative approach.
+ - Compose Material 3 & Material Components
+ - [Jetpack Glance](https://developer.android.com/develop/ui/compose/glance): For creating App Widgets with Jetpack Compose.
+- **Asynchronous Programming:**
+ - [Kotlin Coroutines](https://kotlinlang.org/docs/coroutines-overview.html) & [Flows](https://kotlinlang.org/docs/flow.html): For efficient and structured background tasks and reactive data streams.
+- **Utilities:**
+ - [Koin](https://insert-koin.io/): Dependency injection framework for Kotlin.
+ - [Ktor Client](https://ktor.io/docs/client-reference.html): For making HTTP requests (used for parameter documentation).
+ - [Libsu](https://github.com/topjohnwu/libsu): For interacting with root services.
+ - [Jsoup](https://jsoup.org/): For parsing HTML (used for parameter documentation).
+ - [Kotlinx Serialization](https://github.com/Kotlin/kotlinx.serialization): For JSON serialization/deserialization.
+
+## Contributing
+
+Contributions are always welcomed. Please see the [contributing guide](CONTRIBUTING.md) for more details on how to contribute with this project.
+
+### Translations
+If you'd like to help translate the app into other languages, please see the [translation guide](TRANSLATING.md) for instructions on how to get started. Your contributions will help make SysctlGUI accessible to a wider audience.
+
+
+## Screenshots
+
+
+| | | | |
+|:------------------------------------------------------:|:------------------------------------------------------:|:------------------------------------------------------:|:-------------------------------------------------------:|
+|

|

|

|

|
+
+
+
## Download
@@ -47,7 +80,7 @@ A GUI application for Android sysctl to edit kernel variables
This project is licensed under the terms of the MIT license.
-> Copyright (c) 2019-2024 Lennoard.
+> Copyright (c) 2019-2025 Lennoard.
>
> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
>
diff --git a/TRANSLATING.md b/TRANSLATING.md
new file mode 100644
index 0000000..b0758f5
--- /dev/null
+++ b/TRANSLATING.md
@@ -0,0 +1,62 @@
+# Translating SysctlGUI
+
+Thank you for your interest in translating SysctlGUI! Your contributions help make the app accessible to a wider audience.
+
+## How to Contribute
+
+1. **Fork the Repository:** Start by forking the [SysctlGUI repository](https://github.com/your-github-username/SysctlGUI) (replace `your-github-username/SysctlGUI` with the actual repository URL) to your own GitHub account.
+2. **Clone Your Fork:** Clone your forked repository to your local machine.
+ ```bash
+ git clone https://github.com/YOUR_USERNAME/SysctlGUI.git
+ cd SysctlGUI
+ ```
+3. **Create a New Branch:** Create a new branch for your translation.
+ ```bash
+ git checkout -b translate-yourlanguage
+ ```
+4. **Translate the Files:**
+ * **String Resources:** These are the primary files for translation.
+ * `app/src/main/res/values/strings.xml`
+ * `app/src/main/res/values/params_info.xml`
+ * `data/src/main/res/values/strings.xml`
+
+ To translate these files, create a new `values-xx` directory in the same `res` folder, where `xx` is the ISO 639-1 code for the language you are translating to (e.g., `values-es` for Spanish, `values-de` for German). Then, copy the original `strings.xml` or `params_info.xml` into this new directory and translate the string values within the XML tags.
+
+ **Example (strings.xml for Spanish):**
+ Create `app/src/main/res/values-es/strings.xml` and translate the content, preserving special format tags (%s, %1$s, etc)
+ ```xml
+
+ SysctlGUI
+
+ Undo
+ Selected file: %s
+
+ Deshacer
+ Archivo seleccionado: %s
+
+ ```
+
+ * **Raw Text Files (Optional):** If you feel brave, you can also translate the `.txt` files located in `data/src/main/res/raw/`.
+ * When translating these files, it is **crucial** to respect their original format. These files often have a specific structure that the app relies on.
+ * Translate the text content, but leave any special characters, newlines, or formatting intact.
+ * Place the translated `.txt` files in a new `raw-xx` directory within `data/src/main/res/` (e.g., `data/src/main/res/raw-es/` for Spanish).
+
+5. **Commit Your Changes:** Commit your translated files with a clear commit message.
+ ```bash
+ git add .
+ git commit -m "Add translation to [Your Language]"
+ ```
+6. **Push to Your Fork:** Push your changes to your forked repository.
+ ```bash
+ git push origin translate-yourlanguage
+ ```
+7. **Create a Pull Request:** Go to the original SysctlGUI repository on GitHub and create a new Pull Request from your forked branch. Provide a clear description of your changes.
+
+## Important Notes
+
+* Ensure your translations are accurate and natural-sounding in the target language.
+* Do not translate resource names (e.g., `app_name` in ``). Only translate the text content between the XML tags.
+* For `.txt` files, preserving the exact original formatting is critical for the app to function correctly with the translated content.
+* If you are unsure about any part of the translation process, feel free to open an issue on the main repository to ask for clarification.
+
+Thank you for your contribution!
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 4aa5fe0..f94244f 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,11 +1,11 @@
-import org.jetbrains.kotlin.config.KotlinCompilerVersion
import java.util.Properties
plugins {
- id("com.android.application")
- kotlin("android")
- kotlin("kapt")
- id("com.google.devtools.ksp")
+ 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)
id("kotlin-parcelize")
}
@@ -17,10 +17,12 @@ android {
applicationId = AppConfig.appId
minSdk = AppConfig.minSdkVersion
targetSdk = AppConfig.targetSdkVersion
- versionCode = 16
- versionName = "2.2.2"
+ versionCode = 17
+ versionName = "3.0.0"
vectorDrawables.useSupportLibrary = true
- resourceConfigurations.addAll(listOf("en", "de", "pt-rBR"))
+ androidResources {
+ localeFilters += listOf("en", "de", "pt-rBR", "tr")
+ }
javaCompileOptions {
annotationProcessorOptions {
arguments += mapOf(
@@ -29,6 +31,7 @@ android {
)
}
}
+ multiDexEnabled = true
}
signingConfigs {
@@ -56,15 +59,13 @@ android {
signingConfig = signingConfigs.getByName("release")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro",
- "proguard-kt.pro"
+ "proguard-rules.pro"
)
}
}
buildFeatures {
viewBinding = true
- dataBinding = true
compose = true
}
@@ -93,41 +94,41 @@ 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(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)
- implementation(AndroidX.room)
- implementation(AndroidX.roomRuntime)
- implementation(AndroidX.workManager)
- ksp(AndroidX.roomCompiler)
-
- implementation(Google.gson)
-
- implementation(Dependencies.koinAndroid)
- implementation(Dependencies.libSuCore)
- implementation(Dependencies.libSuIo)
- implementation(Dependencies.liveEvent)
- implementation(Dependencies.tapTargetView)
-}
+ 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.activity.compose)
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.core.splashscreen)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.androidx.material)
+ implementation(libs.androidx.glance.appwidget)
+ implementation(libs.androidx.navigation.compose)
+ implementation(libs.androidx.window)
+ implementation(libs.androidx.work.runtime.ktx)
+ implementation(libs.androidx.multidex)
+
+ // 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(libs.koin)
+ implementation(libs.koin.compose)
+ implementation(libs.bundles.libsu)
+}
\ No newline at end of file
diff --git a/app/proguard-kt.pro b/app/proguard-kt.pro
deleted file mode 100644
index b7f8b24..0000000
--- a/app/proguard-kt.pro
+++ /dev/null
@@ -1,13 +0,0 @@
--dontusemixedcaseclassnames
-
--dontwarn kotlin.**
--keepclassmembers class **$WhenMappings {
- ;
-}
-
--assumenosideeffects class kotlin.jvm.internal.Intrinsics {
- static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
-}
-
-## Useless option for dex
--dontpreverify
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 8393bb1..28e9833 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -20,21 +20,20 @@
# hide the original source file name.
#-renamesourcefileattribute SourceFile
--keep class android.support.v7.widget.SearchView { *; }
--keep class androidx.appcompat.widget.SearchView { *; }
--keep class android.widget.SearchView { *; }
-
--keepclassmembers enum * { *; }
-
-# GSON config
--keep public class com.google.gson.**
--keep public class com.google.gson.** {public private protected *;}
--keepattributes *Annotation*,Signature
-
-# Gson specific classes
--keep class sun.misc.Unsafe { *; }
-
-# Application classes that will be serialized/deserialized over Gson
--keep class com.androidvip.sysctlgui.data.models.KernelParam { *; }
--keep class com.androidvip.sysctlgui.data.models.RoomKernelParam { *; }
--keep class com.androidvip.sysctlgui.domain.models.DomainKernelParam { *; }
\ No newline at end of file
+-keepnames class androidx.lifecycle.ViewModel
+-keepclassmembers class * extends androidx.lifecycle.ViewModel { (...); }
+-keepclassmembers class * implements androidx.lifecycle.LifecycleObserver { (...); }
+-keepclassmembers class * implements androidx.lifecycle.LifecycleOwner { (...); }
+-keepclassmembers class androidx.lifecycle.Lifecycle$State { *; }
+-keepclassmembers class androidx.lifecycle.Lifecycle$Event { *; }
+-keep class * implements androidx.lifecycle.LifecycleOwner { public (...); }
+-keep class * implements androidx.lifecycle.LifecycleObserver { public (...); }
+
+-keepclassmembers class com.androidvip.sysctlgui.ui.main.MainViewModel {
+ static void ();
+}
+
+-keep class org.koin.core.instance.InstanceFactory { *; }
+-keep class * extends org.koin.core.module.Module
+-keep class org.koin.core.registry.ScopeRegistry { *; }
+-keep class org.koin.android.scope.AndroidScopeComponent
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e683bad..9282f21 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">
-
-
-
-
-
+
-
+ android:resource="@xml/favorites_glance_widget_info" />
-
Unit) {
}
}
-fun Uri.readLines(context: Context?, forEachLine: (String) -> Unit) {
- context?.contentResolver?.openInputStream(this).readLines(forEachLine)
-}
-
-fun InputStream?.readLines(forEachLine: (String) -> Unit) {
- this?.use { inputStream ->
- inputStream.bufferedReader().use {
- it.readLines().forEach { line ->
- forEachLine(line)
- }
- }
- }
-}
-
@ExperimentalContracts
fun Bundle?.isValidTaskerBundle() : Boolean {
contract {
@@ -82,13 +34,3 @@ fun Bundle?.isValidTaskerBundle() : Boolean {
}
return this != null && containsKey(TaskerReceiver.BUNDLE_EXTRA_LIST_NUMBER)
}
-
-suspend inline fun Activity?.runSafeOnUiThread(crossinline uiBlock: () -> Unit) {
- this?.let {
- if (!it.isFinishing && !it.isDestroyed) {
- withContext(Dispatchers.Main) {
- runCatching(uiBlock)
- }
- }
- }
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/SysctlGuiApp.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/SysctlGuiApp.kt
index 70fb97a..0fb9ede 100644
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/SysctlGuiApp.kt
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/SysctlGuiApp.kt
@@ -1,8 +1,8 @@
package com.androidvip.sysctlgui
-import android.app.Application
+import androidx.multidex.MultiDexApplication
import com.androidvip.sysctlgui.data.di.dataModules
-import com.androidvip.sysctlgui.di.presentationModules
+import com.androidvip.sysctlgui.di.presentationModule
import com.androidvip.sysctlgui.domain.di.domainModule
import com.androidvip.sysctlgui.domain.repository.AppPrefs
import com.google.android.material.color.DynamicColors
@@ -10,14 +10,14 @@ import org.koin.android.ext.android.get
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
-class SysctlGuiApp : Application() {
+class SysctlGuiApp : MultiDexApplication() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@SysctlGuiApp)
- modules(dataModules + presentationModules + domainModule)
+ modules(dataModules + presentationModule + domainModule)
}
val prefs: AppPrefs = get()
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/core/navigation/TopLevelRoute.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/core/navigation/TopLevelRoute.kt
new file mode 100644
index 0000000..19ede53
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/core/navigation/TopLevelRoute.kt
@@ -0,0 +1,20 @@
+package com.androidvip.sysctlgui.core.navigation
+
+import androidx.compose.ui.graphics.vector.ImageVector
+
+/**
+ * Represents a top-level destination in the application's navigation.
+ *
+ * @param T The type of [UiRoute] this top-level route represents. This allows for specific
+ * route information to be associated with the top-level destination.
+ * @property name The human-readable name of the top-level destination, used for labels.
+ * @property route The actual [UiRoute] object that defines the navigation destination.
+ * @property selectedIcon The icon to display when this top-level route is currently selected.
+ * @property unselectedIcon The icon to display when this top-level route is not selected.
+ */
+data class TopLevelRoute(
+ 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..b148d2d 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,26 @@
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.widgets.FavoriteWidgetParamUpdater
+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.UpdateFavoriteWidgetUseCase
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.singleOf
+import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.module
-internal val presentationModules = module {
- viewModel { BrowseParamsViewModel(get(), get()) }
- viewModelOf(::ListParamsViewModel)
- viewModelOf(::UserParamsViewModel)
+internal val presentationModule = module {
+ singleOf(::MainViewModel)
+ viewModelOf(::SettingsViewModel)
+ viewModelOf(::ParamBrowseViewModel)
viewModelOf(::EditParamViewModel)
- viewModelOf(::MainViewModel)
- viewModel { ExportOptionsViewModel(get(), get(), get()) }
+ viewModelOf(::SearchViewModel)
+ singleOf(::PresetsViewModel)
+ viewModelOf(::UserParamsViewModel)
- single { FavoriteWidgetParamUpdater(androidContext()).getListener() }
+ factory { UpdateFavoriteWidgetUseCase(androidContext()) }
}
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..80bf0f6
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/components/ErrorContainer.kt
@@ -0,0 +1,98 @@
+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.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.androidvip.sysctlgui.R
+
+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 = stringResource(R.string.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..f41e419
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/AppNavHost.kt
@@ -0,0 +1,102 @@
+package com.androidvip.sysctlgui.ui.main
+
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.scaleIn
+import androidx.compose.animation.scaleOut
+import androidx.compose.foundation.layout.PaddingValues
+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.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
+import com.androidvip.sysctlgui.ui.user.UserParamsScreen
+
+@Composable
+internal fun AppNavHost(
+ modifier: Modifier = Modifier,
+ innerPadding: PaddingValues, // Handled in the outer scope, this is for inner scaffolds
+ navController: NavHostController,
+ startDestination: UiRoute = UiRoute.BrowseParams
+) {
+ NavHost(
+ modifier = modifier,
+ navController = navController,
+ startDestination = startDestination,
+ enterTransition = { scaleIn(initialScale = 0.9f) + fadeIn() },
+ exitTransition = { scaleOut(targetScale = 0.9f) + fadeOut() },
+ popEnterTransition = { scaleIn(initialScale = 1.1f) + fadeIn() },
+ popExitTransition = { scaleOut(targetScale = 1.1f) + fadeOut() }
+ ) {
+ 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(
+ outerScaffoldPadding = innerPadding,
+ onParamSelected = {
+ navController.navigate(UiRoute.EditParam(paramName = it.name))
+ }
+ )
+ }
+
+ composable {
+ SettingsScreen(
+ onNavigate = { route ->
+ navController.navigate(route)
+ }
+ )
+ }
+
+ 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..01a668d 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,91 +1,90 @@
package com.androidvip.sysctlgui.ui.main
import android.app.NotificationManager
-import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.os.Handler
-import android.view.MenuItem
+import androidx.activity.ComponentActivity
+import androidx.activity.SystemBarStyle
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
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 androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.androidvip.sysctlgui.core.navigation.UiRoute
+import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
+import com.androidvip.sysctlgui.domain.enums.Actions
+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 mainViewModel: MainViewModel 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)
-
- Handler(mainLooper).postDelayed(1000) {
- checkNotificationPermission()
- }
+ updateEdgeToEdgeConfiguration(prefs.forceDark)
- setUpNavigation()
- navigateFromIntent()
- }
+ setContent {
+ val themeState by mainViewModel.themeState.collectAsStateWithLifecycle()
+ val forceDark = themeState.forceDark
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- when (item.itemId) {
- android.R.id.home -> finish()
+ LaunchedEffect(forceDark) {
+ updateEdgeToEdgeConfiguration(forceDark)
+ }
- R.id.action_exit -> {
- moveTaskToBack(true)
- finish()
+ SysctlGuiTheme(
+ darkTheme = forceDark || isSystemInDarkTheme(),
+ contrastLevel = themeState.contrastLevel,
+ dynamicColor = themeState.dynamicColors
+ ) {
+ MainScreen(startDestination = getRouteFromIntent())
}
}
- return false // Let fragments have a chance to consume it
+ Handler(mainLooper).postDelayed(1000) {
+ checkNotificationPermission()
+ }
}
- override fun onSupportNavigateUp(): Boolean {
- return navHost.navController.navigateUp() || super.onSupportNavigateUp()
+ private fun updateEdgeToEdgeConfiguration(forceDark: Boolean) {
+ enableEdgeToEdge(
+ statusBarStyle = SystemBarStyle.auto(
+ lightScrim = Color.TRANSPARENT,
+ darkScrim = Color.TRANSPARENT,
+ detectDarkMode = { resources ->
+ val isSystemDark = resources.configuration.uiMode and
+ Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
+ forceDark || isSystemDark
+ }
+ )
+ )
}
- 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)
+ private fun getRouteFromIntent(): UiRoute {
+ val extraDestination = intent.getStringExtra(EXTRA_DESTINATION)
+ ?: return UiRoute.BrowseParams
- toolbar.setupWithNavController(navController, appBarConfiguration)
- navView?.setupWithNavController(navController)
- navRail?.setupWithNavController(navController)
- }
+ val extraParamName = intent.getStringExtra(EXTRA_PARAM_NAME)
- private fun navigateFromIntent() {
- val fragmentName = intent.getStringExtra(EXTRA_DESTINATION) ?: return
- when (fragmentName) {
- Actions.BrowseParams.name -> R.id.navigationBrowse
- Actions.ListParams.name -> R.id.navigationList
- Actions.ExportParams.name -> R.id.navigationExport
- Actions.OpenSettings.name -> R.id.navigationSettings
- else -> null
- }?.let { id ->
- navHost.navController.navigate(id)
+ return when (extraDestination) {
+ Actions.BrowseParams.name -> UiRoute.BrowseParams
+ Actions.ExportParams.name -> UiRoute.Presets
+ Actions.OpenSettings.name -> UiRoute.Settings
+ Actions.EditParam.name -> UiRoute.EditParam(extraParamName.orEmpty())
+ else -> UiRoute.BrowseParams
}
}
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,10 +93,8 @@ 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"
+ internal const val EXTRA_PARAM_NAME = "paramName"
}
}
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..b2115d6
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainNavBar.kt
@@ -0,0 +1,79 @@
+package com.androidvip.sysctlgui.ui.main
+
+import androidx.compose.animation.AnimatedContent
+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.platform.LocalContext
+import androidx.compose.ui.text.style.TextOverflow
+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.design.theme.SysctlGuiTheme
+
+@Composable
+internal fun MainNavBar(navController: NavHostController = rememberNavController()) {
+ val context = LocalContext.current
+ val topLevelRoutes = remember { TopLevelRouteProvider(context) }
+
+ 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 = {
+ AnimatedContent(targetState = selected) { selectedState ->
+ Icon(
+ imageVector = if (selectedState) {
+ route.selectedIcon
+ } else {
+ route.unselectedIcon
+ },
+ contentDescription = route.name,
+ )
+ }
+ },
+ label = {
+ Text(
+ text = route.name,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ },
+ 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/MainNavRail.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainNavRail.kt
new file mode 100644
index 0000000..e16feb2
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainNavRail.kt
@@ -0,0 +1,77 @@
+package com.androidvip.sysctlgui.ui.main
+
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.material3.Icon
+import androidx.compose.material3.NavigationRail
+import androidx.compose.material3.NavigationRailItem
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.style.TextOverflow
+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.design.theme.SysctlGuiTheme
+
+@Composable
+internal fun MainNavRail(navController: NavHostController = rememberNavController()) {
+ val context = LocalContext.current
+ val topLevelRoutes = remember { TopLevelRouteProvider(context) }
+
+ NavigationRail {
+ val navBackStackEntry by navController.currentBackStackEntryAsState()
+ val currentDestination = navBackStackEntry?.destination
+
+ topLevelRoutes.forEach { route ->
+ val selected = currentDestination
+ ?.hierarchy
+ ?.any { it.hasRoute(route.route::class) } == true
+
+ NavigationRailItem(
+ icon = {
+ AnimatedContent(targetState = selected) { selectedState ->
+ Icon(
+ imageVector = if (selectedState) {
+ route.selectedIcon
+ } else {
+ route.unselectedIcon
+ },
+ contentDescription = route.name,
+ )
+ }
+ },
+ label = {
+ Text(
+ text = route.name,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ },
+ selected = selected,
+ onClick = {
+ navController.navigate(route.route) {
+ popUpTo(navController.graph.findStartDestination().id) {
+ saveState = true
+ }
+ launchSingleTop = true
+ restoreState = true
+ }
+ }
+ )
+ }
+ }
+}
+
+@Composable
+@PreviewLightDark
+private fun MainNavbarPreview() {
+ SysctlGuiTheme(dynamicColor = true) {
+ MainNavRail()
+ }
+}
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..613c15e
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainScreen.kt
@@ -0,0 +1,140 @@
+package com.androidvip.sysctlgui.ui.main
+
+import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
+import androidx.compose.animation.AnimatedVisibility
+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.animation.slideInHorizontally
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutHorizontally
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarDuration
+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.Modifier
+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 com.androidvip.sysctlgui.design.utils.isLandscape
+import org.koin.androidx.compose.koinViewModel
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun MainScreen(
+ viewModel: MainViewModel = koinViewModel(),
+ startDestination: UiRoute = UiRoute.BrowseParams
+) {
+ 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,
+ duration = SnackbarDuration.Long
+ )
+ viewModel.onEvent(MainViewEvent.OnSnackbarResult(result))
+ }
+ }
+ }
+
+ MainScreenContent(state, navController, snackbarHostState, startDestination)
+}
+
+@Composable
+private fun MainScreenContent(
+ state: MainViewState,
+ navController: NavHostController,
+ snackbarHostState: SnackbarHostState,
+ startDestination: UiRoute = UiRoute.BrowseParams
+) {
+ val onBackPressedDispatcherOwner = LocalOnBackPressedDispatcherOwner.current
+ val isLandscape = isLandscape()
+
+ 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 = {
+ onBackPressedDispatcherOwner?.onBackPressedDispatcher?.onBackPressed()
+ }
+ )
+ }
+ },
+ bottomBar = {
+ AnimatedVisibility(
+ visible = state.showNavBar && !isLandscape,
+ enter = expandVertically() + slideInVertically { it / 2 } + fadeIn(),
+ exit = shrinkVertically() + slideOutVertically { it / 2 } + fadeOut(),
+ label = "BottomBar"
+ ) {
+ MainNavBar(navController = navController)
+ }
+ },
+ snackbarHost = {
+ SnackbarHost(snackbarHostState)
+ },
+ content = { innerPadding ->
+ Row(Modifier.padding(innerPadding)) {
+ if (isLandscape) {
+ AnimatedVisibility(
+ visible = state.showNavBar,
+ enter = expandHorizontally() + slideInHorizontally { it / 2 } + fadeIn(),
+ exit = shrinkHorizontally() + slideOutHorizontally { it / 2 } + fadeOut(),
+ label = "NavRail"
+ ) {
+ MainNavRail(navController = navController)
+ }
+ }
+
+ AppNavHost(
+ modifier = Modifier.weight(1f),
+ innerPadding = innerPadding,
+ navController = navController,
+ startDestination = startDestination
+ )
+ }
+ }
+ )
+}
+
+@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..ae10541
--- /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 = stringResource(R.string.go_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 = "Sysctl GUI",
+ 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..81950f6 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,58 @@
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
- )
+import androidx.compose.material3.SnackbarResult
+import androidx.lifecycle.viewModelScope
+import com.androidvip.sysctlgui.data.Prefs
+import com.androidvip.sysctlgui.data.repository.CONTRAST_LEVEL_NORMAL
+import com.androidvip.sysctlgui.domain.repository.AppPrefs
+import com.androidvip.sysctlgui.utils.BaseViewModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+
+class MainViewModel(
+ private val appPrefs: AppPrefs
+) : BaseViewModel() {
+ private val themeSettingsFlow: Flow = combine(
+ appPrefs.observeKey(Prefs.ForceDarkTheme.key, false),
+ appPrefs.observeKey(Prefs.DynamicColors.key, false),
+ appPrefs.observeKey(Prefs.ContrastLevel.key, CONTRAST_LEVEL_NORMAL)
+ ) { forceDark, dynamicColors, contrastLevel ->
+ ThemeSettings(forceDark, dynamicColors, contrastLevel)
+ }
+ val themeState = themeSettingsFlow.stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(5000),
+ initialValue = loadInitialThemeState()
)
- 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)
+ private fun loadInitialThemeState(): ThemeSettings {
+ return ThemeSettings(
+ forceDark = appPrefs.forceDark,
+ contrastLevel = appPrefs.contrastLevel,
+ dynamicColors = appPrefs.dynamicColors
+ )
+ }
+
+ 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..bc6443c
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/MainViewState.kt
@@ -0,0 +1,33 @@
+package com.androidvip.sysctlgui.ui.main
+
+import androidx.compose.material3.SnackbarResult
+import com.androidvip.sysctlgui.data.repository.CONTRAST_LEVEL_NORMAL
+
+data class MainViewState(
+ val topBarTitle: String = "Sysctl GUI",
+ val showTopBar: Boolean = true,
+ val showNavBar: Boolean = true,
+ val showBackButton: Boolean = false,
+ val showSearchAction: Boolean = true
+)
+
+data class ThemeSettings(
+ val forceDark: Boolean = false,
+ val dynamicColors: Boolean = false,
+ val contrastLevel: Int = CONTRAST_LEVEL_NORMAL
+)
+
+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/main/TopLevelRouteProvider.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/TopLevelRouteProvider.kt
new file mode 100644
index 0000000..cb5bb7f
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/main/TopLevelRouteProvider.kt
@@ -0,0 +1,46 @@
+package com.androidvip.sysctlgui.ui.main
+
+import android.content.Context
+import androidx.compose.material.icons.Icons
+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.Favorite
+import androidx.compose.material.icons.rounded.Home
+import androidx.compose.material.icons.rounded.Settings
+import com.androidvip.sysctlgui.R
+import com.androidvip.sysctlgui.core.navigation.TopLevelRoute
+import com.androidvip.sysctlgui.core.navigation.UiRoute
+
+object TopLevelRouteProvider {
+ operator fun invoke(context: Context): List> {
+ return listOf(
+ TopLevelRoute(
+ name = context.getString(R.string.browse),
+ route = UiRoute.BrowseParams,
+ selectedIcon = Icons.Rounded.Home,
+ unselectedIcon = Icons.Outlined.Home
+ ),
+ TopLevelRoute(
+ name = context.getString(R.string.presets),
+ route = UiRoute.Presets,
+ selectedIcon = Icons.Rounded.Build,
+ unselectedIcon = Icons.Outlined.Build
+ ),
+ TopLevelRoute(
+ name = context.getString(R.string.favorites),
+ route = UiRoute.Favorites,
+ selectedIcon = Icons.Rounded.Favorite,
+ unselectedIcon = Icons.Outlined.FavoriteBorder
+ ),
+ TopLevelRoute(
+ name = context.getString(R.string.settings),
+ route = UiRoute.Settings,
+ selectedIcon = Icons.Rounded.Settings,
+ unselectedIcon = Icons.Outlined.Settings
+ )
+ )
+ }
+}
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..eb7b3d2
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/DocumentationBottomSheet.kt
@@ -0,0 +1,158 @@
+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.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.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 = stringResource(R.string.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..faea61b
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowseScreen.kt
@@ -0,0 +1,368 @@
+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.grid.GridCells
+import androidx.compose.foundation.lazy.grid.GridItemSpan
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.lazy.grid.rememberLazyGridState
+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.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.rememberCoroutineScope
+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.res.stringResource
+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.Preview
+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.design.utils.isLandscape
+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.delay
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import org.koin.compose.viewmodel.koinViewModel
+import java.io.File
+import kotlin.time.Duration.Companion.seconds
+
+@OptIn(ExperimentalMaterialApi::class, 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)
+ },
+ isRefreshing = state.loading,
+ onRefresh = { viewModel.onEvent(ParamBrowseViewEvent.RefreshRequested) }
+ )
+
+ documentation?.let {
+ DocumentationBottomSheet(
+ documentation = it,
+ sheetState = documentationSheetState
+ )
+ }
+}
+
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+private fun ParamBrowseScreenContent(
+ params: List,
+ currentPath: String,
+ documentation: ParamDocumentation?,
+ onParamClicked: (UiKernelParam) -> Unit,
+ onDocumentationClicked: (ParamDocumentation) -> Unit,
+ backEnabled: Boolean = false,
+ onBackPressed: () -> Unit,
+ isRefreshing: Boolean,
+ onRefresh: () -> Unit
+) {
+ val listState = rememberLazyGridState()
+ val pullRefreshState =
+ rememberPullRefreshState(refreshing = isRefreshing, onRefresh = onRefresh)
+ 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()
+ .pullRefresh(pullRefreshState),
+ ) {
+ val spacerHeight by animateDpAsState(if (finalHeaderVisible) 56.dp else 0.dp)
+ val columns = if (isLandscape()) 2 else 1
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(columns),
+ state = listState,
+ modifier = Modifier.fillMaxSize()
+ ) {
+ item(span = { GridItemSpan(columns) }) {
+ Spacer(modifier = Modifier.height(spacerHeight))
+ }
+
+ items(
+ items = params,
+ key = { param -> param.name }
+ ) { param ->
+ ParamFileRow(
+ modifier = Modifier.animateItem(),
+ param = param,
+ showFavoriteIcon = true,
+ onParamClicked = onParamClicked,
+ )
+ }
+
+ if (documentation != null) {
+ item(span = { GridItemSpan(columns) }) {
+ 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 = stringResource(
+ R.string.read_documentation_format,
+ documentation?.title.orEmpty()
+ ),
+ textStyle = MaterialTheme.typography.titleSmall.copy(
+ textDecoration = TextDecoration.Underline,
+ color = MaterialTheme.colorScheme.primary
+ ),
+ icon = painterResource(R.drawable.ic_documentation),
+ onClicked = { onDocumentationClicked(documentation!!) }
+ )
+ }
+
+ PullRefreshIndicator(
+ refreshing = isRefreshing,
+ state = pullRefreshState,
+ modifier = Modifier.align(Alignment.TopCenter),
+ backgroundColor = MaterialTheme.colorScheme.surfaceContainerHighest,
+ contentColor = MaterialTheme.colorScheme.tertiary
+ )
+ }
+}
+
+@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
+@Preview(device = "spec:parent=pixel_5,orientation=landscape")
+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()))
+ }
+ var isRefreshingPreview by remember { mutableStateOf(false) }
+ val scope = rememberCoroutineScope()
+
+ 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 },
+ isRefreshing = isRefreshingPreview,
+ onRefresh = {
+ scope.launch {
+ isRefreshingPreview = true
+ delay(2.seconds)
+ isRefreshingPreview = false
+ }
+ }
+ )
+ }
+ }
+}
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..ec5dd8d
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowseState.kt
@@ -0,0 +1,25 @@
+package com.androidvip.sysctlgui.ui.params.browse
+
+import com.androidvip.sysctlgui.domain.models.ParamDocumentation
+import com.androidvip.sysctlgui.models.UiKernelParam
+
+data class ParamBrowseState(
+ val loading: Boolean = false,
+ 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
+ object RefreshRequested : 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..30109b2
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamBrowseViewModel.kt
@@ -0,0 +1,147 @@
+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.domain.usecase.GetUserParamsUseCase
+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 getUserParams: GetUserParamsUseCase,
+ private val appPrefs: AppPrefs,
+ private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
+) : BaseViewModel() {
+ private val userParams = mutableListOf()
+
+ override fun createInitialState() = ParamBrowseState()
+
+ init {
+ viewModelScope.launch {
+ setState { copy(loading = true) }
+ val startingDirectory = File(Consts.PROC_SYS)
+ val params = getParams(startingDirectory)
+
+ runCatching {
+ userParams.clear()
+ userParams.addAll(getUserParams())
+ }
+
+ setState {
+ copy(
+ params = params,
+ currentPath = startingDirectory.absolutePath,
+ loading = false
+ )
+ }
+ }
+ }
+
+ 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)
+ ParamBrowseViewEvent.RefreshRequested -> handleRefreshRequested()
+ }
+ }
+
+ private fun handleRefreshRequested() {
+ val currentPath = currentState.currentPath
+ if (currentPath == Consts.PROC_SYS) return
+
+ fetchChildParams(currentPath)
+ }
+
+ private fun fetchChildParams(parentParam: UiKernelParam) {
+ viewModelScope.launch {
+ setState { copy(loading = true) }
+ runCatching {
+ val newParamsDeferred = async {
+ getParams(File(parentParam.path))
+ }
+ val directoryDocumentationDeferred = async {
+ runCatching { getParamDocumentation(parentParam) }.getOrNull()
+ }
+
+ val newParams = newParamsDeferred.await()
+ val directoryDocumentation = directoryDocumentationDeferred.await()
+
+ setState {
+ copy(
+ params = newParams,
+ currentPath = parentParam.path,
+ backEnabled = parentParam.path != Consts.PROC_SYS,
+ documentation = directoryDocumentation,
+ loading = false
+ )
+ }
+ }.onFailure {
+ setEffect { ParamBrowseViewEffect.ShowError(it.message ?: "Unknown error") }
+ setState { copy(loading = false) }
+ }
+ }
+ }
+
+ 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 { fileParam ->
+ UiKernelParamMapper.map(fileParam).copy(
+ isFavorite = userParams
+ .filter { it.isFavorite }
+ .any { it.name == fileParam.name }
+ )
+ }
+
+ 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..72f7106
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamFileRow.kt
@@ -0,0 +1,160 @@
+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.res.stringResource
+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) {
+ stringResource(R.string.acessibility_directory_description_format, param.name)
+ } else {
+ stringResource(R.string.acessibility_param_description_format, 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 = stringResource(R.string.acessibility_param_icon_description),
+ 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 = stringResource(R.string.acessibility_davegate_to_directory_description),
+ modifier = Modifier.size(24.dp),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ } else if (param.isFavorite && showFavoriteIcon) {
+ Icon(
+ imageVector = Icons.Rounded.Favorite,
+ contentDescription = stringResource(R.string.favorites),
+ 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..c47fd07
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/browse/ParamRow.kt
@@ -0,0 +1,111 @@
+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.res.stringResource
+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.R
+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 = stringResource(
+ R.string.acessibility_param_description_format,
+ param.name
+ )
+ val rowState = if (param.isFavorite) stringResource(R.string.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..a8819e4
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/ActionToggleButton.kt
@@ -0,0 +1,181 @@
+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.res.stringResource
+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 = stringResource(
+ R.string.toggle_format,
+ contentDescription.orEmpty()
+ ),
+ 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 = stringResource(R.string.mark_as_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 = stringResource(R.string.toggle_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 3719b12..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditKernelParamActivity.kt
+++ /dev/null
@@ -1,113 +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)
- }
-
- 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) {
- 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/EditParamLandscapeContent.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditParamLandscapeContent.kt
new file mode 100644
index 0000000..7a40198
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/params/edit/EditParamLandscapeContent.kt
@@ -0,0 +1,267 @@
+package com.androidvip.sysctlgui.ui.params.edit
+
+import android.content.ClipData
+import android.widget.Toast
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.background
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+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.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.AssistChip
+import androidx.compose.material3.Icon
+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.rememberCoroutineScope
+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.input.KeyboardType
+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.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.utils.performHapticFeedbackForToggle
+import kotlinx.coroutines.launch
+import org.intellij.lang.annotations.Language
+
+@Composable
+internal fun EditParamLandscapeContent(
+ 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 copyParamContentToClipboard = {
+ val clipData = ClipData.newPlainText(
+ context.getString(R.string.kernel_params),
+ "${param.lastNameSegment}=${param.value} (${param.path})"
+ )
+ val clipEntry = ClipEntry(clipData)
+ coroutineScope.launch {
+ clipboardManager.setClipEntry(clipEntry)
+ }
+ Toast.makeText(
+ context,
+ context.getString(R.string.copied_to_clipboard),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+
+ Row {
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .background(MaterialTheme.colorScheme.background)
+ .verticalScroll(rememberScrollState())
+ ) {
+ Text(
+ text = param.lastNameSegment,
+ style = MaterialTheme.typography.displayMedium,
+ modifier = Modifier
+ .combinedClickable(
+ enabled = true,
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() },
+ onClick = {
+ Toast.makeText(
+ context,
+ context.getString(R.string.long_press_to_copy),
+ Toast.LENGTH_SHORT
+ ).show()
+ },
+ onLongClick = copyParamContentToClipboard
+ )
+ .padding(start = 16.dp, end = 16.dp, top = 24.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
+ ) {
+ 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)
+ )
+ }
+
+ AnimatedVisibility(visible = param.isTaskerParam && state.taskerAvailable) {
+ val listName = taskerListNameResolver(param.taskerList)
+ AssistChip(
+ onClick = { onTaskerClicked(true) },
+ modifier = Modifier.padding(16.dp),
+ label = { Text(text = stringResource(R.string.tasker_list_format, listName)) },
+ leadingIcon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_tasker),
+ contentDescription = stringResource(R.string.tasker_list),
+ tint = MaterialTheme.colorScheme.tertiary
+ )
+ }
+ )
+ }
+ }
+
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .background(MaterialTheme.colorScheme.surfaceContainer)
+ .verticalScroll(rememberScrollState())
+ ) {
+ 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
+@Preview(device = "spec:parent=pixel_5,orientation=landscape")
+private fun EditParamContentPreview() {
+
+ @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"
+ ),
+ )
+ EditParamLandscapeContent(
+ 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 }
+ )
+ }
+ }
+}
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..016586d 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,655 @@
package com.androidvip.sysctlgui.ui.params.edit
-import androidx.annotation.DrawableRes
+import android.content.ClipData
+import android.widget.Toast
+import androidx.activity.compose.BackHandler
+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.interaction.MutableInteractionSource
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.stringArrayResource
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.design.utils.isLandscape
+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 = stringArrayResource(R.array.tasker_lists).toList()
+ var showSelectTaskerListDialog by rememberSaveable { mutableStateOf(false) }
+ var selectedOptionIndex by rememberSaveable {
+ mutableIntStateOf(Consts.LIST_NUMBER_PRIMARY_TASKER)
}
+ var errorMessage by rememberSaveable { mutableStateOf("") }
+ var showError by rememberSaveable { mutableStateOf(false) }
+ val appBarTitle = stringResource(R.string.edit_params)
- 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 = appBarTitle,
+ 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) {
+ val successMessage = stringResource(R.string.value_applied_successfully)
+ val undoText = stringResource(R.string.undo)
+ 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 = successMessage,
+ actionLabel = undoText
+ )
)
-
- if (result == SnackbarResult.ActionPerformed) {
- viewModel.onEvent(EditParamViewEvent.ResetPressed)
- }
}
- else -> Unit
}
}
}
+
+ if (isLandscape()) {
+ EditParamLandscapeContent(
+ state = state.value,
+ showError = showError,
+ errorMessage = errorMessage,
+ onValueApply = {
+ viewModel.onEvent(EditParamViewEvent.ApplyPressed(it))
+ },
+ onTaskerClicked = {
+ showSelectTaskerListDialog = it
+ 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()
+ }
+ )
+ } else {
+ EditParamContent(
+ state = state.value,
+ showError = showError,
+ errorMessage = errorMessage,
+ onValueApply = {
+ viewModel.onEvent(EditParamViewEvent.ApplyPressed(it))
+ },
+ onTaskerClicked = {
+ showSelectTaskerListDialog = it
+ 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 = stringResource(R.string.select_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(
+ context.getString(R.string.kernel_params),
+ "${param.lastNameSegment}=${param.value} (${param.path})"
+ )
+ val clipEntry = ClipEntry(clipData)
+ coroutineScope.launch {
+ clipboardManager.setClipEntry(clipEntry)
+ }
+ Toast.makeText(
+ context,
+ context.getString(R.string.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,
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() },
+ onClick = {
+ Toast.makeText(
+ context,
+ context.getString(R.string.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
+ AnimatedVisibility(visible = param.isTaskerParam && state.taskerAvailable) {
+ val listName = taskerListNameResolver(param.taskerList)
+ AssistChip(
+ onClick = { onTaskerClicked(true) },
+ modifier = Modifier.padding(16.dp),
+ label = { Text(text = stringResource(R.string.tasker_list_format, listName)) },
+ leadingIcon = {
+ Icon(
+ painter = painterResource(R.drawable.ic_tasker),
+ contentDescription = stringResource(R.string.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)
+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
+
+ BackHandler(
+ enabled = isEditing,
+ onBack = { isEditing = false }
+ )
+
+ 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 = stringResource(R.string.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 = stringResource(R.string.apply_param),
+ tint = MaterialTheme.colorScheme.primary
+ )
+ } else {
+ Icon(
+ imageVector = Icons.Rounded.Edit,
+ contentDescription = stringResource(R.string.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
- )
+internal 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(stringResource(R.string.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
+internal fun ParamDocs(
+ modifier: Modifier = Modifier,
+ documentation: ParamDocumentation?,
+ onReadMorePressed: () -> Unit,
) {
- var typedValue by rememberSaveable { mutableStateOf(param.value) }
+ HorizontalDivider()
- Column(modifier = Modifier.padding(16.dp)) {
+ Column(modifier = modifier) {
Text(
- text = stringResource(id = R.string.value),
- style = MaterialTheme.typography.headlineMedium,
- color = MaterialTheme.colorScheme.onBackground
- )
-
- Text(
- modifier = Modifier.padding(top = 16.dp),
- text = stringResource(id = R.string.current_value),
+ text = stringResource(R.string.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
- )
+ AnimatedContent(targetState = documentation != null) { documentationAvailable ->
+ if (documentationAvailable && documentation != null) {
+ DocumentationContent(
+ documentation = documentation,
+ onReadMorePressed = onReadMorePressed
+ )
+ } 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(
+ imageVector = Icons.Rounded.Warning,
+ contentDescription = stringResource(android.R.string.dialog_alert_title),
+ tint = MaterialTheme.colorScheme.onErrorContainer
+ )
+ Text(
+ text = stringResource(R.string.no_info_available),
+ style = MaterialTheme.typography.bodyLarge.copy(),
+ fontWeight = FontWeight.Medium,
+ color = MaterialTheme.colorScheme.onErrorContainer
+ )
+ }
+ }
+ }
}
}
}
@Composable
-private fun ParamActions(
- onFavoriteClicked: () -> Unit,
- onTaskerClicked: () -> Unit,
- param: KernelParam,
- taskerAvailable: Boolean
+internal fun DocumentationContent(
+ documentation: ParamDocumentation,
+ onReadMorePressed: () -> Unit
) {
- 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)
+ Column {
+ 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)
+ }
+ SelectionContainer {
Text(
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,
+ .fillMaxWidth()
+ .padding(vertical = 8.dp),
+ text = documentationText,
style = MaterialTheme.typography.bodyMedium,
- maxLines = 2,
- overflow = TextOverflow.Ellipsis,
- color = MaterialTheme.colorScheme.onBackground
+ color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
- if (taskerAvailable) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- 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)
- ) {
- Icon(
- painter = painterResource(id = R.drawable.ic_action_tasker),
- contentDescription = stringResource(id = R.string.set_favorite),
- tint = MaterialTheme.colorScheme.onPrimaryContainer
- )
- }
- 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
- )
- }
+ TextButton(
+ onClick = onReadMorePressed,
+ modifier = Modifier
+ .padding(vertical = 8.dp)
+ .align(Alignment.End)
+ ) {
+ Text(text = stringResource(R.string.read_more))
}
}
}
@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..fc92295 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,168 @@
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.StringProvider
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.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.utils.BaseViewModel
-import java.io.InputStream
+import com.androidvip.sysctlgui.widgets.UpdateFavoriteWidgetUseCase
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 updateFavoriteWidget: UpdateFavoriteWidgetUseCase,
+ private val stringProvider: StringProvider,
+ 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))
+ init {
+ viewModelScope.launch {
+ if (paramName.isNullOrEmpty()) return@launch setEffect { EditParamViewEffect.GoBack }
+
+ val param = runCatching { getUserParam(paramName) }.getOrNull()
+ ?: getRuntimeParam(paramName)
+ ?: return@launch setEffect { EditParamViewEffect.GoBack }
+
+ setState {
+ copy(
+ kernelParam = UiKernelParamMapper.map(param),
+ taskerAvailable = isTaskerInstalled(),
+ keyboardType = guessKeyboardType(param.value)
+ )
}
- EditParamViewEvent.TaskerPressed -> {
- setEffect { EditParamViewEffect.ShowTaskerListSelection }
+
+ val documentation = runCatching { getDocumentation(param) }.getOrNull()
+ setState {
+ copy(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
-
- setState {
- copy(
- param = param,
- restoreValue = param.value,
- typedValue = param.value,
- paramInfo = findParamInfo(param, context),
- taskerAvailable = isTaskerInstalled(context),
- keyboardType = keyboardType,
- singleLine = singleLine
- )
+ override fun createInitialState() = EditParamViewState()
+
+ override fun onEvent(event: EditParamViewEvent) {
+ when (event) {
+ is EditParamViewEvent.ApplyPressed -> applyKernelParam(event.newValue)
+ is EditParamViewEvent.UndoRequested -> {
+ previousKernelParamValue?.let { applyKernelParam(it, true) }
+ }
+ 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, isUndo: Boolean = false) {
+ val oldParam = currentState.kernelParam
viewModelScope.launch {
+ val newParam = oldParam.copy(value = newValue)
runCatching {
- applyParams(param)
- updateUserParam(param)
+ applyParam(newParam)
+ upsertUserParam(newParam)
+ }.onSuccess {
+ setState { copy(kernelParam = newParam) }
+ if (!isUndo) {
+ setEffect {
+ EditParamViewEffect.ShowApplySuccess(previousValue = oldParam.value)
+ }
+ }
+ previousKernelParamValue = oldParam.value
+ updateFavoriteWidget()
}.onFailure {
- val messageRes = when (it) {
- is ApplyValueException -> R.string.apply_value_error
- is CommitModeException -> R.string.commit_value_error
- else -> R.string.error
+ Log.e("EditParamViewModel", "Failed to apply param", it)
+ val message = when (it) {
+ is BlankValueNotAllowedException -> stringProvider.getString(
+ R.string.apply_error_blank_values
+ )
+ is CommitModeException -> stringProvider.getString(
+ R.string.apply_error_commit_mode
+ )
+
+ is ApplyValueException -> stringProvider.getString(
+ R.string.apply_error_command_execution_failed
+ )
+ else -> it.message.orEmpty()
}
- setEffect { EditParamViewEffect.ShowApplyError(messageRes) }
- }.onSuccess {
- setEffect { EditParamViewEffect.ShowApplySuccess }
- setState {
- copy(param = param, hasApplied = param.value != currentState.restoreValue)
+ setEffect {
+ EditParamViewEffect.ShowError(message)
}
}
}
}
- 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..1184ab9
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/presets/ImportPresetScreen.kt
@@ -0,0 +1,493 @@
+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.res.stringResource
+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.R
+import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
+import com.androidvip.sysctlgui.design.utils.isLandscape
+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 = context.getString(R.string.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 -> {
+ if (isLandscape()) {
+ IncomingPresetsLandscapeContent(
+ paramsToImport = state.paramsToImport,
+ onImportPressed = {
+ viewModel.onEvent(PresetsViewEvent.ConfirmImportPressed)
+ },
+ onCancelPressed = {
+ viewModel.onEvent(PresetsViewEvent.CancelImportPressed)
+ }
+ )
+ } else {
+ 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 = stringResource(R.string.parameters_found_format, paramsToImport.size),
+ 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 = stringResource(android.R.string.cancel))
+ }
+
+ OutlinedButton(
+ modifier = Modifier.weight(1f),
+ onClick = onImportPressed,
+ enabled = paramsToImport.isNotEmpty(),
+ colors = ButtonDefaults.outlinedButtonColors(
+ containerColor = MaterialTheme.colorScheme.primary,
+ contentColor = MaterialTheme.colorScheme.onPrimary
+ )
+ ) {
+ Text(text = stringResource(R.string.import_text))
+ }
+ }
+ }
+}
+
+@Composable
+private fun IncomingPresetsLandscapeContent(
+ paramsToImport: List,
+ onImportPressed: () -> Unit,
+ onCancelPressed: () -> Unit
+) {
+ Row(
+ modifier = Modifier.fillMaxSize(),
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ 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)
+ )
+ }
+ }
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxHeight()
+ .weight(1f),
+ verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = stringResource(R.string.parameters_found_format, paramsToImport.size),
+ style = MaterialTheme.typography.headlineMedium,
+ color = MaterialTheme.colorScheme.onBackground,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 24.dp)
+ )
+
+ Row(
+ modifier = Modifier.padding(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally)
+ ) {
+ OutlinedButton(
+ modifier = Modifier.weight(1f),
+ onClick = onCancelPressed
+ ) {
+ Text(text = stringResource(android.R.string.cancel))
+ }
+
+ OutlinedButton(
+ modifier = Modifier.weight(1f),
+ onClick = onImportPressed,
+ enabled = paramsToImport.isNotEmpty(),
+ colors = ButtonDefaults.outlinedButtonColors(
+ containerColor = MaterialTheme.colorScheme.primary,
+ contentColor = MaterialTheme.colorScheme.onPrimary
+ )
+ ) {
+ Text(text = stringResource(R.string.import_text))
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun LoadingIndicator() {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(32.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically)
+ ) {
+ CircularProgressIndicator()
+ Text(
+ text = stringResource(R.string.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 = stringResource(R.string.success),
+ tint = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.size(128.dp)
+ )
+
+ Text(
+ text = stringResource(R.string.presets_import_success_message),
+ 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..49462bd
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/presets/PresetsScreen.kt
@@ -0,0 +1,247 @@
+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.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.tooling.preview.Preview
+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.design.utils.isLandscape
+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("*/*"),
+ 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)
+ ) {
+ if (isLandscape()) {
+ Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
+ ImportCards(
+ onImportPressed = onImportPressed,
+ onExportPressed = onExportPressed,
+ modifier = Modifier.weight(1f)
+ )
+ }
+ } else {
+ ImportCards(
+ onImportPressed = onImportPressed,
+ onExportPressed = onExportPressed,
+ modifier = Modifier.fillMaxWidth(1f)
+ )
+ }
+
+
+ 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(
+ onImportPressed: () -> Unit,
+ onExportPressed: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ ImportCard(
+ onClick = onImportPressed,
+ modifier = modifier,
+ title = stringResource(R.string.import_text),
+ description = stringResource(R.string.import_presets_description),
+ iconRes = R.drawable.ic_import
+ )
+ ImportCard(
+ modifier = modifier,
+ onClick = onExportPressed,
+ title = stringResource(R.string.export),
+ description = stringResource(R.string.export_presets_description),
+ iconRes = R.drawable.ic_export
+ )
+}
+
+@Composable
+private fun ImportCard(
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit, title: String,
+ description: String, iconRes: Int
+) {
+ Card(onClick = onClick, modifier = modifier) {
+ 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
+@Preview(device = "spec:parent=pixel_5,orientation=landscape")
+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..f184388
--- /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(stringProvider.getString(R.string.import_failure)) }
+ }
+ }
+ }
+
+ 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(stringProvider.getString(R.string.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..cea7c9d
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/search/SearchScreen.kt
@@ -0,0 +1,510 @@
+package com.androidvip.sysctlgui.ui.search
+
+import androidx.compose.animation.AnimatedContent
+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.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkHorizontally
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+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.grid.GridCells
+import androidx.compose.foundation.lazy.grid.GridItemSpan
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.lazy.grid.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.res.stringResource
+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.design.utils.isLandscape
+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(),
+ outerScaffoldPadding: PaddingValues,
+ onParamSelected: (UiKernelParam) -> 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 = false,
+ )
+ )
+ )
+ }
+
+ LaunchedEffect(Unit) {
+ viewModel.effect.collect { effect ->
+ when (effect) {
+ is SearchViewEffect.EditKernelParam -> onParamSelected(effect.param)
+ }
+ }
+ }
+
+ SearchScreenContent(
+ outerScaffoldPadding = outerScaffoldPadding,
+ searchQuery = searchQuery,
+ onSearchQueryChange = {
+ searchQuery = it
+ viewModel.onEvent(SearchViewEvent.SearchQueryChange(it))
+ },
+ searchActive = searchActive,
+ onSearchActiveChange = { searchActive = it },
+ state = state,
+ onHistoryItemRemoveClicked = {
+ viewModel.onEvent(SearchViewEvent.HistoryItemRemoveClicked(it))
+ },
+ onParamClicked = {
+ viewModel.onEvent(SearchViewEvent.ParamClicked(it))
+ },
+ onSearch = { query ->
+ searchActive = false
+ viewModel.onEvent(SearchViewEvent.SearchRequested(query))
+ }
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun SearchScreenContent(
+ state: SearchViewState,
+ outerScaffoldPadding: PaddingValues = PaddingValues(),
+ searchQuery: String,
+ onSearchQueryChange: (String) -> Unit,
+ searchActive: Boolean,
+ onSearchActiveChange: (Boolean) -> Unit,
+ onHistoryItemRemoveClicked: (SearchHint) -> Unit,
+ onParamClicked: (UiKernelParam) -> Unit,
+ onSearch: (String) -> Unit,
+) {
+ val focusManager = LocalFocusManager.current
+ val navHostTopPadding = outerScaffoldPadding.calculateTopPadding()
+ 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(
+ modifier = Modifier
+ .fillMaxSize()
+ .offset(y = -navHostTopPadding),
+ 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(stringResource(R.string.search_title)) },
+ leadingIcon = {
+ AnimatedContent(
+ targetState = searchActive,
+ label = "SearchIconsAnimation"
+ ) { targetSearchActive ->
+ if (targetSearchActive) {
+ IconButton(onClick = {
+ focusManager.clearFocus()
+ onSearchActiveChange(false)
+ onSearchQueryChange("")
+ }) {
+ Icon(
+ Icons.AutoMirrored.Rounded.ArrowBack,
+ contentDescription = stringResource(R.string.go_back)
+ )
+ }
+ } else {
+ Icon(
+ imageVector = Icons.Rounded.Search,
+ contentDescription = stringResource(
+ R.string.acessibility_search_icon
+ )
+ )
+ }
+ }
+ },
+ trailingIcon = {
+ AnimatedVisibility(
+ visible = searchQuery.isNotEmpty() && searchActive,
+ enter = expandHorizontally(expandFrom = Alignment.Start) + fadeIn(),
+ exit = shrinkHorizontally(shrinkTowards = Alignment.Start) + fadeOut()
+ ) {
+ IconButton(onClick = {
+ onSearchQueryChange("")
+ }) {
+ Icon(
+ imageVector = Icons.Rounded.Clear,
+ contentDescription = stringResource(R.string.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 = state.searchHints,
+ onHistoryItemRemoveClicked = onHistoryItemRemoveClicked,
+ onSearchQueryChange = onSearchQueryChange,
+ onSearch = onSearch,
+ onSearchActiveChange = onSearchActiveChange,
+ searchQuery = searchQuery
+ )
+ }
+ }
+ ) { innerPadding ->
+ AnimatedContent(
+ targetState = state.loading,
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding)
+ ) {
+ if (it) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
+ }
+ } else {
+ SearchResultsContent(
+ searchResults = state.searchResults,
+ searchQuery = searchQuery,
+ onParamClicked = onParamClicked
+ )
+ }
+ }
+ }
+}
+
+@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 }
+ val columnsCount = if (isLandscape()) 2 else 1
+ val hintItemColumns = GridCells.Fixed(columnsCount)
+
+ LazyVerticalGrid(
+ columns = hintItemColumns,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ if (historyHints.isNotEmpty()) {
+ item(
+ key = "history_header",
+ span = { GridItemSpan(maxLineSpan) }
+ ) {
+ Text(
+ text = stringResource(R.string.recent_searches),
+ style = MaterialTheme.typography.titleSmall,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.primary,
+ modifier = Modifier
+ .animateItem()
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 12.dp)
+ )
+ }
+ items(historyHints, key = { "history:${it.hint}" }) { historyItem ->
+ ListItem(
+ colors = ListItemDefaults.colors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
+ ),
+ headlineContent = { Text(historyItem.hint) },
+ leadingContent = {
+ Icon(
+ painter = painterResource(R.drawable.ic_history),
+ contentDescription = stringResource(R.string.history_item)
+ )
+ },
+ trailingContent = {
+ IconButton(
+ onClick = { onHistoryItemRemoveClicked(historyItem) },
+ modifier = Modifier.offset(16.dp)
+ ) {
+ Icon(
+ Icons.Rounded.Clear,
+ contentDescription = stringResource(R.string.clear_history_item)
+ )
+ }
+ },
+ modifier = Modifier
+ .clickable {
+ onSearchQueryChange(historyItem.hint)
+ onSearch(historyItem.hint)
+ onSearchActiveChange(false)
+ }
+ .animateItem()
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp, vertical = 4.dp)
+ )
+ }
+
+ if (suggestionHints.isNotEmpty()) {
+ item(
+ key = "history_suggestion_divider",
+ span = { GridItemSpan(maxLineSpan) }
+ ) {
+ HorizontalDivider(
+ modifier = Modifier
+ .padding(vertical = 8.dp)
+ .padding(horizontal = 16.dp)
+ )
+ }
+ }
+ }
+
+ if (suggestionHints.isNotEmpty()) {
+ item(
+ key = "suggestions_header",
+ span = { GridItemSpan(maxLineSpan) }
+ ) {
+ Text(
+ text = stringResource(R.string.suggestions),
+ style = MaterialTheme.typography.titleSmall,
+ color = MaterialTheme.colorScheme.primary,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp, vertical = 12.dp)
+ )
+ }
+ items(suggestionHints, key = { "hint:${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 = 8.dp, vertical = 4.dp) // Padding for grid spacing
+ )
+ }
+ }
+
+ if (searchHints.isEmpty() && searchQuery.isBlank()) {
+ item(
+ key = "empty_search_suggestions",
+ span = { GridItemSpan(maxLineSpan) }
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(stringResource(R.string.search_no_suggestions))
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun SearchResultsContent(
+ searchResults: List,
+ searchQuery: String,
+ onParamClicked: (UiKernelParam) -> Unit
+) {
+ val columns = if (isLandscape()) 2 else 1
+ if (searchResults.isNotEmpty()) {
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(columns),
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = PaddingValues(bottom = 8.dp)
+ ) {
+ itemsIndexed(searchResults) { index, param ->
+ ParamRow(
+ modifier = Modifier
+ .animateItem()
+ .fillMaxWidth()
+ .padding(4.dp),
+ param = param,
+ showFullName = true,
+ onParamClicked = onParamClicked
+ )
+
+ if (columns == 1 && index < searchResults.lastIndex) {
+ HorizontalDivider()
+ }
+ }
+ }
+ } else if (searchQuery.isNotEmpty()) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(24.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = stringResource(R.string.search_no_results_format, 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 = stringResource(R.string.search_message),
+ 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 },
+ state = SearchViewState(
+ searchHints = searchHints,
+ searchResults = searchResults
+ ),
+ onHistoryItemRemoveClicked = { searchHints = searchHints - it },
+ onParamClicked = {},
+ 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..9d6fccb
--- /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 = mutableListOf()
+
+ init {
+ viewModelScope.launch {
+ val fetchedHistory = appPrefs.searchHistory
+ val fetchedParams = fetchParams()
+
+ searchHistory.addAll(fetchedHistory)
+ searchableParams.addAll(fetchedParams)
+
+ val uiSearchHints = fetchedHistory
+ .map { SearchHint(hint = it, isFromHistory = true) }
+ .takeLast(MAX_SEARCH_HINTS)
+
+ 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) {
+ 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 searchResults = withContext(Dispatchers.IO) {
+ searchableParams
+ .filter { it.name.contains(query, ignoreCase = true) }
+ .map(UiKernelParamMapper::map)
+ }
+
+ setState {
+ copy(loading = false, searchResults = searchResults)
+ }
+ }
+ }
+
+ private fun handlePreSearch(query: String) {
+ viewModelScope.launch {
+ val hints = withContext(Dispatchers.IO) {
+ val historyHints = searchHistory
+ .toList()
+ .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 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..282a131
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/search/SearchViewState.kt
@@ -0,0 +1,21 @@
+package com.androidvip.sysctlgui.ui.search
+
+import com.androidvip.sysctlgui.models.SearchHint
+import com.androidvip.sysctlgui.models.UiKernelParam
+
+data class SearchViewState(
+ val loading: Boolean = false,
+ val searchHints: List = emptyList(),
+ val searchResults: List = emptyList()
+)
+
+sealed interface 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 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..2bf4142
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/SettingsScreen.kt
@@ -0,0 +1,334 @@
+package com.androidvip.sysctlgui.ui.settings
+
+import android.Manifest
+import android.content.pm.PackageManager
+import android.os.Build
+import android.widget.Toast
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+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.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.GridItemSpan
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Info
+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.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+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.R
+import com.androidvip.sysctlgui.core.navigation.UiRoute
+import com.androidvip.sysctlgui.data.Prefs
+import com.androidvip.sysctlgui.design.theme.SysctlGuiTheme
+import com.androidvip.sysctlgui.design.utils.isLandscape
+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.SettingsComponentColumn
+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 com.androidvip.sysctlgui.utils.browse
+import org.koin.androidx.compose.koinViewModel
+
+internal const val DISABLED_ALPHA = 0.38f
+
+@Composable
+internal fun SettingsScreen(
+ mainViewModel: MainViewModel = koinViewModel(),
+ viewModel: SettingsViewModel = koinViewModel(),
+ onNavigate: (UiRoute) -> 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,
+ showSearchAction = false
+ )
+ )
+ )
+ }
+
+ 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)
+ }
+ }
+ }
+
+ is SettingsViewEffect.OpenBrowser -> {
+ context.browse(effect.url)
+ }
+
+ is SettingsViewEffect.Navigate -> {
+ onNavigate(effect.route)
+ }
+
+ is SettingsViewEffect.ShowToast -> {
+ Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+ }
+
+ SettingsScreenContent(
+ settings = state.value.settings,
+ onSettingHeaderClicked = { appSetting ->
+ viewModel.onEvent(SettingsViewEvent.SettingHeaderClicked(appSetting))
+ },
+ onValueChanged = { appSetting, newValue ->
+ viewModel.onEvent(SettingsViewEvent.SettingValueChanged(appSetting, newValue))
+ }
+ )
+}
+
+@Composable
+private fun SettingsScreenContent(
+ settings: List> = emptyList(),
+ onSettingHeaderClicked: (AppSetting<*>) -> Unit,
+ onValueChanged: (AppSetting<*>, Any) -> Unit
+) {
+ val groupedSettings = settings.groupBy { it.category }
+ val columns = if (isLandscape()) 2 else 1
+
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(columns),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ groupedSettings.forEach { (category, categorySettings) ->
+ item(span = { GridItemSpan(columns) }) {
+ Text(
+ text = category,
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.primary,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ top = 8.dp,
+ bottom = 8.dp,
+ start = if (columns > 1) 16.dp else 56.dp,
+ end = 16.dp,
+ )
+ )
+ }
+
+ items(
+ items = categorySettings,
+ key = { setting -> setting.key }
+ ) { appSetting ->
+ val itemModifier = if (columns > 1) {
+ Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp, vertical = 4.dp)
+ } else {
+ Modifier.fillMaxWidth()
+ }
+
+ when (appSetting.type) {
+ SettingItemType.Text -> {
+ HeaderComponent(
+ modifier = itemModifier,
+ appSetting = appSetting,
+ onClick = { onSettingHeaderClicked(appSetting) }
+ )
+ }
+ SettingItemType.List -> {
+ TextSettingComponent(
+ modifier = itemModifier,
+ appSetting = appSetting,
+ onValueChange = { newValue ->
+ onValueChanged(appSetting, newValue)
+ }
+ )
+ }
+ SettingItemType.Switch -> {
+ SwitchSettingComponent(
+ modifier = itemModifier,
+ appSetting = appSetting,
+ onValueChange = { newValue ->
+ onValueChanged(appSetting, newValue)
+ }
+ )
+ }
+ SettingItemType.Slider -> {
+ SliderSettingComponent(
+ modifier = itemModifier,
+ appSetting = appSetting,
+ onValueChange = { newValue ->
+ onValueChanged(appSetting, newValue)
+ }
+ )
+ }
+ }
+ }
+ }
+
+ item(span = { GridItemSpan(columns) }) {
+ Box(contentAlignment = Alignment.Center) {
+ Row(
+ modifier = Modifier.padding(all = 16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(space = 16.dp)
+ ) {
+
+ Icon(
+ imageVector = Icons.Rounded.Info,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onBackground,
+ modifier = Modifier.size(24.dp)
+ )
+
+ SettingsComponentColumn(
+ title = stringResource(R.string.about),
+ description = stringResource(R.string.sysctl_help),
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ }
+ }
+
+ item(span = { GridItemSpan(columns) }) {
+ Text(
+ text = "Developed with ❤️ by Lennoard Silva\nAndroid enthusiast 🤖",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onBackground,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ top = 64.dp,
+ bottom = 32.dp,
+ start = 24.dp,
+ end = 24.dp,
+ )
+ )
+ }
+ }
+}
+
+@Composable
+@PreviewLightDark
+@Preview(device = "spec:parent=pixel_5,orientation=landscape")
+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,
+ ),
+ AppSetting(
+ key = Prefs.GuessInputType.key,
+ value = true,
+ category = "General",
+ title = "Guess input type",
+ description = "Description",
+ 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,
+ onSettingHeaderClicked = {},
+ 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..412eb38
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/SettingsViewModel.kt
@@ -0,0 +1,106 @@
+package com.androidvip.sysctlgui.ui.settings
+
+import android.content.SharedPreferences
+import androidx.core.content.edit
+import androidx.lifecycle.viewModelScope
+import com.androidvip.sysctlgui.R
+import com.androidvip.sysctlgui.core.navigation.UiRoute
+import com.androidvip.sysctlgui.data.Prefs
+import com.androidvip.sysctlgui.domain.StringProvider
+import com.androidvip.sysctlgui.domain.enums.SettingItemType
+import com.androidvip.sysctlgui.domain.models.KEY_CONTRIBUTORS
+import com.androidvip.sysctlgui.domain.models.KEY_DELETE_HISTORY
+import com.androidvip.sysctlgui.domain.models.KEY_MANAGE_PARAMS
+import com.androidvip.sysctlgui.domain.models.KEY_SOURCE_CODE
+import com.androidvip.sysctlgui.domain.models.KEY_TRANSLATIONS
+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,
+ private val stringProvider: StringProvider
+) : BaseViewModel() {
+
+ init {
+ viewModelScope.launch {
+ loadSettings()
+ }
+ }
+
+ 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 }
+ }
+
+ viewModelScope.launch {
+ loadSettings()
+ }
+ }
+
+ is SettingsViewEvent.SettingHeaderClicked<*> -> {
+ when (event.appSetting.key) {
+ KEY_MANAGE_PARAMS -> {
+ setEffect { SettingsViewEffect.Navigate(UiRoute.UserParams) }
+ }
+
+ KEY_DELETE_HISTORY -> {
+ sharedPreferences.edit { remove(Prefs.SearchHistory.key) }
+ setEffect {
+ SettingsViewEffect.ShowToast(stringProvider.getString(R.string.history_deleted))
+ }
+ }
+
+ KEY_SOURCE_CODE, KEY_CONTRIBUTORS, KEY_TRANSLATIONS -> {
+ setEffect {
+ SettingsViewEffect.OpenBrowser(event.appSetting.value as String)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private suspend fun loadSettings() {
+ val settings = getSettings()
+ setState { copy(settings = settings) }
+ }
+}
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..a19637d
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/components/HeaderComponent.kt
@@ -0,0 +1,57 @@
+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.Spacer
+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.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import com.androidvip.sysctlgui.domain.models.AppSetting
+
+@Composable
+fun HeaderComponent(
+ modifier: Modifier = Modifier,
+ appSetting: AppSetting<*>,
+ onClick: () -> Unit
+) {
+ Box(
+ modifier = modifier.clickable(onClick = onClick),
+ contentAlignment = Alignment.Center
+ ) {
+ Row(
+ modifier = Modifier.padding(all = 16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(space = 16.dp)
+ ) {
+
+ if (appSetting.iconResource != null) {
+ Box(modifier = Modifier.align(Alignment.CenterVertically)) {
+ Icon(
+ painter = painterResource(appSetting.iconResource!!),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurface,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+ } else {
+ Spacer(modifier = Modifier.size(24.dp))
+ }
+
+ 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..ecf396d
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/components/TextSettingComponent.kt
@@ -0,0 +1,120 @@
+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.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.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.Icon
+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.res.painterResource
+import androidx.compose.ui.tooling.preview.PreviewLightDark
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.dp
+import com.androidvip.sysctlgui.R
+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
+) {
+ 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)
+ ) {
+ if (appSetting.iconResource != null) {
+ Box(modifier = Modifier.align(Alignment.CenterVertically)) {
+ Icon(
+ painter = painterResource(appSetting.iconResource!!),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSurface,
+ modifier = Modifier.size(24.dp)
+ )
+ }
+ } else {
+ Spacer(modifier = Modifier.size(24.dp))
+ }
+
+ SettingsComponentColumn(
+ title = appSetting.title,
+ description = appSetting.description,
+ enabled = appSetting.enabled,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+
+ DropdownMenu(
+ expanded = expanded,
+ offset = DpOffset(16.dp, (-32).dp),
+ 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 = "",
+ iconResource = R.drawable.ic_history,
+ 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..75e2859
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/settings/model/SettingsViewEvent.kt
@@ -0,0 +1,20 @@
+package com.androidvip.sysctlgui.ui.settings.model
+
+import com.androidvip.sysctlgui.core.navigation.UiRoute
+import com.androidvip.sysctlgui.domain.models.AppSetting
+
+sealed interface SettingsViewEvent {
+ class SettingValueChanged(val appSetting: AppSetting, val newValue: Any) : SettingsViewEvent
+ class SettingHeaderClicked(val appSetting: AppSetting) : SettingsViewEvent
+}
+
+sealed interface SettingsViewEffect {
+ object RequestNotificationPermission : SettingsViewEffect
+ class OpenBrowser(val url: String) : SettingsViewEffect
+ class Navigate(val route: UiRoute) : SettingsViewEffect
+ class ShowToast(val message: String) : 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..d173902 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,50 +4,43 @@ 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.core.view.isVisible
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.goAway
-import com.androidvip.sysctlgui.helpers.Actions
+import com.androidvip.sysctlgui.domain.enums.Actions
+import com.androidvip.sysctlgui.domain.repository.AppPrefs
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) {
prefs.useBusybox = false
@@ -55,7 +48,7 @@ class StartActivity : BaseAppCompatActivity() {
navigate()
finish()
} else {
- binding.splashProgress.goAway()
+ binding.splashProgress.isVisible = false
toast(R.string.root_not_found_sum, Toast.LENGTH_LONG)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
startActivity(
@@ -67,58 +60,22 @@ 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() {
- val shortcutNames = arrayOf(
- Actions.BrowseParams.name,
- Actions.ExportParams.name,
- Actions.OpenSettings.name
- )
- val nextIntent = when (intent.action) {
- in shortcutNames -> {
- Intent(this, MainActivity::class.java).apply {
- putExtra(MainActivity.EXTRA_DESTINATION, intent.action)
- }
- }
-
- Actions.EditParam.name -> {
- Intent(this, EditKernelParamActivity::class.java).apply {
- putExtra(
- EditKernelParamActivity.EXTRA_PARAM,
- intent.extras!!.getParcelable(
- EditKernelParamActivity.EXTRA_PARAM
- )
- )
- putExtra(
- EditKernelParamActivity.EXTRA_EDIT_SAVED_PARAM,
- intent.getBooleanExtra(
- EditKernelParamActivity.EXTRA_EDIT_SAVED_PARAM,
- false
- )
- )
- }
- }
-
- else -> {
- Intent(this, MainActivity::class.java)
+ val shortcutNames = Actions.entries.map { it.name }
+ val nextIntent = Intent(this, MainActivity::class.java).apply {
+ if (intent.action in shortcutNames) {
+ putExtra(MainActivity.EXTRA_DESTINATION, intent.action)
+ putExtras(intent.extras ?: Bundle())
}
}
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..0909855 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,16 +4,19 @@ import android.content.Intent
import android.graphics.drawable.AnimatedVectorDrawable
import android.os.Bundle
import android.os.Handler
+import androidx.activity.enableEdgeToEdge
+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?) {
super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+
binding = ActivityStartErrorBinding.inflate(layoutInflater)
setContentView(binding.root)
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..a1eb806 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,25 +1,41 @@
package com.androidvip.sysctlgui.ui.tasker
-import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
+import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
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?) {
super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
binding = ActivityTaskerPluginBinding.inflate(layoutInflater)
setContentView(binding.root)
+ ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, windowInsets ->
+ val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+
+ view.updatePadding(
+ left = insets.left,
+ top = insets.top,
+ right = insets.right,
+ bottom = insets.bottom
+ )
+
+ WindowInsetsCompat.CONSUMED
+ }
+
binding.taskerDoneButton.setOnClickListener {
val selectedListNumber = binding.taskerListSpinner.selectedItemPosition // 0-based index
@@ -32,7 +48,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..bd5b529
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/user/UserParamsScreen.kt
@@ -0,0 +1,273 @@
+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.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.itemsIndexed
+import androidx.compose.foundation.lazy.grid.rememberLazyGridState
+import androidx.compose.material3.CircularProgressIndicator
+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.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.PreviewScreenSizes
+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.design.utils.isLandscape
+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()
+ val appBarTitle = if (state.userParams.all { it.isFavorite }) {
+ stringResource(R.string.app_name)
+ } else {
+ stringResource(R.string.startup_params)
+ }
+
+ LaunchedEffect(Unit) {
+ viewModel.onEvent(UserParamsViewEvent.ScreenLoaded(filterPredicate))
+
+ mainViewModel.onEvent(
+ MainViewEvent.OnSateChangeRequested(
+ MainViewState(
+ topBarTitle = appBarTitle,
+ 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 isLandscape = isLandscape()
+ val gridState = rememberLazyGridState()
+ val columns = if (isLandscape) 2 else 1
+
+ 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)
+ )
+ Text(
+ text = stringResource(R.string.empty_favorites_widget),
+ style = MaterialTheme.typography.titleLarge,
+ color = MaterialTheme.colorScheme.onBackground,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ return
+ }
+
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(columns),
+ state = gridState,
+ modifier = Modifier.fillMaxSize()
+ ) {
+ itemsIndexed(
+ items = favoriteParams,
+ key = { _, param -> param.name }
+ ) { index, param ->
+ 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)
+ .fillMaxWidth(),
+ param = param,
+ showFavoriteIcon = true,
+ onParamClicked = onParamClicked
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+@PreviewScreenSizes
+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..90ed643
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/ui/user/UserParamsViewModel.kt
@@ -0,0 +1,70 @@
+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 {
+ setState { copy(loading = true) }
+ val params = getUserParams()
+ .map(UiKernelParamMapper::map)
+ .filter(predicate)
+ setState { copy(userParams = params, loading = false) }
+ }
+ }
+
+ 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
deleted file mode 100644
index 82a3ac6..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/FavoriteWidgetParamUpdater.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.androidvip.sysctlgui.widgets
-
-import android.app.PendingIntent
-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) :
- ParamsRepositoryImpl.ChangeListener {
- override fun onChange() {
- val intentUpdate = Intent(context, FavoritesWidget::class.java)
- intentUpdate.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
- val idArray = AppWidgetManager.getInstance(context).getAppWidgetIds(
- ComponentName(context, FavoritesWidget::class.java)
- )
-
- 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
- }
-
- idArray.forEach {
- intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(it))
- PendingIntent.getBroadcast(context, it, intentUpdate, flags).send()
- }
- }
-
- fun getListener(): ParamsRepositoryImpl.ChangeListener = this
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/FavoritesGlanceWidget.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/FavoritesGlanceWidget.kt
new file mode 100644
index 0000000..f4ea51e
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/FavoritesGlanceWidget.kt
@@ -0,0 +1,110 @@
+package com.androidvip.sysctlgui.widgets
+
+import android.content.Context
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.glance.GlanceId
+import androidx.glance.GlanceModifier
+import androidx.glance.action.actionParametersOf
+import androidx.glance.action.clickable
+import androidx.glance.appwidget.GlanceAppWidget
+import androidx.glance.appwidget.action.actionRunCallback
+import androidx.glance.appwidget.lazy.LazyColumn
+import androidx.glance.appwidget.lazy.items
+import androidx.glance.appwidget.provideContent
+import androidx.glance.background
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.Column
+import androidx.glance.layout.Spacer
+import androidx.glance.layout.fillMaxSize
+import androidx.glance.layout.fillMaxWidth
+import androidx.glance.layout.height
+import androidx.glance.layout.padding
+import androidx.glance.text.FontWeight
+import androidx.glance.text.Text
+import androidx.glance.text.TextStyle
+import androidx.glance.unit.ColorProvider
+import com.androidvip.sysctlgui.R
+import com.androidvip.sysctlgui.design.theme.onPrimaryContainerLight
+import com.androidvip.sysctlgui.design.theme.primaryContainerLight
+import com.androidvip.sysctlgui.design.theme.primaryLight
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import com.androidvip.sysctlgui.domain.usecase.GetUserParamsUseCase
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+
+class FavoritesGlanceWidget : GlanceAppWidget(), KoinComponent {
+ private val getUserParamsUseCase: GetUserParamsUseCase by inject()
+
+ override suspend fun provideGlance(context: Context, id: GlanceId) {
+ val favoriteParams = getUserParamsUseCase().filter { it.isFavorite }
+
+ provideContent {
+ FavoritesWidgetContent(params = favoriteParams)
+ }
+ }
+
+ @Composable
+ fun FavoritesWidgetContent(params: List) {
+ Column(
+ modifier = GlanceModifier
+ .fillMaxSize()
+ .background(primaryContainerLight)
+ .padding(16.dp),
+ horizontalAlignment = Alignment.Horizontal.Start
+ ) {
+ Text(
+ text = stringResource(R.string.favorite_widget_title),
+ style = TextStyle(
+ fontWeight = FontWeight.Bold,
+ fontSize = 16.sp,
+ color = ColorProvider(onPrimaryContainerLight)
+ ),
+ modifier = GlanceModifier.padding(vertical = 8.dp)
+ )
+ Spacer(GlanceModifier.height(8.dp))
+
+ if (params.isEmpty()) {
+ Text(stringResource(R.string.empty_favorites_widget))
+ } else {
+ LazyColumn {
+ items(params) { param ->
+ FavoriteItem(param = param)
+ Spacer(GlanceModifier.height(4.dp))
+ }
+ }
+ }
+ }
+ }
+
+ @Composable
+ fun FavoriteItem(param: KernelParam) {
+ Column(
+ modifier = GlanceModifier
+ .padding(vertical = 8.dp)
+ .fillMaxWidth()
+ .clickable(
+ onClick = actionRunCallback(
+ parameters = actionParametersOf(kernelParamNameKey to param.name)
+ )
+ )
+ ) {
+ Text(
+ text = param.name,
+ style = TextStyle(
+ fontWeight = FontWeight.Medium,
+ color = ColorProvider(primaryLight)
+ )
+ )
+ Text(
+ text = param.value,
+ style = TextStyle(
+ fontWeight = FontWeight.Medium,
+ color = ColorProvider(primaryLight)
+ )
+ )
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/FavoritesGlanceWidgetReceiver.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/FavoritesGlanceWidgetReceiver.kt
new file mode 100644
index 0000000..e9eb354
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/FavoritesGlanceWidgetReceiver.kt
@@ -0,0 +1,8 @@
+package com.androidvip.sysctlgui.widgets
+
+import androidx.glance.appwidget.GlanceAppWidget
+import androidx.glance.appwidget.GlanceAppWidgetReceiver
+
+class FavoritesGlanceWidgetReceiver : GlanceAppWidgetReceiver() {
+ override val glanceAppWidget: GlanceAppWidget = FavoritesGlanceWidget()
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/FavoritesWidget.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/FavoritesWidget.kt
deleted file mode 100644
index a02911d..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/FavoritesWidget.kt
+++ /dev/null
@@ -1,113 +0,0 @@
-package com.androidvip.sysctlgui.widgets
-
-import android.app.PendingIntent
-import android.appwidget.AppWidgetManager
-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 com.androidvip.sysctlgui.R
-import com.androidvip.sysctlgui.data.mapper.DomainParamMapper
-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.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
-
-class FavoritesWidget : AppWidgetProvider(), KoinComponent {
- private val getUserParamsUseCase: GetUserParamsUseCase by inject()
-
- override fun onUpdate(
- context: Context,
- appWidgetManager: AppWidgetManager,
- appWidgetIds: IntArray
- ) {
- for (appWidgetId in appWidgetIds) {
- updateAppWidget(context, appWidgetManager, appWidgetId)
- }
- super.onUpdate(context, appWidgetManager, appWidgetIds)
- }
-
- override fun onReceive(context: Context?, intent: Intent?) {
- if (context == null || intent == null) return
- if (intent.action != EDIT_PARAM_EXTRA) {
- return super.onReceive(context, intent)
- }
-
- runBlocking {
- val params = getUserParamsUseCase().filter {
- it.favorite
- }.map {
- DomainParamMapper.map(it)
- }.toMutableList()
-
- if (params.isEmpty()) return@runBlocking
-
- val param = params[intent.getIntExtra(EXTRA_ITEM, 0)]
- 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)
- context.startActivity(this)
- }
- }
-
- super.onReceive(context, intent)
- }
-
- override fun onDisabled(context: Context?) {
- super.onDisabled(context)
- }
-
- companion object {
- const val EDIT_PARAM_EXTRA = "com.androidvip.sysctlgui.EDIT_PARAM_EXTRA"
- const val EXTRA_ITEM = "com.androidvip.sysctlgui.EXTRA_ITEM"
- }
-}
-
-internal fun updateAppWidget(
- context: Context,
- appWidgetManager: AppWidgetManager,
- appWidgetId: Int
-) {
- val intent = Intent(context, FavoritesWidgetService::class.java).apply {
- putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
- data = Uri.parse(this.toUri(Intent.URI_INTENT_SCHEME))
- }
-
- val views = RemoteViews(
- context.packageName,
- R.layout.favorites_widget
- ).apply {
- setRemoteAdapter(R.id.favorites_list, intent)
- setEmptyView(R.id.favorites_list, R.id.empty_view)
- }
-
- val editParamPendingIntent: PendingIntent = Intent(
- context,
- FavoritesWidget::class.java
- ).run {
- action = EDIT_PARAM_EXTRA
- putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
- data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
-
- val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
- } else {
- PendingIntent.FLAG_UPDATE_CURRENT
- }
-
- PendingIntent.getBroadcast(context, 0, this, flags)
- }
- views.setPendingIntentTemplate(R.id.favorites_list, editParamPendingIntent)
-
- appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.favorites_list)
- appWidgetManager.updateAppWidget(appWidgetId, views)
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/FavoritesWidgetService.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/FavoritesWidgetService.kt
deleted file mode 100644
index 9b7b82d..0000000
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/FavoritesWidgetService.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-package com.androidvip.sysctlgui.widgets
-
-import android.appwidget.AppWidgetManager
-import android.content.Context
-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.usecase.GetUserParamsUseCase
-import com.androidvip.sysctlgui.widgets.FavoritesWidget.Companion.EXTRA_ITEM
-import kotlinx.coroutines.runBlocking
-import org.koin.core.component.KoinComponent
-import org.koin.core.component.inject
-
-class FavoritesWidgetService : RemoteViewsService() {
- override fun onGetViewFactory(intent: Intent?): RemoteViewsFactory {
- return FavoritesRemoteViewsFactory(applicationContext, intent!!)
- }
-}
-
-class FavoritesRemoteViewsFactory(
- val context: Context,
- val intent: Intent
-) : RemoteViewsService.RemoteViewsFactory, KoinComponent {
- private val getUserParamsUseCase: GetUserParamsUseCase by inject()
-
- private var widgetId: Any = intent.getIntExtra(
- AppWidgetManager.EXTRA_APPWIDGET_ID,
- AppWidgetManager.INVALID_APPWIDGET_ID
- )
-
- private var params: MutableList = mutableListOf()
-
- override fun onCreate() {
- runBlocking {
- params = getUserParamsUseCase().filter {
- it.favorite
- }.map {
- DomainParamMapper.map(it)
- }.toMutableList()
- }
- }
-
- override fun getLoadingView(): RemoteViews? = null
-
- override fun getItemId(position: Int): Long {
- return position.toLong()
- }
-
- override fun onDataSetChanged() {
- runBlocking {
- params = getUserParamsUseCase().filter {
- it.favorite
- }.map {
- DomainParamMapper.map(it)
- }.toMutableList()
- }
- }
-
- override fun hasStableIds(): Boolean = true
-
- override fun getViewAt(position: Int): RemoteViews {
- val views = RemoteViews(
- context.packageName,
- R.layout.list_item_kernel_param_widget_list
- )
-
- val param = params[position]
- views.setTextViewText(R.id.listKernelParamName, param.name)
- views.setTextViewText(R.id.listKernelParamValue, param.value)
-
- val fillInIntent = Intent().apply {
- putExtra(EXTRA_ITEM, position)
- }
-
- views.setOnClickFillInIntent(R.id.listKernelParamLayout, fillInIntent)
- return views
- }
-
- override fun getCount(): Int = params.size
-
- override fun getViewTypeCount(): Int = 1
-
- override fun onDestroy() {
- params.clear()
- }
-}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/UpdateFavoriteWidgetUseCase.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/UpdateFavoriteWidgetUseCase.kt
new file mode 100644
index 0000000..b4ed4ae
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/UpdateFavoriteWidgetUseCase.kt
@@ -0,0 +1,14 @@
+package com.androidvip.sysctlgui.widgets
+
+import android.content.Context
+import androidx.glance.appwidget.GlanceAppWidgetManager
+
+class UpdateFavoriteWidgetUseCase(private val context: Context) {
+ suspend operator fun invoke() {
+ val manager = GlanceAppWidgetManager(context)
+ val glanceIds = manager.getGlanceIds(FavoritesGlanceWidget::class.java)
+ glanceIds.forEach { glanceId ->
+ FavoritesGlanceWidget().update(context, glanceId)
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/ViewKernelParamDetailsAction.kt b/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/ViewKernelParamDetailsAction.kt
new file mode 100644
index 0000000..009e0d3
--- /dev/null
+++ b/app/src/main/kotlin/com/androidvip/sysctlgui/widgets/ViewKernelParamDetailsAction.kt
@@ -0,0 +1,29 @@
+package com.androidvip.sysctlgui.widgets
+
+import android.content.Context
+import android.content.Intent
+import androidx.glance.GlanceId
+import androidx.glance.action.ActionParameters
+import androidx.glance.appwidget.action.ActionCallback
+import com.androidvip.sysctlgui.domain.enums.Actions
+import com.androidvip.sysctlgui.ui.main.MainActivity
+import com.androidvip.sysctlgui.ui.start.StartActivity
+
+internal val kernelParamNameKey = ActionParameters.Key("kernelParamNameKey")
+
+class ViewKernelParamDetailsAction : ActionCallback {
+ override suspend fun onAction(
+ context: Context,
+ glanceId: GlanceId,
+ parameters: ActionParameters
+ ) {
+ val paramName = parameters[kernelParamNameKey] ?: return
+
+ val intent = Intent(context, StartActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ action = Actions.EditParam.name
+ putExtra(MainActivity.EXTRA_PARAM_NAME, paramName)
+ }
+ context.startActivity(intent)
+ }
+}
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_backup_params.xml b/app/src/main/res/drawable/ic_backup_params.xml
deleted file mode 100644
index b6e306a..0000000
--- a/app/src/main/res/drawable/ic_backup_params.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
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_export_params.xml b/app/src/main/res/drawable/ic_export_params.xml
deleted file mode 100644
index 4556bb7..0000000
--- a/app/src/main/res/drawable/ic_export_params.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
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_favorite_selected.xml b/app/src/main/res/drawable/ic_favorite_selected.xml
deleted file mode 100644
index 7c78dca..0000000
--- a/app/src/main/res/drawable/ic_favorite_selected.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_favorite_unselected.xml b/app/src/main/res/drawable/ic_favorite_unselected.xml
deleted file mode 100644
index beb8d66..0000000
--- a/app/src/main/res/drawable/ic_favorite_unselected.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
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_import_outline.xml b/app/src/main/res/drawable/ic_file_import_outline.xml
deleted file mode 100644
index a03b753..0000000
--- a/app/src/main/res/drawable/ic_file_import_outline.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
\ No newline at end of file
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_import_params.xml b/app/src/main/res/drawable/ic_import_params.xml
deleted file mode 100644
index fbc9092..0000000
--- a/app/src/main/res/drawable/ic_import_params.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
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..047116c 100644
--- a/app/src/main/res/drawable/ic_launcher_background.xml
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -6,5 +6,12 @@
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_restore_params.xml b/app/src/main/res/drawable/ic_restore_params.xml
deleted file mode 100644
index 20c7b56..0000000
--- a/app/src/main/res/drawable/ic_restore_params.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
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..99a6e14 100644
--- a/app/src/main/res/layout/activity_tasker_plugin.xml
+++ b/app/src/main/res/layout/activity_tasker_plugin.xml
@@ -12,9 +12,12 @@
android:layout_gravity="bottom|end"
android:layout_marginEnd="@dimen/d16"
android:layout_marginBottom="@dimen/d16"
+ android:fontFamily="@font/sansation_bold"
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"
+ app:fontFamily="@font/sansation_bold" />
+ android:background="@color/neutral_800">
diff --git a/app/src/main/res/layout/fragment_export_options.xml b/app/src/main/res/layout/fragment_export_options.xml
deleted file mode 100644
index 87886c6..0000000
--- a/app/src/main/res/layout/fragment_export_options.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/list_item_kernel_param_widget_list.xml b/app/src/main/res/layout/list_item_kernel_param_widget_list.xml
deleted file mode 100644
index b02125f..0000000
--- a/app/src/main/res/layout/list_item_kernel_param_widget_list.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
-
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/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index db4457e..a267d70 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,6 +1,6 @@
-
-
+
+
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index db507df..a267d70 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..92ec626
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..027253f
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..92ec626
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..681db35
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..35fea43
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..681db35
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..cd8dad4
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..3594d2f
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..cd8dad4
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..d91d030
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..1bcf674
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..d91d030
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aae64e2
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
new file mode 100644
index 0000000..0a1c7b7
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..aae64e2
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
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-de/arrays.xml b/app/src/main/res/values-de/arrays.xml
deleted file mode 100644
index 2749019..0000000
--- a/app/src/main/res/values-de/arrays.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
- - sysctl -w verwenden
- - echo \'value\' > /proc/sys/… verwenden
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index fb984c8..f28c3e7 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -2,74 +2,28 @@
Überprüfe Rootstatus
Überprüfe busybox
- Prüfe auf ein altes Datenbankschema
- Führe Datenbankmigration aus
Fehler
Einstellungen
Über
- Variabeln anzeigen
- Kernel-Parameter in einer Liste anzeigen und verändern
- Sysctl Operationen
- Werte werden aus der Datei gelesen
- Verwenden sie diese Option um aus einer .conf oder .json Datei die Parameter zu verwenden
- Favoriten anzeigen
- Erreichen sie ihre favorisierten Kernel-Parameter mit dieser Liste
Beenden
Kernel Parameter
Variabeln durchstöbern
In /proc/sys nach einem bestimmten Kernel-Parameter stöbern.
Parameter verändern
- Oh nein! Ein Fehler ist aufgetretten.
- Überprüfen sie ihre Eingaben
- Fehlgeschlagen
- Ungültiger Pfad
- Sysctl
- Mit Sysctl können Kernel-Parameter zur Laufzeit geändert werden. Die verfügbaren Parameter verden aus /proc/sys bestimmt.
- Mit dieser App soll eine graphische Möglichkeit geschaffen werden diese Parameter zu ändern. Der Quellcode ist einsehbar auf GitHub. Support finden sie im XDA Thread.
- Ordner zuerst anzeigen
- Beim Durchstöbern der Kernel-Parameter werden Ordner zuerst angezeigt
- Anwendung
- Operationen
- Leerstehende Werte zulassen
- Übernahmemodus
- Eingabetyp erraten
Fertig
- Zur Gewährleistung von Kompatibilität wird Busybox verwendet
- Busybox verwenden
- Zurzeit wird sysctl verwendet
- Anhand des Parameterwertes wird versucht den Eingabetyp der Tastatur zu setzten
Rootzugriff verweigert. Werte können nur mit Rootzugriff geändert werden.
- Information
Keine Informationen für den Parameter verfügbar
Suche
- Systemstart
- Beim Systemstart die Parameter übernehmen
wende Parameter an
Ihre Parameter werden angewendet
Wende in %d Sekunden an…
Startbenachrichtigung
Ihre Parameter werden angewendet
- Parameter verwalten
- Verwalte Parameter die beim Systemstart angewandt werden
- Exportiere Parameter
- Exportiert die beim Start angewandeten Parameter
- Keine Parameter gefunden
- Parameter wiederherstellen
- Stellt die Parameter von einem vorherigen Backup wieder her
Importieren von Parametern ist fehlgeschlagen
- Ungültige oder falsch formatierte JSON-Datei
- Datei konnte nicht geöffnet werden: ungültiger Dateityp.
Import ist nicht möglich: die Datei ist leer.
Import ist nicht möglich: die Datei enthält einen Formatierungsfehler.
- %d Parameter angewendet
- Dokumentation öffnen
Bearbeiten
Löschen
- Startup Verzögerung
- Die Ausführung wird auf %d Sekunden verzögert
- Ausführung findet sofort statt
- Als Favorit auswählen
- Aus Favoriten entfernen
keine Favoriten ausgewählt
Tasker Liste
Primäre Liste
@@ -80,43 +34,23 @@
Beim Systemstart angewendete Liste
SysctlGUI: Tasker Profil #%d angewendet
Wähle Tasker Liste aus
- Lösche von Tasker Liste #%d
- #%d wurde zur Tasker Liste hinzugefügt
Wähle eine Liste aus die mit Tasker verwendet werden soll
Wenn Tasker dieses Plugin ausführt, werden alle Parameter dieser Liste mit ihren Werten angewendet.
Tasker Einstellungen
- Zu Tasker hinzufügen
- Aus Tasker entfernen
- Es ist ein Fehler beim Anwenden der Parameter: %s aufgetreten
Rückgängig
Sysctl GUI starten
Sysctl GUI beim Systemstart ausführen
Kein Rootzugriff
Rootzugriff wird benötigt
- Parameter importieren
- Parameter Backup
- Erstellt ein Backup von allen Laufzeit Parametern. Nicht alle können bei der Wiederhestellung angewendet werden.
- Parameter konnten nicht exportiert werden
Aufgrund eines IO Fehlers ist der Export der Parameter fehlgeschlagen
Exportieren der Parmameter ist fehlgeschlagen: keine Parameter gefunden
Export Optionen
- Parameter wurden erfolgreich exportiert
Kernel Parameter importieren, exportieren oder ein Backup erstellen
- Bitte warten, dies kann eine Weile dauern…
Browse
Export
- Param list
Try again
Anwenden
Wiederhestellen
Parameter
Wert
- Zuletzt angewendeter Wert
- Aktueller Wert
- Der ausgewählte Wert konnte nicht angewendet werden
- Der ausgewählte Wert konnte nicht angewendet werden. Versuche \"echo\" Modus.
- Dynamische Farben
- Dynamische Farben (Monet Theme) verwenden wenn möglich
- Dark Theme erzwingen
- Dark Theme erzwingen wenn möglich
diff --git a/app/src/main/res/values-land/bools.xml b/app/src/main/res/values-land/bools.xml
deleted file mode 100644
index 65b37a3..0000000
--- a/app/src/main/res/values-land/bools.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- true
-
\ No newline at end of file
diff --git a/app/src/main/res/values-land/dimens.xml b/app/src/main/res/values-land/dimens.xml
deleted file mode 100644
index c946c4a..0000000
--- a/app/src/main/res/values-land/dimens.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
- 120dp
- @dimen/d32
-
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/arrays.xml b/app/src/main/res/values-pt-rBR/arrays.xml
deleted file mode 100644
index 2721cdd..0000000
--- a/app/src/main/res/values-pt-rBR/arrays.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
- - Usar sysctl -w
- - Usar echo \'valor\' > /proc/sys/…
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index e3152f0..bfdbbcf 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -1,77 +1,33 @@
Sobre
- Permitir valores em branco
- Aplicativo
Procurar variáveis
Procure em /proc/sys um parâmetro específico do kernel
- Modo de aplicação
Pronto
Editar parâmetros
- Por favor, verifique os campos de entrada
Erro
Sair
- Falhou
- Adivinhar tipo de entrada
- Caminho inválido
Parâmetros do kernel
- Listar pastas primeiro
- Listar pastas primeiro ao usar a opção de navegador de parâmetros do kernel
- Operações
- Ler valores do arquivo
- Use esta opção para carregar parâmetros de um arquivo .conf ou .json selecionado
Configurações
- Mostrar variáveis
- Exibir e editar todos os parâmetros do kernel em uma lista
Verificando o busybox
Verificando acesso root
- Sysctl
- Sysctl é usado para modificar os parâmetros do kernel no tempo de execução. Os parâmetros disponíveis são aqueles listados em /proc/sys.
- Operações Sysctl
- Algo de errado não está certo
- Usar busybox
- Busybox será usado para garantir a compatibilidade
- O objetivo principal deste aplicativo é fornecer uma maneira gráfica de editar esses parâmetros. Você pode encontrar o código fonte no GitHub. Suporte no XDA Thread.
- Atualmente confiando no binário sysctl do sistema
- Tentar definir o melhor tipo de entrada para o teclado com base no valor do parâmetro
Acesso root não encontrado. Você só pode editar parâmetros com acesso root.
- Informação
- Nenhuma informação disponível para este parâmetro
+ Nenhuma documentação disponível para este parâmetro
Pesquisar
Aplicando seus parâmetros
Notificação de inicialização
Aplicando seus parâmetros
Aplicando parâmetros
- Inicialização
- Permitir a reaplicação de parâmetros na inicialização
- Gerenciar parâmetros
- Gerenciar os parâmetros que serão aplicados na inicialização
- Nenhum parâmetro encontrado
- Exportar parâmetros
- Exportar os parâmetros que você alterou usando este aplicativo para um arquivo JSON
- Não foi possível abrir o arquivo: tipo de arquivo inválido.
Não foi possível importar os parâmetros: existem erros de formatação no arquivo.
Não foi possível importar os parâmetros: o arquivo está vazio.
- Restaurar parâmetros
- Arquivo JSON inválido ou mal formado
Falha ao importar parâmetros
- %d parâmetro(s) aplicados
- Abrir documentação
Remover
Editar
- Atraso de inicialização
- A execução será atrasada em %d segundos
- A execução não será atrasada
Aplicando em %d segundos…
- Mostrar favoritos
- Acesse os seus parâmetros favoritos a partir desta lista
- Definir como favorito
- nenhum favorito selecionado
+ Nenhum favorito adicionado
Lista do Tasker
SysctlGUI: Perfil Tasker #%d aplicado
Selecione a lista do Tasker
- Removido da lista do Tasker #%d
- Adicionado à lista do Tasker #%d
Selecione uma lista para ser usada com o Tasker
Uma vez acionado pelo Tasker, o plugin reaplicará imediatamente todos os parâmetros da lista selecionada com seus respectivos valores.
Configurações do Tasker
@@ -81,42 +37,72 @@
Lista secundária do Tasker
Lista de favoritos
Lista de aplicar na inicialização
- Adicionar ao Tasker
- Falha ao aplicar: %s
Desfazer
- Migrando banco de dados
- Verificando esquema de banco de dados antigo
Acesso root necessário
Requer acesso root
Executar o SysctlGUI na inicialização
Iniciar o SysctlGUI
- Fazer backup dos parâmetros
- Fazer backup de todos os parêmetros em runtime atuais. Por favor, note que nem todos os parâmetros podem ser reaplicados novamente, uma vez restaurados.
- Importar parâmetros
- 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
- Por favor aguarde, isto pode demorar algum tempo…
Nevegar
Exportar
- Lista de parâmetros
Tentar novamente
Aplicar
Restaurar
Parâmetro
Valor
- Valor aplicado
- Valor atual
- Remover do Tasker
- Remover dos favoritos
- O valor selecionado não pôde ser aplicado
- O valor selecionado não pôde ser aplicado. Tente usar o modo \"echo\".
- Cores dinâmicas
- Usar cores dinâmicas (Tema Monet) quando disponível
- Forçar tema escuro
- Forçar o uso do tema escuro quando disponível
+ Favoritos
+ Predefinições
+ A criação do arquivo foi cancelada ou falhou
+ A seleção de arquivo foi cancelada ou falhou
+ Ocorreu um erro ao abrir o arquivo
+ Ocorreu um erro ao processar o arquivo
+ Parâmetro excluído: %s
+ Voltar
+ Ler a documentação para \"%1$s\"
+ Diretório: %1$s
+ Parâmetro: %1$s
+ Ícone de parâmetro
+ Navegar para o diretório
+ Marcado como favorito
+ Alternar %1$s
+ Favoritar
+ Alternar parâmetro do Tasker
+ Valor aplicado com sucesso
+ Copiado para a área de transferência
+ Pressione e segure para copiar
+ Lista do Tasker: %1$s
+ Novo valor
+ Ler mais
+ Documentação
+ Valor do parâmetro
+ Aplicando predefinição
+ %1$d parâmetros encontrados
+ Importar
+ Carregando predefinição…
+ Sucesso
+ Predefinições importadas com sucesso
+ Importar predefinições de um arquivo
+ Exportar predefinições para um arquivo
+ Pesquisar parâmetros do kernel
+ Nenhuma sugestão disponível
+ Nenhum resultado encontrado para \"%1$s\"
+ Insira uma consulta para pesquisar parâmetros do kernel
+ Ícone de pesquisa
+ Limpar pesquisa
+ Pesquisas recentes
+ Item do histórico
+ Limpar item do histórico
+ Exportação concluída
+ Falha ao importar parâmetros
+ Valores em branco não são permitidos atualmente
+ O valor se recusou a aplicar com o modo de aplicação atual
+ Falha na execução do comando
+ Sugestões
+ Parâmetros favoritos
+ Histórico deletado
+ Parâmetros da inicialização
+ Sysctl é usado para modificar parâmetros do kernel em tempo de execução. Os parâmetros disponíveis são aqueles listados em /proc/sys e o objetivo principal deste aplicativo é fornecer uma maneira gráfica de editá-los.
\ No newline at end of file
diff --git a/app/src/main/res/values-sw600dp-land/bools.xml b/app/src/main/res/values-sw600dp-land/bools.xml
deleted file mode 100644
index 65b37a3..0000000
--- a/app/src/main/res/values-sw600dp-land/bools.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- true
-
\ No newline at end of file
diff --git a/app/src/main/res/values-sw600dp-land/dimens.xml b/app/src/main/res/values-sw600dp-land/dimens.xml
deleted file mode 100644
index 9c117c3..0000000
--- a/app/src/main/res/values-sw600dp-land/dimens.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- 72dp
-
\ No newline at end of file
diff --git a/app/src/main/res/values-sw600dp/bools.xml b/app/src/main/res/values-sw600dp/bools.xml
deleted file mode 100644
index 2d2cf03..0000000
--- a/app/src/main/res/values-sw600dp/bools.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- false
-
\ No newline at end of file
diff --git a/app/src/main/res/values-sw600dp/dimens.xml b/app/src/main/res/values-sw600dp/dimens.xml
deleted file mode 100644
index 513920d..0000000
--- a/app/src/main/res/values-sw600dp/dimens.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- @dimen/d32
-
\ No newline at end of file
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 84ea67b..94fc7d3 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -1,75 +1,28 @@
- Sysctl GUI
Kök erişimi denetleniyor
Busybox kontrol ediliyor
- Eski veritabanı şeması kontrol ediliyor
- Veritabanı taşıma gerçekleştiriliyor
Hata
Ayarlar
Hakkında
- Değişkenleri göster
- Tüm çekirdek parametrelerini bir listede görüntüleme ve düzenleme
- Sysctl işlemleri
- Dosyadan değerleri oku
- Seçilen bir .conf veya .json dosyasından parametre yüklemek için bu seçeneği kullanın
- Favorileri göster
- Bu listeden en sevdiğiniz çekirdek parametrelerine erişin
Çıkış
Çekirdek parametreleri
Değişkenlere göz at
Belirli bir çekirdek parametresi için /proc/sys üzerine göz atın
Parametreleri düzenle
- Yanlış olan bir şey doğru değil
- Lütfen giriş alanlarını kontrol edin
- Başarısız
- Geçersiz yol
- Sysctl
- Sysctl, çalışma zamanında çekirdek parametrelerini değiştirmek için kullanılır. Mevcut parametreler/proc/sys altında listelenen parametrelerdir.
- Bu uygulamanın temel amacı, bu parametreleri düzenlemek için grafiksel bir yol sağlamaktır. Kaynak kodunu şurada bulabilirsiniz: GitHub.DestekXDA Başlığı.
- Önce klasörleri listele
- Çekirdek parametresi tarayıcı seçeneğini kullanırken önce klasörleri listeleyin
- Uygulama
- Seçenekler
- Boş değerlere izin ver
- Gönderim modu
- Tahmin giriş türü
Bitti
- Uyumluluğu sağlamak için Busybox kullanılacaktır
- Busybox kutusunu kullan
- Şu anda sistemin sysctl ikilisine güveniliyor
- Parametrenin değerine göre klavye için en iyi giriş türünü ayarlamaya çalışın
Kök erişimi bulunamadı. Yalnızca kök erişimi olan özellikleri düzenleyebilirsiniz.
- Bilgi
Bu parametre için bilgi yok
Ara
- Başlatma
- Başlangıçta parametrelerin uygulanmasına izin ver
Parametreler uygulanıyor
Parametreleriniz uygulanıyor
%d saniye içinde uygulanıyor…
Başlatma Bildirimi
Parametreleriniz uygulanıyor
- Parametreleri yönet
- Başlangıçta uygulanacak parametreleri yönetin
- Parametreleri dışa aktar
- Bu uygulamayı kullanarak değiştirdiğiniz parametreleri bir JSON dosyasına aktarın
- Parametre bulunamadı
- Parametreleri geri yükle
- Önceki parametre yedeklemenizi geri yükleyin
Parametreler içe aktarılamadı
- Geçersiz veya hatalı biçimlendirilmiş JSON dosyası
- Dosya açılamıyor: geçersiz dosya türü.
Parametreler içe aktarılamıyor: boş dosya.
Parametreler içe aktarılamıyor: dosyada bir biçimlendirme hatası var.
- %d parametre uygulandı
- Belgeyi aç
Düzenle
Kaldır
- Başlatma gecikmesi
- Yürütme %d saniye gecikecek
- Yürütme gecikmeyecek
- Favori olarak ayarla
- Favorilerden kaldır
favori seçilmedi
Görevli listesi
Birincil liste
@@ -80,43 +33,23 @@
Önyükleme listesine uygula
SysctlGUI: Görev profili #%d uygulandı
Bir Görevli listesi seçin
- Görevli listesinden kaldırıldı #%d
- Görevli listesine #%d eklendi
Görevli ile kullanılacak bir liste seçin
Görevli tarafından tetiklendikten sonra, eklenti seçilen listedeki tüm parametreleri ilgili değerleriyle birlikte hemen yeniden uygulayacaktır.
Görevli ayarları
- Görevliye ekle
- Görevliden kaldır
- Parametreler uygulanamadı: %s
Geri al
Sysctl GUI\'yi başlat
Önyüklemede Sysctl GUI\'yi çalıştır
Kök erişimi gerektirir
Kök erişimi gerekli
- Parametreleri içe aktar
- Parametreleri yedekle
- Mevcut tüm çalışma zamanı parametrelerini yedekleyin. Bunların tamamının geri yüklendikten sonra tekrar uygulanamayacağını lütfen unutmayın.
- Parametreler dışa aktarılamadı
GÇ hatası nedeniyle parametre dışa aktarımı başarısız oldu
Parametre dışa aktarılamadı: dışa aktarılacak parametre yok
Dışa aktarma seçenekleri
- Parametreler başarıyla dışa aktarıldı
Çekirdek parametrelerini içe aktarın, dışa aktarın veya yedekleyin
- Lütfen bekleyin, bu biraz zaman alabilir…
Göz at
Dışa Aktar
- Parametre listesi
Tekrar deneyin
Uygula
Geri Yükle
Parametre
Değer
- Son uygulanan değer
- Mevcut değer
- Seçilen değer uygulanamadı
- Seçilen değer uygulanamadı. \"echo\" modunu kullanmayı deneyin.
- Dinamik renkler
- Mevcut olduğunda dinamik renkler (Monet teması) kullanın
- Karanlık temayı zorla
- Mevcut olduğunda koyu temayı zorla
\ No newline at end of file
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/arrays.xml b/app/src/main/res/values/arrays.xml
index fcc996f..e0df010 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -1,14 +1,4 @@
-
- - Use sysctl -w
- - Use echo \'value\' > /proc/sys/…
-
-
-
- - sysctl
- - echo
-
-
- @string/tasker_list_primary
- @string/tasker_list_secondary
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
deleted file mode 100644
index 55f0cef..0000000
--- a/app/src/main/res/values/attrs.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values/bools.xml b/app/src/main/res/values/bools.xml
deleted file mode 100644
index 2d2cf03..0000000
--- a/app/src/main/res/values/bools.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- false
-
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
deleted file mode 100644
index ab87823..0000000
--- a/app/src/main/res/values/dimens.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
- 160dp
- @dimen/d16
- @dimen/d16
-
-
-
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..2a5ce33 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -2,75 +2,29 @@
Sysctl GUI
Checking for root access
Checking for busybox
- Checking for old database schema
- Performing database migration
Error
Settings
About
- Show variables
- Display and edit all kernel parameters in a list
- Sysctl operations
- Read values from file
- Use this option to load parameters from a selected .conf or .json file
- Show favorites
- Access your favorite kernel parameters from this list
Exit
Kernel parameters
Browse variables
Browse /proc/sys for a specific kernel parameter
Edit parameters
- Something wrong is not right
- Please check the input fields
- Failed
- Invalid path
- Sysctl
- Sysctl is used to modify kernel parameters at runtime. The parameters available are those listed under /proc/sys.
- This apps main purpose is to provide a graphical way to edit these parameters. You can find its source code on GitHub. Support on XDA Thread.
- List folders first
- List folders first when using the kernel parameter browser option
- Application
- Operations
- Allow blank values
- Commit mode
- Guess input type
Done
- Busybox will be used to ensure compatibility
- Use busybox
- Currently relying on system\'s sysctl binary
- Try to set the best input type for the keyboard based on the value of the parameter
Root access not found. You can only edit properties with root access.
- Information
- No info available for this parameter
+ No documentation available for this parameter
Search
- Start Up
- Allow to apply parameters on start up
Applying parameters
Applying your parameters
Applying in %d seconds…
Start Up Notification
Applying your parameters
- Manage parameters
- Manage the parameters that will be applied at startup
- Export parameters
- Export parameters that you have altered using this application to a JSON file
- No parameters found
- Restore parameters
- Restore your previous parameter backup
Failed to import parameters
- Invalid or malformed JSON file
- Can\'t open file: invalid file type.
Can\'t import params: empty file.
Can\'t import params: there is a formatting error in the file.
- %d parameter(s) applied
- Open documentation
Edit
Remove
- Startup delay
- Execution will be delayed by %d seconds
- Execution will not delayed
- Set as favorite
- Remove from favorites
- no favorites selected
+ No favorites added
Tasker list
Primary list
Secondary list
@@ -80,43 +34,75 @@
Apply on boot list
SysctlGUI: Tasker profile #%d applied
Select a Tasker list
- Removed from Tasker list #%d
- Added to Tasker list #%d
Select a list to be used with Tasker
Once triggered by Tasker, the plugin will immediately reapply all parameters from the selected list with their respective values.
Tasker settings
- Add to Tasker
- Remove from Tasker
- Failed to apply params: %s
Undo
Start Sysctl GUI
Run Sysctl GUI at boot
Requires root access
Root access required
- Import parameters
- Backup parameters
- 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
- Please wait, this may take some time…
Browse
Export
- Param list
Try again
Apply
Restore
Parameter
Value
- Last applied value
- Current value
- Selected value could not be applied
- Selected value could not be applied. Try using \"echo\" mode.
- Dynamic colors
- 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
+ Param deleted: %s
+ Go back
+ Read documentation for \"%1$s\"
+ Directory: %1$s
+ Parameter: %1$s
+ Parameter icon
+ Navigate to directory
+ Marked as favorite
+ Toggle %1$s
+ Favorite
+ Toggle Tasker param
+ Value applied successfully
+ Copied to clipboard
+ Long press to copy
+ Tasker list: %1$s
+ New value
+ Read more
+ Documentation
+ Parameter value
+ Applying preset
+ %1$d parameters found
+ Import
+ Loading preset…
+ Success
+ Presets successfully imported
+ Import presets from a file
+ Export presets to a file
+ Search kernel parameters
+ No suggestions available
+ No results found for \"%1$s\"
+ Enter a query to search for kernel parameters
+ Search Icon
+ Clear search
+ Recent searches
+ History item
+ Clear history item
+ Export complete
+ Failed to import params
+ Blank values are currently not allowed
+ Value refused to apply with current commit mode
+ Command execution failed
+ Suggestions
+ Favorite Kernel Params
+ History deleted
+ Startup params
+ Sysctl is used to modify kernel parameters at runtime. The parameters available are those listed under /proc/sys and this app\'s main purpose is to provide a graphical way to edit them.
diff --git a/app/src/main/res/xml/favorites_widget_info.xml b/app/src/main/res/xml/favorites_glance_widget_info.xml
similarity index 53%
rename from app/src/main/res/xml/favorites_widget_info.xml
rename to app/src/main/res/xml/favorites_glance_widget_info.xml
index 8dbf6cc..7e03039 100644
--- a/app/src/main/res/xml/favorites_widget_info.xml
+++ b/app/src/main/res/xml/favorites_glance_widget_info.xml
@@ -1,10 +1,12 @@
+ android:widgetCategory="home_screen"
+ tools:ignore="UnusedAttribute" />
\ No newline at end of file
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/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
deleted file mode 100644
index 6f22260..0000000
--- a/buildSrc/src/main/kotlin/AndroidX.kt
+++ /dev/null
@@ -1,27 +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 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"
-
- 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/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/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 e2da347..7208696 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,23 @@ 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)
+ api(libs.material)
+
+ androidTestApi(platform(libs.androidx.compose.bom))
+ debugApi(libs.androidx.ui.tooling)
+ debugApi(libs.androidx.ui.test.manifest)
}
+
diff --git a/common/design/proguard-rules.pro b/common/design/proguard-rules.pro
index 481bb43..8396c29 100644
--- a/common/design/proguard-rules.pro
+++ b/common/design/proguard-rules.pro
@@ -18,4 +18,9 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
+#-renamesourcefileattribute SourceFile
+
+-keep class com.androidvip.sysctlgui.design.theme.ColorKt { *; }
+-keep class com.androidvip.sysctlgui.design.theme.ThemeKt { *; }
+-keep class com.androidvip.sysctlgui.design.theme.TypeKt { *; }
+-keep class com.androidvip.sysctlgui.design.utils.UiUtilsKt { *; }
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..4268a32 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
@@ -2,7 +2,8 @@ package com.androidvip.sysctlgui.design.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.MaterialExpressiveTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
@@ -10,90 +11,264 @@ 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,
+)
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@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(
+ MaterialExpressiveTheme(
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/java/com/androidvip/sysctlgui/design/utils/UiUtils.kt b/common/design/src/main/java/com/androidvip/sysctlgui/design/utils/UiUtils.kt
new file mode 100644
index 0000000..13018c5
--- /dev/null
+++ b/common/design/src/main/java/com/androidvip/sysctlgui/design/utils/UiUtils.kt
@@ -0,0 +1,12 @@
+package com.androidvip.sysctlgui.design.utils
+
+import android.content.res.Configuration
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.ui.platform.LocalConfiguration
+
+@Composable
+@ReadOnlyComposable
+fun isLandscape(): Boolean {
+ return LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
+}
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 0000000..25a35e4
Binary files /dev/null and b/common/design/src/main/res/font/passionone_bold.ttf differ
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 0000000..099be0f
Binary files /dev/null and b/common/design/src/main/res/font/passionone_regular.ttf differ
diff --git a/common/design/src/main/res/font/sansation_bold.ttf b/common/design/src/main/res/font/sansation_bold.ttf
new file mode 100644
index 0000000..41f3108
Binary files /dev/null and b/common/design/src/main/res/font/sansation_bold.ttf differ
diff --git a/common/design/src/main/res/font/sansation_bold_italic.ttf b/common/design/src/main/res/font/sansation_bold_italic.ttf
new file mode 100644
index 0000000..8677879
Binary files /dev/null and b/common/design/src/main/res/font/sansation_bold_italic.ttf differ
diff --git a/common/design/src/main/res/font/sansation_regular.ttf b/common/design/src/main/res/font/sansation_regular.ttf
new file mode 100644
index 0000000..e9bda17
Binary files /dev/null and b/common/design/src/main/res/font/sansation_regular.ttf differ
diff --git a/common/design/src/main/res/font/sansation_regular_italic.ttf b/common/design/src/main/res/font/sansation_regular_italic.ttf
new file mode 100644
index 0000000..95258de
Binary files /dev/null and b/common/design/src/main/res/font/sansation_regular_italic.ttf differ
diff --git a/common/design/src/main/res/layout/dialog_web.xml b/common/design/src/main/res/layout/dialog_web.xml
deleted file mode 100644
index 7947cf0..0000000
--- a/common/design/src/main/res/layout/dialog_web.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/common/design/src/main/res/layout/modal_bottom_sheet.xml b/common/design/src/main/res/layout/modal_bottom_sheet.xml
deleted file mode 100644
index ca17b9e..0000000
--- a/common/design/src/main/res/layout/modal_bottom_sheet.xml
+++ /dev/null
@@ -1,79 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/common/design/src/main/res/layout/preference_widget_switch_compat.xml b/common/design/src/main/res/layout/preference_widget_switch_compat.xml
deleted file mode 100644
index 8641b11..0000000
--- a/common/design/src/main/res/layout/preference_widget_switch_compat.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
\ No newline at end of file
diff --git a/common/design/src/main/res/values/colors.xml b/common/design/src/main/res/values/colors.xml
index 578d018..262fc85 100644
--- a/common/design/src/main/res/values/colors.xml
+++ b/common/design/src/main/res/values/colors.xml
@@ -1,152 +1,72 @@
- @color/gray_800
- @color/gray_900
- @color/gray_700
- @color/gray_500
- @color/violet_700
- @color/violet_500
- @color/neutral_300
- @color/gray_300
+ @color/md_theme_light_primary
+ @color/md_theme_light_onPrimaryContainer
+ @color/md_theme_light_tertiary
+ @color/md_theme_light_tertiaryContainer
- #f6f7f8
- #e1f0fa
- #c2d9e4
- #a2becc
- #81a3b4
- #688fa2
- #4e7c90
- #426d7f
- #345969
- #274654
- #16313d
-
- #e6e6ef
- #c1c0d9
- #9997be
- #736fa5
- #595293
- #413581
- #3b2e79
- #33256e
- #2c1c62
- #1f0b4d
-
- #e0f2f1
- #b3dfda
- #82cbc3
- #51b6ab
- #2fa699
- #179687
- #15897a
- #15897a
- #12796a
- #0b4d3f
-
- #80E4A9
- #00C853
- #00B439
-
- #FFCB80
- #FF9600
- #FF7900
-
- #feebee
- #feced2
- #ed9b9a
- #e27573
- #ec5751
- #f04837
- #e13f36
- #cf3630
- #c22f29
- #b3251e
-
- #000000
#DE000000
- #B3000000
- #8A000000
- #80000000
- #61000000
- #12000000
-
- #FFFFFF
- #DEFFFFFF
- #B3FFFFFF
- #8AFFFFFF
- #80FFFFFF
- #61FFFFFF
- #12FFFFFF
-
- #e3eaf5
- #bfccda
- #9aaabb
- #75899e
- #5b7289
- #415b74
- #354e65
- #273c50
#192b3b
- #021219
+ #FFFFFF
- #006685
+ #4B5C92
#FFFFFF
- #BFE9FF
- #001F2A
- #4D616C
+ #DBE1FF
+ #334478
+ #595E72
#FFFFFF
- #D0E6F3
- #081E27
- #5E5A7D
+ #DDE1F9
+ #414659
+ #1D6B50
#FFFFFF
- #E4DFFF
- #1A1836
+ #A7F2D0
+ #00513A
#BA1A1A
#FFDAD6
#FFFFFF
- #410002
- #FBFCFE
- #191C1E
- #FBFCFE
- #191C1E
- #DCE3E9
- #40484C
- #70787D
- #F0F1F3
- #2E3133
- #6DD2FF
+ #93000A
+ #FAF8FF
+ #1A1B21
+ #FAF8FF
+ #1A1B21
+ #E2E2EC
+ #45464F
+ #757680
+ #F1F0F7
+ #2F3036
+ #B4C5FF
#000000
- #006685
- #C0C8CD
+ #4B5C92
+ #C5C6D0
#000000
- #6DD2FF
- #003547
- #004D65
- #BFE9FF
- #B4CAD6
- #1F333D
- #364954
- #D0E6F3
- #C7C2EA
- #2F2D4C
- #464364
- #E4DFFF
+ #B4C5FF
+ #1A2D60
+ #334478
+ #DBE1FF
+ #C1C5DD
+ #2B3042
+ #414659
+ #DDE1F9
+ #8CD5B4
+ #003827
+ #00513A
+ #A7F2D0
#FFB4AB
#93000A
#690005
#FFDAD6
- #191C1E
- #E1E2E5
- #191C1E
- #E1E2E5
- #40484C
- #C0C8CD
- #8A9297
- #191C1E
- #E1E2E5
- #006685
+ #121318
+ #E3E2E9
+ #121318
+ #E3E2E9
+ #45464F
+ #C5C6D0
+ #8F909A
+ #2F3036
+ #E3E2E9
+ #4B5C92
#000000
- #6DD2FF
- #40484C
+ #B4C5FF
+ #45464F
#000000
diff --git a/common/utils/build.gradle.kts b/common/utils/build.gradle.kts
index 6f0ac70..68576a4 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,14 +9,13 @@ android {
defaultConfig {
minSdk = AppConfig.minSdkVersion
- targetSdk = AppConfig.targetSdkVersion
testInstrumentationRunner = AppConfig.testInstrumentationRunner
consumerProguardFiles(AppConfig.proguardConsumerRules)
}
buildTypes {
- release {
+ getByName("release") {
isMinifyEnabled = !AppConfig.devCycle
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
@@ -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/common/utils/proguard-rules.pro b/common/utils/proguard-rules.pro
index 481bb43..fa38585 100644
--- a/common/utils/proguard-rules.pro
+++ b/common/utils/proguard-rules.pro
@@ -18,4 +18,8 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
+#-renamesourcefileattribute SourceFile
+
+-keep class com.androidvip.sysctlgui.utils.BaseViewModel { *; }
+-keep class com.androidvip.sysctlgui.utils.ContextUtilsKt { *; }
+-keep class com.androidvip.sysctlgui.utils.MiscKt { *; }
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..ad8eace
--- /dev/null
+++ b/common/utils/src/main/kotlin/com/androidvip/sysctlgui/utils/Misc.kt
@@ -0,0 +1,33 @@
+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 an "=" character.
+ * - Have a key that matches the pattern: `^[a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*$`
+ * (e.g., "vm.swappiness", "net.ipv4.tcp_congestion_control").
+ * - Have a non-blank value after the "=".
+ *
+ * @return `true` if the string is a valid sysctl line, `false` otherwise.
+ */
+fun String.isValidSysctlOutputLine(): Boolean {
+ val trimmedLine = this.trim()
+ val linePattern = """^([a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*)\s*=\s*(.+)$""".toRegex()
+ val matchResult = linePattern.matchEntire(trimmedLine)
+
+ return matchResult != null
+}
+
+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 9e7f726..56b4f2d 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,23 @@ 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)
+ 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(Dependencies.koinAndroid)
+ // Room
+ implementation(libs.androidx.room.runtime)
+ implementation(libs.androidx.room.ktx)
+ ksp(libs.androidx.room.compiler)
- 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/consumer-rules.pro b/data/consumer-rules.pro
new file mode 100644
index 0000000..2e8e82c
--- /dev/null
+++ b/data/consumer-rules.pro
@@ -0,0 +1 @@
+-keep class com.androidvip.sysctlgui.data.models.KernelParamDTO { *; }
diff --git a/data/proguard-rules.pro b/data/proguard-rules.pro
index fbcf9ce..ff875af 100644
--- a/data/proguard-rules.pro
+++ b/data/proguard-rules.pro
@@ -20,5 +20,4 @@
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-# Application classes that will be serialized/deserialized over Gson
--keep class com.androidvip.sysctlgui.data.models.RoomKernelParam { *; }
\ No newline at end of file
+-keep class com.androidvip.sysctlgui.data.models.KernelParamDTO { *; }
\ No newline at end of file
diff --git a/data/src/androidTest/java/com/androidvip/sysctlgui/data/ExampleInstrumentedTest.kt b/data/src/androidTest/java/com/androidvip/sysctlgui/data/ExampleInstrumentedTest.kt
deleted file mode 100644
index 9f0a565..0000000
--- a/data/src/androidTest/java/com/androidvip/sysctlgui/data/ExampleInstrumentedTest.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.androidvip.sysctlgui.data
-
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.ext.junit.runners.AndroidJUnit4
-
-import org.junit.Test
-import org.junit.runner.RunWith
-
-import org.junit.Assert.*
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-@RunWith(AndroidJUnit4::class)
-class ExampleInstrumentedTest {
- @Test
- fun useAppContext() {
- // Context of the app under test.
- val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- assertEquals("com.androidvip.sysctlgui.data", appContext.packageName)
- }
-}
\ No newline at end of file
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..02b2ddc
--- /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"),
+ AllowBlankValues("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..7f2c886 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,43 @@
package com.androidvip.sysctlgui.data.di
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 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 kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
+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 { Dispatchers.IO }
factory { RootUtils(Dispatchers.Default) }
+ factory { PresetsFileProcessor(androidContext().contentResolver) }
+ factory { AndroidStringProvider(androidApplication()) }
}
val dbModule = module {
@@ -25,17 +46,57 @@ 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
+
+ single { UserRepositoryImpl(paramDao = get().paramDao()) }
+
+ factory {
+ AppSettingsRepositoryImpl(
+ context = androidContext(),
+ sharedPreferences = get(),
+ isTaskerInstalled = get(),
+ rootUtils = get()
+ )
+ }
+
+ 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..3909de2
--- /dev/null
+++ b/data/src/main/java/com/androidvip/sysctlgui/data/models/KernelParamDTO.kt
@@ -0,0 +1,43 @@
+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) {
+ companion object {
+ fun fromKernelParam(param: KernelParam): KernelParamDTO {
+ return KernelParamDTO(
+ name = param.name,
+ path = param.path,
+ value = param.value,
+ isFavorite = param.isFavorite,
+ isTaskerParam = param.isTaskerParam,
+ taskerList = param.taskerList
+ )
+ }
+ }
+}
+
+internal const val PARAMS_TABLE_NAME = "RoomKernelParam" // For compatibility with older versions
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..d21206f 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,107 @@ 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
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+/**
+ * Implementation of [AppPrefs] that uses [SharedPreferences] to store and retrieve app preferences.
+ */
class AppPrefsImpl(private val prefs: SharedPreferences) : AppPrefs {
+ @Suppress("UNCHECKED_CAST")
+ override fun observeKey(key: String, default: T): Flow = callbackFlow {
+ val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, updatedKey ->
+ if (updatedKey == key) {
+ trySend(prefs.all[key] as T ?: default)
+ }
+ }
+ prefs.registerOnSharedPreferenceChangeListener(listener)
+ trySend(prefs.all[key] as T ?: default)
+ awaitClose { prefs.unregisterOnSharedPreferenceChangeListener(listener) }
+ }
+
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.AllowBlankValues.key, false)
set(value) {
- prefs.edit { putBoolean(Consts.Prefs.ALLOW_BLANK, value) }
+ prefs.edit { putBoolean(Prefs.AllowBlankValues.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)
- set(value) {
- prefs.edit { putBoolean(Consts.Prefs.SHOW_TASKER_TOAST, value) }
- }
- override var migrationCompleted: Boolean
- get() = prefs.getBoolean(Consts.Prefs.MIGRATION_COMPLETED, false)
+ get() = prefs.getBoolean(Prefs.ShowTaskerToast.key, true)
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..c0d87ca
--- /dev/null
+++ b/data/src/main/java/com/androidvip/sysctlgui/data/repository/AppSettingsRepositoryImpl.kt
@@ -0,0 +1,210 @@
+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.R
+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.models.KEY_CONTRIBUTORS
+import com.androidvip.sysctlgui.domain.models.KEY_DELETE_HISTORY
+import com.androidvip.sysctlgui.domain.models.KEY_MANAGE_PARAMS
+import com.androidvip.sysctlgui.domain.models.KEY_SOURCE_CODE
+import com.androidvip.sysctlgui.domain.models.KEY_TRANSLATIONS
+import com.androidvip.sysctlgui.domain.repository.AppSettingsRepository
+import com.androidvip.sysctlgui.domain.usecase.IsTaskerInstalledUseCase
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import kotlin.coroutines.CoroutineContext
+
+class AppSettingsRepositoryImpl(
+ private val context: Context,
+ private val sharedPreferences: SharedPreferences,
+ private val rootUtils: RootUtils,
+ private val isTaskerInstalled: IsTaskerInstalledUseCase,
+ private val ioContext: CoroutineContext = Dispatchers.IO
+) : AppSettingsRepository {
+ override suspend fun getAppSettings(): List> = withContext(ioContext) {
+ val isTaskerInstalled = isTaskerInstalled()
+ val usingDynamicColors = sharedPreferences.getBoolean(Prefs.DynamicColors.key, false)
+ val supportsDynamicColors = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
+ val currentCommitMode = sharedPreferences.getString(
+ Prefs.CommitMode.key,
+ CommitMode.SYSCTL.name.lowercase()
+ ) ?: CommitMode.SYSCTL.name
+
+ listOf(
+ /////////// GENERAL SETTINGS ////////////
+ AppSetting(
+ key = Prefs.ListFoldersFirst.key,
+ value = sharedPreferences.getBoolean(Prefs.ListFoldersFirst.key, true),
+ category = context.getString(R.string.prefs_category_general),
+ title = context.getString(R.string.prefs_list_folders_first_title),
+ description = context.getString(R.string.prefs_list_folders_first_description),
+ type = SettingItemType.Switch,
+ ),
+ AppSetting(
+ key = Prefs.GuessInputType.key,
+ value = sharedPreferences.getBoolean(Prefs.GuessInputType.key, true),
+ category = context.getString(R.string.prefs_category_general),
+ title = context.getString(R.string.prefs_guess_input_type_title),
+ description = context.getString(R.string.prefs_guess_input_type_description),
+ type = SettingItemType.Switch,
+ ),
+ AppSetting(
+ key = Prefs.UseOnlineDocs.key,
+ value = sharedPreferences.getBoolean(Prefs.UseOnlineDocs.key, true),
+ category = context.getString(R.string.prefs_category_general),
+ title = context.getString(R.string.prefs_online_docs_title),
+ description = context.getString(R.string.prefs_online_docs_description),
+ type = SettingItemType.Switch,
+ ),
+ AppSetting(
+ key = KEY_DELETE_HISTORY,
+ value = Unit,
+ category = context.getString(R.string.prefs_category_general),
+ title = context.getString(R.string.pref_search_history_title),
+ description = context.getString(R.string.pref_search_history_description),
+ type = SettingItemType.Text,
+ ),
+
+ /////////// THEME SETTINGS ////////////
+
+ AppSetting(
+ key = Prefs.ForceDarkTheme.key,
+ value = sharedPreferences.getBoolean(Prefs.ForceDarkTheme.key, false),
+ category = context.getString(R.string.prefs_category_theme),
+ title = context.getString(R.string.prefs_force_dark_title),
+ description = context.getString(R.string.prefs_force_dark_description),
+ type = SettingItemType.Switch,
+ ),
+ AppSetting(
+ key = Prefs.DynamicColors.key,
+ value = usingDynamicColors,
+ enabled = supportsDynamicColors,
+ category = context.getString(R.string.prefs_category_theme),
+ title = context.getString(R.string.prefs_dynamic_colors_title),
+ description = context.getString(R.string.prefs_dynamic_colors_description),
+ type = SettingItemType.Switch,
+ ),
+ AppSetting(
+ key = Prefs.ContrastLevel.key,
+ enabled = !usingDynamicColors,
+ value = sharedPreferences.getInt(Prefs.ContrastLevel.key, 1),
+ category = context.getString(R.string.prefs_category_theme),
+ title = context.getString(R.string.prefs_contrast_level_title),
+ description = context.getString(R.string.prefs_contrast_level_description),
+ type = SettingItemType.Slider,
+ values = listOf(CONTRAST_LEVEL_NORMAL, CONTRAST_LEVEL_MEDIUM, CONTRAST_LEVEL_HEIGH),
+ ),
+
+ /////////// COMMIT SETTINGS ////////////
+
+ AppSetting(
+ key = Prefs.CommitMode.key,
+ value = currentCommitMode,
+ category = context.getString(R.string.prefs_category_operations),
+ title = context.getString(
+ R.string.prefs_commit_mode_title_format,
+ currentCommitMode
+ ),
+ description = context.getString(R.string.prefs_commit_mode_description),
+ 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 = context.getString(R.string.prefs_category_operations),
+ title = context.getString(R.string.prefs_use_busybox_title),
+ description = context.getString(R.string.prefs_use_busybox_description),
+ type = SettingItemType.Switch,
+ ),
+ AppSetting(
+ key = Prefs.AllowBlankValues.key,
+ value = sharedPreferences.getBoolean(Prefs.AllowBlankValues.key, false),
+ category = context.getString(R.string.prefs_category_operations),
+ title = context.getString(R.string.prefs_allow_blank_values_title),
+ type = SettingItemType.Switch,
+ ),
+ AppSetting(
+ key = Prefs.ShowTaskerToast.key,
+ enabled = isTaskerInstalled,
+ value = sharedPreferences.getBoolean(Prefs.ShowTaskerToast.key, isTaskerInstalled),
+ category = context.getString(R.string.prefs_category_operations),
+ title = context.getString(R.string.prefs_show_tasker_toast_title),
+ description = context.getString(R.string.prefs_show_toast_description),
+ type = SettingItemType.Switch,
+ ),
+
+ /////////// STARTUP SETTINGS ////////////
+
+ AppSetting(
+ key = Prefs.RunOnStartup.key,
+ value = sharedPreferences.getBoolean(Prefs.RunOnStartup.key, false),
+ category = context.getString(R.string.prefs_category_startup),
+ title = context.getString(R.string.prefs_run_on_startup_title),
+ description = context.getString(R.string.prefs_run_on_startup_description),
+ type = SettingItemType.Switch,
+ ),
+ AppSetting(
+ key = KEY_MANAGE_PARAMS,
+ value = Unit,
+ category = context.getString(R.string.prefs_category_startup),
+ title = context.getString(R.string.prefs_manage_parameters_title),
+ description = context.getString(R.string.prefs_manage_parameters_description),
+ type = SettingItemType.Text,
+ ),
+ AppSetting(
+ key = Prefs.StartupDelay.key,
+ value = sharedPreferences.getInt(Prefs.StartupDelay.key, 0),
+ category = context.getString(R.string.prefs_category_startup),
+ title = context.getString(R.string.prefs_startup_delay_title),
+ description = context.getString(R.string.prefs_startup_delay_description),
+ type = SettingItemType.Slider,
+ values = (0..10).toList(),
+ ),
+
+ /////////// OTHERS ////////////
+ AppSetting(
+ key = KEY_SOURCE_CODE,
+ value = SOURCE_CODE_URL,
+ category = context.getString(R.string.prefs_category_others),
+ title = context.getString(R.string.pref_source_code_title),
+ description = context.getString(R.string.pref_source_code_description),
+ iconResource = R.drawable.ic_code,
+ type = SettingItemType.Text,
+ ),
+ AppSetting(
+ key = KEY_CONTRIBUTORS,
+ value = "$SOURCE_CODE_URL/graphs/contributors",
+ category = context.getString(R.string.prefs_category_others),
+ title = context.getString(R.string.pref_contributors_title),
+ description = "free-bots, mikropsoft (Holi)",
+ iconResource = R.drawable.ic_group,
+ type = SettingItemType.Text,
+ ),
+ AppSetting(
+ key = KEY_TRANSLATIONS,
+ value = "$SOURCE_CODE_URL/blob/develop/TRANSLATING.md",
+ category = context.getString(R.string.prefs_category_others),
+ title = context.getString(R.string.pref_translations_title),
+ description = context.getString(R.string.pref_translations_description),
+ iconResource = R.drawable.ic_language,
+ type = SettingItemType.Text,
+ )
+ )
+ }
+}
+
+const val CONTRAST_LEVEL_NORMAL = 1
+const val CONTRAST_LEVEL_MEDIUM = 2
+const val CONTRAST_LEVEL_HEIGH = 3
+const val SOURCE_CODE_URL = "https://github.com/Lennoard/SysctlGUI"
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..ee6f3ea
--- /dev/null
+++ b/data/src/main/java/com/androidvip/sysctlgui/data/repository/DocumentationRepositoryImpl.kt
@@ -0,0 +1,41 @@
+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
+import kotlinx.coroutines.withTimeout
+import kotlinx.coroutines.withTimeoutOrNull
+
+/**
+ * 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.
+ * 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) {
+ withTimeoutOrNull(REQUEST_TIMEOUT_MS) {
+ onlineDataSource.getDocumentation(param)
+ } ?: offlineDataSource.getDocumentation(param)
+ } else {
+ offlineDataSource.getDocumentation(param)
+ }
+ }
+
+ companion object {
+ private const val REQUEST_TIMEOUT_MS = 3000L
+ }
+}
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..cc080e0 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,147 @@
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.isValidSysctlOutputLine
+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()
- }
- }
+ 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()
+ }
+ .toList()
- override suspend fun removeUserParam(param: DomainKernelParam) = withContext(ioContext) {
- return@withContext roomParamDataSource.remove(param).also {
- changeListener?.onChange()
- }
- }
+ emit(paramsList)
+ }.flowOn(ioDispatcher)
- override suspend fun clearUserParams() = withContext(ioContext) {
- return@withContext roomParamDataSource.clear().also {
- jsonParamDataSource.clear()
- changeListener?.onChange()
- }
- }
+ override suspend fun getRuntimeParam(paramName: String, useBusybox: Boolean): KernelParam? {
+ val command = String.format(
+ SYSCTL_GET_PARAM_COMMAND_FORMAT,
+ if (useBusybox) BUSYBOX_PREFIX else "",
+ paramName
+ )
- override suspend fun performDatabaseMigration() = withContext(ioContext) {
- val jsonParams = getJsonParams()
+ val paramValue = runCatching {
+ rootUtils.executeCommandAndStreamOutput(command).single()
+ }.getOrNull() ?: return null
- return@withContext roomParamDataSource.addAll(jsonParams, true).also {
- changeListener?.onChange()
- }
+ return KernelParam.createFromName(
+ name = paramName,
+ value = paramValue
+ )
}
- 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)
- }
- }
+ 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)
}
- val type: Type = object : TypeToken>() {}.type
- return@withContext Gson().fromJson(rawText, type)
- }
-
- override suspend fun importParamsFromConf(
- stream: InputStream
- ): List = withContext(ioContext) {
- fun String.validConfLine() = !startsWith("#") && !startsWith(";") && isNotEmpty()
- val readParams = mutableListOf()
-
- if (stream.available() == 0) throw EmptyFileException()
- 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)
+ 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
+ if (file.isDirectory) {
+ KernelParam.createFromPath(path, "")
+ } else {
+ val fileContent = runCatching { file.readText() }.getOrNull()
+ if (fileContent != null) {
+ KernelParam.createFromPath(path, fileContent)
+ } else {
+ val command = String.format(CAT_COMMAND_FORMAT, path)
+ val paramValue = rootUtils.executeCommandAndStreamOutput(command)
+ .toList()
+ .joinToString("\n")
+ KernelParam.createFromPath(path, paramValue)
}
- )
- }.onFailure {
- throw MalformedLineException()
- }
- }
- return@withContext readParams
- }
-
- override suspend fun exportParams(
- params: List,
- fileDescriptor: FileDescriptor
- ) = withContext(ioContext) {
- return@withContext FileOutputStream(fileDescriptor).use { stream ->
- stream.write(Gson().toJson(params).toByteArray())
- }
- }
-
- override suspend fun backupParams(
- params: List,
- fileDescriptor: FileDescriptor
- ) = withContext(ioContext) {
- val rawText = buildString {
- params.forEach { param ->
- appendLine(param.toString())
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to process file: ${file.path}", e)
+ null
}
}
+ emit(params)
+ }.flowOn(ioDispatcher)
- return@withContext FileOutputStream(fileDescriptor).use { stream ->
- stream.write(rawText.toByteArray())
- }
+ override fun getParamsFromPath(path: String): Flow> {
+ val files = File(path).listFiles()?.toList() ?: emptyList()
+ return getParamsFromFiles(files)
}
- 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
- }
+ private fun String.isValidSysctlOutput(): Boolean {
+ return isValidSysctlOutputLine() &&
+ !this.contains("denied", ignoreCase = true) &&
+ !this.startsWith("sysctl")
}
interface ChangeListener {
fun onChange()
}
+
+ 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..a75d123
--- /dev/null
+++ b/data/src/main/java/com/androidvip/sysctlgui/data/repository/UserRepositoryImpl.kt
@@ -0,0 +1,62 @@
+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 userParamsFlow: Flow>
+ get() = paramDao.getAllAsFlow()
+
+ override suspend fun getUserParams(): List = withContext(coroutineContext) {
+ paramDao.getAll()
+ }
+
+ override suspend fun getParamByName(name: String) = withContext(coroutineContext) {
+ paramDao.getParamByName(name)
+ }
+
+ override suspend fun upsertUserParam(param: KernelParam) = withContext(coroutineContext) {
+ val currentDatabaseParam = getParamByName(param.name)
+ val newParam = KernelParamDTO.fromKernelParam(param).copy(
+ id = currentDatabaseParam?.id ?: 0
+ )
+ paramDao.upsert(newParam)
+ }
+
+ override suspend fun upsertUserParams(
+ params: List
+ ) = withContext(coroutineContext) {
+ val currentDatabaseParams = paramDao.getAll()
+ val newParams = params.map { param ->
+ val currentDatabaseParam = currentDatabaseParams.find { it.name == param.name }
+ KernelParamDTO.fromKernelParam(param).copy(
+ id = currentDatabaseParam?.id ?: 0
+ )
+ }
+
+ paramDao.upsertAll(newParams)
+ }
+
+ override suspend fun removeUserParam(param: KernelParam) = withContext(coroutineContext) {
+ paramDao.delete(KernelParamDTO.fromKernelParam(param))
+ }
+
+ 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..c160563
--- /dev/null
+++ b/data/src/main/java/com/androidvip/sysctlgui/data/source/OnlineDocumentationDataSource.kt
@@ -0,0 +1,123 @@
+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 java.io.File
+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('_', '-')
+
+
+ if (File(param.path).isDirectory) {
+ // If we got something out of the request, might as well return at least the URL
+ return@withContext ParamDocumentation(
+ title = param.name,
+ documentationText = "",
+ documentationHtml = "", // HTML might be huge (directory documentation)
+ url = url
+ )
+ }
+
+ val elements = document.select("section#$htmlElementId :not(h2)")
+
+ if (elements.isEmpty()) {
+ Log.w(
+ "OnlineDocRepo",
+ "No documentation found for ${param.name} with id $htmlElementId on $url"
+ )
+ return@withContext null
+ } else {
+ // Remove first element (usually a heading remnant)
+ elements.removeAt(0)
+ }
+
+ 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 {
+ if (File(param.path).isDirectory) {
+ return "${DOC_BASE_URL}${param.name}.html"
+ }
+ val configName = param.groupName
+ return "${DOC_BASE_URL}$configName.html#${param.lastNameSegment.replace('_', '-')}"
+ }
+
+ /**
+ * 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(
+ "
+ "", // Handles
+ ""
+ )
+ .replace("", "") // Universal closer for the above
+ .removeSuffix("
")
+ }
+
+ 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..8ec44f9
--- /dev/null
+++ b/data/src/main/java/com/androidvip/sysctlgui/data/utils/PresetsFileProcessor.kt
@@ -0,0 +1,59 @@
+package com.androidvip.sysctlgui.data.utils
+
+import android.content.ContentResolver
+import android.net.Uri
+import android.util.Log
+import com.androidvip.sysctlgui.domain.exceptions.NoParameterFoundException
+import com.androidvip.sysctlgui.domain.models.KernelParam
+import com.androidvip.sysctlgui.utils.isValidSysctlOutputLine
+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()
+ val params = lines.mapNotNull { line ->
+ if (line.isValidSysctlOutputLine()) {
+ runCatching {
+ KernelParam.createFromName(
+ name = line.substringBefore('=').trim(),
+ value = line.substringAfter('=').trim(),
+ isFavorite = true
+ )
+ }.getOrNull()
+ } else {
+ Log.w("PresetsFileProcessor", "Invalid line: $line")
+ null
+ }
+ }
+
+ if (params.isEmpty()) {
+ throw NoParameterFoundException()
+ }
+
+ params
+ } ?: 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 54c0620..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,44 +1,50 @@
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 isBusyboxAvailable(): Boolean = withContext(dispatcher) {
- val results: List = Shell.sh("which busybox").exec().out
- return@withContext if (ShellUtils.isValidOutput(results)) {
- results.first().isNotEmpty()
- } else false
+ suspend fun isRootAvailable(): Boolean = withContext(shellDispatcher) {
+ Shell.isAppGrantedRoot() == true
}
- suspend fun executeWithOutput(
- command: String,
- defaultOutput: String = "",
- forEachLine: ((String) -> Unit)? = null
- ): String = withContext(dispatcher) {
- return@withContext runCatching {
- buildString {
- val outputs = Shell.su(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)
+ suspend fun isBusyboxAvailable(): Boolean = withContext(shellDispatcher) {
+ val results: List = Shell.cmd("which busybox").exec().out
+ return@withContext ShellUtils.isValidOutput(results) && results.firstOrNull()
+ ?.isNotEmpty() == true
}
+ 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 {
Shell.getCachedShell()?.close()
diff --git a/data/src/main/res/drawable/ic_code.xml b/data/src/main/res/drawable/ic_code.xml
new file mode 100644
index 0000000..369840b
--- /dev/null
+++ b/data/src/main/res/drawable/ic_code.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/data/src/main/res/drawable/ic_group.xml b/data/src/main/res/drawable/ic_group.xml
new file mode 100644
index 0000000..e6b060c
--- /dev/null
+++ b/data/src/main/res/drawable/ic_group.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/data/src/main/res/drawable/ic_language.xml b/data/src/main/res/drawable/ic_language.xml
new file mode 100644
index 0000000..643d3fc
--- /dev/null
+++ b/data/src/main/res/drawable/ic_language.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
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
diff --git a/data/src/main/res/values-pt-rBR/strings.xml b/data/src/main/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000..73809c7
--- /dev/null
+++ b/data/src/main/res/values-pt-rBR/strings.xml
@@ -0,0 +1,40 @@
+
+
+ Geral
+ Listar pastas primeiro
+ Listar as pastas primeiro ao usar a opção do navegador de parâmetros do kernel
+ Adivinhar tipo de entrada
+ Tentar definir o melhor tipo de entrada para o teclado virtual com base no valor do parâmetro
+ Usar documentação online
+ Tentar a usar documentação online ao exibir a descrições dos parâmetros
+ Tema
+ Forçar Modo Escuro
+ Forçar tema escuro quando disponível
+ Cores Dinâmicas
+ Usar cores dinâmicas quando disponíveis
+ Nível de contraste
+ Nível de contraste para as cores do tema
+ Operações
+ Modo de aplicação (%s)
+ Comando usado ao aplicar o valor do parâmetro
+ Usar busybox
+ Use o busybox para executar comandos, se disponível
+ Permitir valores em branco
+ Inicialização
+ Executar na inicialização
+ Permitir que o aplicativo aplique parâmetros na inicialização
+ Atraso na inicialização
+ Atraso em segundos antes de reaplicar os parâmetros na inicialização
+ Gerenciar parâmetros
+ Gerenciar os parâmetros que serão aplicados na inicialização
+ Mostrar toast do Tasker
+ Mostrar uma mensagem quando um parâmetro do Tasker for aplicado
+ Histórico de pesquisa
+ Toque para limpar o histórico de pesquisa
+ Outros
+ Código fonte
+ Confira o código fonte no GitHub!
+ Contribuidores
+ Traduções
+ Ajude a traduzir este aplicativo
+
\ No newline at end of file
diff --git a/data/src/main/res/values/strings.xml b/data/src/main/res/values/strings.xml
new file mode 100644
index 0000000..2b01f67
--- /dev/null
+++ b/data/src/main/res/values/strings.xml
@@ -0,0 +1,40 @@
+
+
+ General
+ List folders first
+ List folders first when using the kernel parameter browser option
+ Guess input type
+ Try to set the best input type for the soft keyboard based on the value of the parameter
+ Use online documentation
+ Try to use online documentation when displaying parameter descriptions
+ Theme
+ Force Dark
+ Force dark theme when available
+ Dynamic Colors
+ Use dynamic colors when available
+ Contrast level
+ Contrast level for the theme colors
+ Operations
+ Commit mode (%s)
+ Command used when applying the parameter value
+ Use busybox
+ Use busybox to execute commands, if available
+ Allow blank values
+ Startup
+ Run on startup
+ Allow the application to apply parameters on startup
+ Startup delay
+ Delay in seconds before applying parameters on startup
+ Manage parameters
+ Manage the parameters that will be applied at startup
+ Show Tasker toast
+ Show toast when a Tasker parameter is applied
+ Search history
+ Tap to clear search history
+ Others
+ Source code
+ Check the sauce code on GitHub!
+ Contributors
+ Translations
+ Help translate this application
+
diff --git a/data/src/test/java/com/androidvip/sysctlgui/data/ExampleUnitTest.kt b/data/src/test/java/com/androidvip/sysctlgui/data/ExampleUnitTest.kt
deleted file mode 100644
index 2365b1c..0000000
--- a/data/src/test/java/com/androidvip/sysctlgui/data/ExampleUnitTest.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.androidvip.sysctlgui.data
-
-import org.junit.Test
-
-import org.junit.Assert.*
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-class ExampleUnitTest {
- @Test
- fun addition_isCorrect() {
- assertEquals(4, 2 + 2)
- }
-}
\ No newline at end of file
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/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/app/src/main/kotlin/com/androidvip/sysctlgui/helpers/Actions.kt b/domain/src/main/java/com/androidvip/sysctlgui/domain/enums/Actions.kt
similarity index 60%
rename from app/src/main/kotlin/com/androidvip/sysctlgui/helpers/Actions.kt
rename to domain/src/main/java/com/androidvip/sysctlgui/domain/enums/Actions.kt
index 5750be5..347b501 100644
--- a/app/src/main/kotlin/com/androidvip/sysctlgui/helpers/Actions.kt
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/enums/Actions.kt
@@ -1,9 +1,8 @@
-package com.androidvip.sysctlgui.helpers
+package com.androidvip.sysctlgui.domain.enums
enum class Actions {
BrowseParams,
- ListParams,
ExportParams,
OpenSettings,
EditParam
-}
+}
\ No newline at end of file
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..e2a5cdb
--- /dev/null
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/models/AppSetting.kt
@@ -0,0 +1,39 @@
+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.
+ * @property iconResource An optional resource ID for an icon that can be displayed with the setting.
+ */
+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,
+ val iconResource: Int? = null
+)
+
+const val KEY_MANAGE_PARAMS = "manageParams"
+const val KEY_DELETE_HISTORY = "deleteHistory"
+const val KEY_SOURCE_CODE = "sauce"
+const val KEY_CONTRIBUTORS = "contributors"
+const val KEY_TRANSLATIONS = "translations"
\ No newline at end of file
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..9759f98
--- /dev/null
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/models/KernelParam.kt
@@ -0,0 +1,142 @@
+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()
+
+ override fun toString(): String {
+ return "$name=$value"
+ }
+
+ 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 = name,
+ path = derivedPath,
+ value = value,
+ isFavorite = 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, path = path, value = value)
+ }
+ }
+}
+
+/**
+ * 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..16f7f7d 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,6 +1,15 @@
package com.androidvip.sysctlgui.domain.repository
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * 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 {
+ fun observeKey(key: String, default: T): Flow
var listFoldersFirst: Boolean
var guessInputType: Boolean
var commitMode: String
@@ -9,8 +18,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..062e1fc 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,65 @@
package com.androidvip.sysctlgui.domain.repository
-import com.androidvip.sysctlgui.domain.models.DomainKernelParam
+import com.androidvip.sysctlgui.domain.enums.CommitMode
+import com.androidvip.sysctlgui.domain.models.KernelParam
+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?
- suspend fun addUserParam(param: DomainKernelParam, allowBlank: Boolean)
- suspend fun addUserParams(params: List, allowBlank: Boolean)
- suspend fun removeUserParam(param: DomainKernelParam)
- suspend fun clearUserParams()
+ /**
+ * 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 performDatabaseMigration()
+ /**
+ * 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 importParamsFromJson(stream: InputStream): List
- suspend fun importParamsFromConf(stream: InputStream): List
- suspend fun exportParams(params: List, fileDescriptor: FileDescriptor)
- suspend fun backupParams(params: List, fileDescriptor: FileDescriptor)
+ /**
+ * 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>
}
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..953acd6
--- /dev/null
+++ b/domain/src/main/java/com/androidvip/sysctlgui/domain/repository/UserRepository.kt
@@ -0,0 +1,49 @@
+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 userParamsFlow: Flow>
+
+ suspend fun getUserParams(): List
+
+ 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..afa5561
--- /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 + message)
+ }
+}
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..cefe97c 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,7 @@
package com.androidvip.sysctlgui.domain.usecase
-import com.androidvip.sysctlgui.domain.repository.ParamsRepository
+import com.androidvip.sysctlgui.domain.repository.UserRepository
-class GetUserParamsUseCase(private val repository: ParamsRepository) {
+class GetUserParamsUseCase(private val repository: UserRepository) {
suspend operator fun invoke() = repository.getUserParams()
}
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..b4a2095
--- /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.exceptions.BlankValueNotAllowedException
+import com.androidvip.sysctlgui.domain.models.KernelParam
+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)
+ }
+}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..b18e73f
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,88 @@
+[versions]
+agp = "8.12.0"
+coreSplashscreen = "1.0.1"
+composeMaterial = "1.8.3"
+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"
+ktor = "3.2.3"
+lifecycle = "2.9.2"
+activityCompose = "1.10.1"
+composeBom = "2025.07.00"
+appcompat = "1.7.1"
+materialIconsCoreCompose = "1.7.8"
+material = "1.12.0"
+multidex = "2.0.1"
+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"
+workRuntimeKtx = "2.10.3"
+glanceAppwidget = "1.1.1"
+
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
+androidx-material = { module = "androidx.compose.material:material", version.ref = "composeMaterial" }
+androidx-material-icons-core = { module = "androidx.compose.material:material-icons-core", version.ref = "materialIconsCoreCompose" }
+androidx-multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" }
+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" }
+androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtx" }
+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-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-glance-appwidget = { group = "androidx.glance", name = "glance-appwidget", version.ref = "glanceAppwidget" }
+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-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" }
+material = { module = "com.google.android.material:material", version.ref = "material" }
+
+[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/img/icon.png b/img/icon.png
new file mode 100644
index 0000000..4b35a94
Binary files /dev/null and b/img/icon.png differ
diff --git a/img/labs_badge.png b/img/labs_badge.png
deleted file mode 100644
index 3a068bc..0000000
Binary files a/img/labs_badge.png and /dev/null differ
diff --git a/img/ss01.png b/img/ss01.png
new file mode 100644
index 0000000..193109f
Binary files /dev/null and b/img/ss01.png differ
diff --git a/img/ss02.png b/img/ss02.png
new file mode 100644
index 0000000..dd5429a
Binary files /dev/null and b/img/ss02.png differ
diff --git a/img/ss03.png b/img/ss03.png
new file mode 100644
index 0000000..188199c
Binary files /dev/null and b/img/ss03.png differ
diff --git a/img/ss04.png b/img/ss04.png
new file mode 100644
index 0000000..023c4f5
Binary files /dev/null and b/img/ss04.png differ
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")